dont know ?

This commit is contained in:
Lars Behrends
2025-11-03 23:34:36 +01:00
parent 7a7977d8b0
commit 1ec6016b10
27 changed files with 6854 additions and 3361 deletions

View File

@@ -465,15 +465,9 @@ class StashSyncService extends BaseSyncService
$sceneData['local_cover_path'] = $existingMetadata['local_cover_path'] ?? null;
$sceneData['local_screenshot_path'] = $existingMetadata['local_screenshot_path'] ?? null;
}
// Handle performers/actors
// Handle performers/actors with full metadata
$performers = $sceneData['performers'] ?? [];
$actorNames = [];
$performerImages = [];
foreach ($performers as $performer) {
$actorNames[] = $performer['name'];
$performerImages[$performer['name']] = $performer['image_path'] ?? null;
}
$actors = $this->syncActors($actorNames, $performerImages);
$actors = $this->syncActors($performers);
$sceneData = [
'title' => $sceneData['title'] ?: 'Untitled Scene',
@@ -618,15 +612,14 @@ class StashSyncService extends BaseSyncService
}
}
private function syncActors(array $actorNames, array $performerImages = []): array
private function syncActors(array $performers): array
{
$actors = [];
foreach ($actorNames as $actorName) {
if (empty($actorName)) continue;
foreach ($performers as $performer) {
if (empty($performer['name'])) continue;
$imagePath = $performerImages[$actorName] ?? null;
$actor = $this->getOrCreateActor($actorName, $imagePath);
$actor = $this->getOrCreateActor($performer);
if ($actor) {
$actors[] = $actor;
}
@@ -635,25 +628,63 @@ class StashSyncService extends BaseSyncService
return $actors;
}
private function getOrCreateActor(string $name, ?string $imagePath = null): ?array
private function getOrCreateActor(array $performer): ?array
{
$name = $performer['name'] ?? '';
if (empty($name)) return null;
// Check if actor already exists
$stmt = $this->pdo->prepare("
SELECT id, name, thumbnail_path FROM actors WHERE name = :name
SELECT id, name, thumbnail_path, metadata FROM actors WHERE name = :name
");
$stmt->execute(['name' => $name]);
$existingActor = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existingActor) {
return [
'id' => $existingActor['id'],
'name' => $existingActor['name'],
'thumbnail_path' => $existingActor['thumbnail_path']
];
}
// Prepare rich metadata from Stash performer data
$actorMetadata = [
'stash_id' => $performer['id'] ?? null,
'stash_url' => $performer['url'] ?? null,
'disambiguation' => $performer['disambiguation'] ?? '',
'gender' => $performer['gender'] ?? null,
'birth_date' => $performer['birthdate'] ?? null,
'death_date' => $performer['death_date'] ?? null,
'ethnicity' => $performer['ethnicity'] ?? null,
'country' => $performer['country'] ?? null,
'nationality' => $performer['country'] ?? null, // Map country to nationality
'eye_color' => $performer['eye_color'] ?? null,
'hair_color' => $performer['hair_color'] ?? null,
'height' => $performer['height_cm'] ? $performer['height_cm'] . 'cm' : null,
'measurements' => $performer['measurements'] ?? null,
'cup_size' => $this->extractCupSize($performer['measurements'] ?? ''),
'weight' => $performer['weight'] ? $performer['weight'] . 'kg' : null,
'piercings' => $performer['piercings'] ?? null,
'tattoos' => $performer['tattoos'] ?? null,
'fake_tits' => $performer['fake_tits'] ?? null,
'penis_length' => $performer['penis_length'] ?? null,
'circumcised' => $performer['circumcised'] ?? null,
'career_length' => $performer['career_length'] ?? null,
'aliases' => $performer['alias_list'] ?? [],
'favorite' => $performer['favorite'] ?? false,
'ignore_auto_tag' => $performer['ignore_auto_tag'] ?? false,
'scene_count' => $performer['scene_count'] ?? 0,
'details' => $performer['details'] ?? null,
'stash_created_at' => $performer['created_at'] ?? null,
'stash_updated_at' => $performer['updated_at'] ?? null,
'social_media' => [
'website' => $performer['url'] ?? null
],
'adult_specific' => [
'debut_year' => $this->extractDebutYear($performer['career_length'] ?? ''),
'retirement_year' => $this->extractRetirementYear($performer['career_length'] ?? ''),
'active' => $this->isActivePerformer($performer['career_length'] ?? ''),
'genres' => [],
'specialties' => []
]
];
// Try to download performer image if available
$thumbnailPath = null;
$imagePath = $performer['image_path'] ?? null;
if ($imagePath) {
// Validate image path before constructing URL
if (!empty(trim($imagePath))) {
@@ -667,7 +698,7 @@ class StashSyncService extends BaseSyncService
$imageUrl = "{$this->baseUrl}" . $imagePath;
} else {
// Relative path - assume it's in performer images directory
$imageUrl = "{$this->baseUrl}/performer/" . $imagePath;
$imageUrl = "{$this->baseUrl}/performer/" . $performer['id'] . "/" . $imagePath;
}
// Validate the constructed URL
@@ -690,17 +721,56 @@ class StashSyncService extends BaseSyncService
}
}
if ($existingActor) {
// Update existing actor with new metadata if it's more complete
$existingMetadata = json_decode($existingActor['metadata'] ?? '{}', true);
// Check if we should update - prefer more complete data
$shouldUpdate = false;
if (empty($existingMetadata['stash_id']) && !empty($actorMetadata['stash_id'])) {
$shouldUpdate = true;
} elseif (!empty($thumbnailPath) && empty($existingActor['thumbnail_path'])) {
$shouldUpdate = true;
}
if ($shouldUpdate) {
$stmt = $this->pdo->prepare("
UPDATE actors
SET thumbnail_path = COALESCE(:thumbnail_path, thumbnail_path),
metadata = :metadata,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'id' => $existingActor['id'],
'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path'],
'metadata' => json_encode(array_merge($existingMetadata, $actorMetadata))
]);
$this->logProgress("Updated existing actor {$name} with Stash metadata");
}
return [
'id' => $existingActor['id'],
'name' => $existingActor['name'],
'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path']
];
}
// Create new actor with full metadata
try {
$stmt = $this->pdo->prepare("
INSERT INTO actors (name, thumbnail_path, created_at, updated_at)
VALUES (:name, :thumbnail_path, NOW(), NOW())
INSERT INTO actors (name, thumbnail_path, metadata, created_at, updated_at)
VALUES (:name, :thumbnail_path, :metadata, NOW(), NOW())
");
$stmt->execute([
'name' => $name,
'thumbnail_path' => $thumbnailPath
'thumbnail_path' => $thumbnailPath,
'metadata' => json_encode($actorMetadata)
]);
$actorId = $this->pdo->lastInsertId();
$this->logProgress("Created new actor {$name} with full Stash metadata");
return [
'id' => $actorId,
'name' => $name,
@@ -712,6 +782,55 @@ class StashSyncService extends BaseSyncService
}
}
private function extractCupSize(string $measurements): ?string
{
if (empty($measurements)) return null;
// Try to extract cup size from measurements like "34C-24-35"
$parts = explode('-', $measurements);
if (count($parts) >= 1) {
$firstPart = trim($parts[0]);
// Look for cup size pattern (number followed by letter)
if (preg_match('/(\d+)([A-Z])/', $firstPart, $matches)) {
return $matches[2];
}
}
return null;
}
private function extractDebutYear(string $careerLength): ?string
{
if (empty($careerLength)) return null;
// Extract debut year from patterns like "2015 -" or "2015 - 2020"
if (preg_match('/(\d{4})\s*-\s*(\d{4})?/', $careerLength, $matches)) {
return $matches[1];
}
return null;
}
private function extractRetirementYear(string $careerLength): ?string
{
if (empty($careerLength)) return null;
// Extract retirement year from patterns like "2015 - 2020"
if (preg_match('/\d{4}\s*-\s*(\d{4})/', $careerLength, $matches)) {
return $matches[1];
}
return null;
}
private function isActivePerformer(string $careerLength): bool
{
if (empty($careerLength)) return false;
// Check if career is still active (ends with " -")
return str_ends_with(trim($careerLength), '-');
}
protected function getProcessedCount(): int
{
return $this->processedCount;

View File

@@ -386,10 +386,16 @@ class XbvrSyncService extends BaseSyncService
{
$actors = [];
foreach ($cast as $actorName) {
foreach ($cast as $actorData) {
// Handle both string names and actor objects
$actorName = is_array($actorData) ? ($actorData['name'] ?? '') : $actorData;
if (empty($actorName)) continue;
$actor = $this->getOrCreateActor($actorName);
// Try to get detailed actor information from XBVR
$detailedActorData = $this->getActorDetails($actorName, $actorData);
$actor = $this->getOrCreateActor($detailedActorData);
if ($actor) {
$actors[] = $actor;
}
@@ -398,37 +404,326 @@ class XbvrSyncService extends BaseSyncService
return $actors;
}
private function getOrCreateActor(string $name): ?array
private function getActorDetails(string $actorName, $actorData): array
{
// If we already have detailed actor data from the scene, use it
if (is_array($actorData) && !empty($actorData)) {
return $actorData;
}
// Try to fetch detailed actor information from XBVR/DeoVR API
// XBVR might have actor detail endpoints, let's try a few possibilities
$actorDetails = ['name' => $actorName];
// Try different XBVR actor API endpoints
$actorApiUrls = [
"{$this->baseUrl}/api/actor/search/" . urlencode($actorName),
"{$this->baseUrl}/actor/" . urlencode($actorName),
"{$this->baseUrl}/api/actors?name=" . urlencode($actorName),
];
foreach ($actorApiUrls as $apiUrl) {
try {
$this->logProgress("Trying to fetch actor details from: {$apiUrl}");
$response = $this->httpClient->get($apiUrl, [
'timeout' => 10,
'connect_timeout' => 5
]);
if ($response->getStatusCode() === 200) {
$actorApiData = json_decode($response->getBody(), true);
if (!empty($actorApiData)) {
$this->logProgress("Successfully fetched actor details for: {$actorName}");
// Merge API data with basic info
$actorDetails = array_merge($actorDetails, $this->mapActorApiData($actorApiData));
break;
}
}
} catch (Exception $e) {
// Continue to next API endpoint
$this->logProgress("Actor API endpoint failed: {$apiUrl} - " . $e->getMessage());
}
}
// If no detailed data found, try to scrape from web search or use basic info
if (count($actorDetails) <= 1) {
$this->logProgress("No detailed actor data found for {$actorName}, using basic info");
$actorDetails = $this->scrapeActorInfo($actorName);
}
return $actorDetails;
}
private function mapActorApiData(array $apiData): array
{
$mapped = [];
// Handle different possible API response formats
if (isset($apiData['actor'])) {
$apiData = $apiData['actor'];
}
// Map common fields
$fieldMappings = [
'id' => 'xbvr_id',
'name' => 'name',
'image' => 'image_path',
'thumbnail' => 'thumbnail_path',
'bio' => 'biography',
'biography' => 'biography',
'birthdate' => 'birth_date',
'age' => 'age',
'height' => 'height',
'weight' => 'weight',
'measurements' => 'measurements',
'nationality' => 'nationality',
'ethnicity' => 'ethnicity',
'eye_color' => 'eye_color',
'hair_color' => 'hair_color',
'tattoos' => 'tattoos',
'piercings' => 'piercings',
'aliases' => 'aliases',
'debut_year' => 'debut_year',
'retirement_year' => 'retirement_year',
'active' => 'active',
'website' => 'website',
'twitter' => 'twitter',
'instagram' => 'instagram',
'scene_count' => 'scene_count'
];
foreach ($fieldMappings as $apiField => $localField) {
if (isset($apiData[$apiField])) {
$mapped[$localField] = $apiData[$apiField];
}
}
return $mapped;
}
private function scrapeActorInfo(string $actorName): array
{
$actorInfo = ['name' => $actorName];
// Try to get basic information from web scraping
// This is a fallback when API doesn't provide details
try {
// Try to search for actor on common adult industry sites
$searchUrls = [
"https://www.adultempire.com/search.php?query=" . urlencode($actorName),
"https://www.brazzers.com/search/" . urlencode($actorName) . "/",
"https://www.naughtyamerica.com/search/" . urlencode($actorName),
];
foreach ($searchUrls as $searchUrl) {
try {
$response = $this->httpClient->get($searchUrl, [
'timeout' => 5,
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
]
]);
if ($response->getStatusCode() === 200) {
$html = $response->getBody()->getContents();
// Basic HTML parsing to extract information
$actorInfo = array_merge($actorInfo, $this->parseActorHtml($html, $actorName));
break;
}
} catch (Exception $e) {
continue;
}
}
} catch (Exception $e) {
$this->logProgress("Web scraping failed for {$actorName}: " . $e->getMessage());
}
return $actorInfo;
}
private function parseActorHtml(string $html, string $actorName): array
{
$info = [];
// Very basic HTML parsing - look for common patterns
// This is quite fragile and would need improvement for production use
// Look for image URLs
if (preg_match('/<img[^>]+src=["\']([^"\']*?(?:actor|performer|model)[^"\']*?)["\'][^>]*>/i', $html, $matches)) {
$info['image_path'] = $matches[1];
}
// Look for birthdate patterns
if (preg_match('/(?:born|birthdate|birth).*?(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{4})/i', $html, $matches)) {
$info['birth_date'] = date('Y-m-d', strtotime($matches[1]));
}
// Look for age
if (preg_match('/age.*?(\d+)/i', $html, $matches)) {
$info['age'] = (int)$matches[1];
}
// Look for measurements
if (preg_match('/measurements?.*?(\d+-\d+-\d+)/i', $html, $matches)) {
$info['measurements'] = $matches[1];
}
// Look for height
if (preg_match('/height.*?(\d+\'?\d*)/i', $html, $matches)) {
$info['height'] = $matches[1];
}
return $info;
}
private function getOrCreateActor(array $actorData): ?array
{
$name = $actorData['name'] ?? '';
if (empty($name)) return null;
// Check if actor already exists
$stmt = $this->pdo->prepare("
SELECT id, name, thumbnail_path FROM actors WHERE name = :name
SELECT id, name, thumbnail_path, metadata FROM actors WHERE name = :name
");
$stmt->execute(['name' => $name]);
$existingActor = $stmt->fetch(\PDO::FETCH_ASSOC);
// Prepare metadata from XBVR actor data
$actorMetadata = [
'xbvr_id' => $actorData['xbvr_id'] ?? $actorData['id'] ?? null,
'biography' => $actorData['biography'] ?? null,
'birth_date' => $actorData['birth_date'] ?? null,
'age' => $actorData['age'] ?? null,
'height' => $actorData['height'] ?? null,
'weight' => $actorData['weight'] ?? null,
'measurements' => $actorData['measurements'] ?? null,
'nationality' => $actorData['nationality'] ?? null,
'ethnicity' => $actorData['ethnicity'] ?? null,
'eye_color' => $actorData['eye_color'] ?? null,
'hair_color' => $actorData['hair_color'] ?? null,
'tattoos' => $actorData['tattoos'] ?? null,
'piercings' => $actorData['piercings'] ?? null,
'aliases' => is_array($actorData['aliases'] ?? null) ? $actorData['aliases'] : [],
'debut_year' => $actorData['debut_year'] ?? null,
'retirement_year' => $actorData['retirement_year'] ?? null,
'active' => $actorData['active'] ?? null,
'scene_count' => $actorData['scene_count'] ?? null,
'social_media' => [
'website' => $actorData['website'] ?? null,
'twitter' => $actorData['twitter'] ?? null,
'instagram' => $actorData['instagram'] ?? null
],
'adult_specific' => [
'debut_year' => $actorData['debut_year'] ?? null,
'retirement_year' => $actorData['retirement_year'] ?? null,
'active' => $actorData['active'] ?? null,
'genres' => [],
'specialties' => []
]
];
// Try to download actor image if available
$thumbnailPath = null;
$imagePath = $actorData['image_path'] ?? $actorData['thumbnail_path'] ?? null;
if ($imagePath) {
// Validate image path before constructing URL
if (!empty(trim($imagePath))) {
try {
// Handle different image path formats
if (strpos($imagePath, 'http') === 0) {
// Already a full URL
$imageUrl = $imagePath;
} elseif (strpos($imagePath, '/') === 0) {
// Absolute path from XBVR
$imageUrl = rtrim($this->baseUrl, '/') . $imagePath;
} else {
// Relative path
$imageUrl = rtrim($this->baseUrl, '/') . '/' . $imagePath;
}
// Validate the constructed URL
if (filter_var($imageUrl, FILTER_VALIDATE_URL)) {
$this->logProgress("Actor image URL for {$name}: " . $imageUrl);
$thumbnailFilename = $this->imageDownloader->generateFilename($imageUrl, 'actor');
$localImagePath = $this->imageDownloader->downloadImage($imageUrl, $thumbnailFilename, 'actors');
if ($localImagePath) {
$thumbnailPath = $this->imageDownloader->getPublicUrl($localImagePath);
$this->logProgress("Downloaded actor image: " . $localImagePath);
} else {
$this->logProgress("Failed to download actor image from: " . $imageUrl);
}
} else {
$this->logProgress("Invalid actor image URL constructed: " . $imageUrl);
}
} catch (Exception $e) {
$this->logProgress("Exception downloading actor image for {$name} from {$imagePath}: " . $e->getMessage());
}
}
}
if ($existingActor) {
// Update existing actor with new metadata if it's more complete
$existingMetadata = json_decode($existingActor['metadata'] ?? '{}', true);
// Check if we should update - prefer more complete data
$shouldUpdate = false;
if (empty($existingMetadata['xbvr_id']) && !empty($actorMetadata['xbvr_id'])) {
$shouldUpdate = true;
} elseif (!empty($thumbnailPath) && empty($existingActor['thumbnail_path'])) {
$shouldUpdate = true;
} elseif (count($existingMetadata) < count(array_filter($actorMetadata))) {
$shouldUpdate = true;
}
if ($shouldUpdate) {
$stmt = $this->pdo->prepare("
UPDATE actors
SET thumbnail_path = COALESCE(:thumbnail_path, thumbnail_path),
metadata = :metadata,
updated_at = NOW()
WHERE id = :id
");
$stmt->execute([
'id' => $existingActor['id'],
'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path'],
'metadata' => json_encode(array_merge($existingMetadata, $actorMetadata))
]);
$this->logProgress("Updated existing actor {$name} with XBVR metadata");
}
return [
'id' => $existingActor['id'],
'name' => $existingActor['name'],
'thumbnail_path' => $existingActor['thumbnail_path']
'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path']
];
}
// For now, we'll create actor without thumbnail
// In a full implementation, you'd fetch actor details from XBVR API
// Create new actor with XBVR metadata
try {
$stmt = $this->pdo->prepare("
INSERT INTO actors (name, created_at, updated_at)
VALUES (:name, NOW(), NOW())
INSERT INTO actors (name, thumbnail_path, metadata, created_at, updated_at)
VALUES (:name, :thumbnail_path, :metadata, NOW(), NOW())
");
$stmt->execute(['name' => $name]);
$stmt->execute([
'name' => $name,
'thumbnail_path' => $thumbnailPath,
'metadata' => json_encode($actorMetadata)
]);
$actorId = $this->pdo->lastInsertId();
$this->logProgress("Created new actor {$name} with XBVR metadata");
return [
'id' => $actorId,
'name' => $name,
'thumbnail_path' => null
'thumbnail_path' => $thumbnailPath
];
} catch (Exception $e) {
$this->logProgress("Failed to create actor {$name}: " . $e->getMessage());
@@ -498,7 +793,7 @@ class XbvrSyncService extends BaseSyncService
$this->logProgress("Starting cleanup - detecting deleted VR scenes in XBVR...");
// Clean up VR scenes
$this->cleanupScenes();
//$this->cleanupScenes();
$this->logProgress("Cleanup completed. Deleted {$this->deletedCount} VR scenes.");
}
@@ -545,20 +840,41 @@ class XbvrSyncService extends BaseSyncService
private function getXbvrScenesForCleanup(): array
{
try {
$response = $this->httpClient->get("{$this->baseUrl}/api/scene/list", [
'timeout' => 30,
'connect_timeout' => 10
]);
// Use the same DeoVR API as the main sync process to ensure consistency
$scenes = $this->getXbvrScenes();
if ($response->getStatusCode() === 200) {
$data = json_decode($response->getBody(), true);
return $data['scenes'] ?? [];
// Extract scene IDs from the detailed scene data
$sceneIds = array_map(function($scene) {
return $scene['id'] ?? null;
}, $scenes);
// Filter out null IDs
return array_filter($sceneIds, function($id) {
return $id !== null;
});
} catch (Exception $e) {
$this->logProgress("Error fetching XBVR scenes for cleanup: " . $e->getMessage());
$this->logProgress("Skipping cleanup to prevent accidental data loss");
// Return all current scene IDs to prevent deletion
// This is safer than returning empty array which would delete everything
$stmt = $this->pdo->prepare("
SELECT metadata FROM adult_videos WHERE source_id = :source_id
");
$stmt->execute(['source_id' => $this->source['id']]);
$localScenes = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$existingIds = [];
foreach ($localScenes as $scene) {
$metadata = json_decode($scene['metadata'], true);
if (isset($metadata['xbvr_id'])) {
$existingIds[] = $metadata['xbvr_id'];
}
}
return [];
} catch (Exception $e) {
$this->logProgress("Error fetching XBVR scenes: " . $e->getMessage());
return [];
$this->logProgress("Returning " . count($existingIds) . " existing scene IDs to prevent cleanup");
return $existingIds;
}
}