baseUrl = rtrim($source['api_url'], '/'); $this->httpClient = new Client([ 'timeout' => 30, 'headers' => [ 'User-Agent' => 'MediaCollector/1.0' ] ]); $this->imageDownloader = new ImageDownloader(__DIR__ . '/../../storage/images'); } protected function executeSync(string $syncType): void { if (empty($this->baseUrl)) { throw new Exception('XBVR URL not configured'); } $this->logProgress('Starting XBVR library sync...'); // Sync VR scenes $this->syncScenes(); $this->logProgress("Processed {$this->processedCount} XBVR items"); } private function syncScenes(): void { try { $scenes = $this->getXbvrScenes(); foreach ($scenes as $sceneData) { try { $this->syncScene($sceneData); $this->processedCount++; } catch (Exception $e) { $this->logProgress("Error processing XBVR scene {$sceneData['id']}: " . $e->getMessage()); $this->processedCount++; // Still count as processed even if failed } } } catch (Exception $e) { $this->logProgress('Error syncing XBVR scenes: ' . $e->getMessage()); } } private function getXbvrScenes(): array { try { $this->logProgress("Fetching XBVR DeoVR main response from: {$this->baseUrl}/deovr"); // Step 1: Fetch the main DeoVR response containing the video list $response = $this->httpClient->get("{$this->baseUrl}/deovr", [ 'timeout' => 30, 'connect_timeout' => 10 ]); if ($response->getStatusCode() !== 200) { throw new Exception("XBVR DeoVR API returned status: " . $response->getStatusCode()); } $mainData = json_decode($response->getBody(), true); $this->logProgress("XBVR DeoVR main response received successfully"); // Step 2: Extract the video list from the main response $videoList = $this->extractVideoList($mainData); $videoList = $videoList[0]['list']; if (empty($videoList)) { throw new Exception("No videos found in XBVR DeoVR response"); } $this->logProgress("Found " . count($videoList) . " videos in XBVR list"); // Step 3: Fetch detailed information for each video $detailedScenes = []; $processedCount = 0; foreach ($videoList as $videoItem) { try { $detailUrl = $this->extractDetailUrl($videoItem); if (!$detailUrl) { $this->logProgress("No detail URL found for video: " . ($videoItem['title'] ?? 'Unknown')); continue; } $this->logProgress("Fetching details for: " . ($videoItem['title'] ?? 'Unknown')); $detailResponse = $this->httpClient->get($detailUrl, [ 'timeout' => 30, 'connect_timeout' => 10 ]); if ($detailResponse->getStatusCode() === 200) { $detailData = json_decode($detailResponse->getBody(), true); $detailedScenes[] = $detailData; $processedCount++; $this->logProgress("Successfully fetched details for: " . ($detailData['title'] ?? 'Unknown')); } else { $this->logProgress("Failed to fetch details from {$detailUrl}: Status " . $detailResponse->getStatusCode()); } // Add small delay to be respectful to the API usleep(100000); // 0.1 second delay } catch (Exception $e) { $this->logProgress("Error fetching details for video: " . $e->getMessage()); } } $this->logProgress("Successfully processed {$processedCount} out of " . count($videoList) . " videos"); return $detailedScenes; } catch (Exception $e) { throw new Exception('Failed to fetch XBVR scenes: ' . $e->getMessage()); } } private function syncScene(array $sceneData): void { $this->logProgress("Processing XBVR scene: " . json_encode(array_slice($sceneData, 0, 5))); $adultVideoModel = new AdultVideo($this->pdo); // Check if scene already exists by xbvr_id in metadata $stmt = $this->pdo->prepare(" SELECT id, metadata FROM adult_videos WHERE source_id = :source_id "); $stmt->execute(['source_id' => $this->source['id']]); $existingScenes = $stmt->fetchAll(\PDO::FETCH_ASSOC); $existingScene = null; foreach ($existingScenes as $scene) { $metadata = json_decode($scene['metadata'], true); if (isset($metadata['xbvr_id']) && $metadata['xbvr_id'] === $sceneData['id']) { $existingScene = $scene; break; } } // Map XBVR/DeoVR fields to our database structure // Based on the 46367.json example structure $mappedData = [ 'title' => $sceneData['title'] ?? 'Untitled VR Scene', 'overview' => $sceneData['description'] ?? null, 'release_date' => isset($sceneData['date']) ? date('Y-m-d', $sceneData['date']) : null, 'runtime_minutes' => isset($sceneData['videoLength']) ? round($sceneData['videoLength'] / 60) : null, 'rating' => $sceneData['rating_avg'] ?? null, 'director' => null, // DeoVR doesn't seem to have director info 'cast' => [], // Will be extracted from categories/actors if available 'tags' => [], // Will be extracted from categories ]; // Handle categories/tags from DeoVR format $tags = []; if (isset($sceneData['categories']) && is_array($sceneData['categories'])) { foreach ($sceneData['categories'] as $category) { if (isset($category['tag']['name'])) { $tags[] = $category['tag']['name']; } } } $mappedData['tags'] = $tags; // Handle actors (DeoVR format might have actors array or might be null) $castData = []; if (isset($sceneData['actors']) && is_array($sceneData['actors']) && !empty($sceneData['actors'])) { foreach ($sceneData['actors'] as $actor) { if (isset($actor['name'])) { $castData[] = $actor['name']; } } } $this->logProgress("Mapped DeoVR scene data: title='{$mappedData['title']}', tags=" . json_encode($tags) . ", cast=" . json_encode($castData)); // Extract image URLs from DeoVR API response - try multiple possible field names $coverUrl = null; $screenshotUrl = null; // Try different possible cover image field names for DeoVR $coverFields = ['thumbnailUrl', 'cover_url', 'cover', 'poster_url', 'poster', 'thumbnail_url', 'thumbnail', 'image_url', 'image']; foreach ($coverFields as $field) { if (!empty($sceneData[$field])) { $coverUrl = $sceneData[$field]; break; } } // Try different possible screenshot field names for DeoVR $screenshotFields = ['screenshot_url', 'screenshot', 'preview_url', 'preview', 'thumb_url', 'thumb']; foreach ($screenshotFields as $field) { if (!empty($sceneData[$field])) { $screenshotUrl = $sceneData[$field]; break; } } if (!empty($coverUrl)) { $this->logProgress("DeoVR Cover URL: " . $coverUrl); } if (!empty($screenshotUrl)) { $this->logProgress("DeoVR Screenshot URL: " . $screenshotUrl); } if (!empty($coverUrl)) { // Validate URL before attempting download if (filter_var($coverUrl, FILTER_VALIDATE_URL)) { try { $coverFilename = $this->imageDownloader->generateFilename($coverUrl, 'cover'); $localCoverPath = $this->imageDownloader->downloadImage($coverUrl, $coverFilename, 'adult_videos'); if ($localCoverPath) { $mappedData['local_cover_path'] = $this->imageDownloader->getPublicUrl($localCoverPath); $this->logProgress("Downloaded cover: " . $localCoverPath); } else { $this->logProgress("Failed to download cover from: " . $coverUrl); } } catch (Exception $e) { $this->logProgress("Exception downloading cover from {$coverUrl}: " . $e->getMessage()); } } else { $this->logProgress("Invalid cover URL: " . $coverUrl); } } if (!empty($screenshotUrl)) { // Validate URL before attempting download if (filter_var($screenshotUrl, FILTER_VALIDATE_URL)) { try { $screenshotFilename = $this->imageDownloader->generateFilename($screenshotUrl, 'screenshot'); $localScreenshotPath = $this->imageDownloader->downloadImage($screenshotUrl, $screenshotFilename, 'adult_videos'); if ($localScreenshotPath) { $mappedData['local_screenshot_path'] = $this->imageDownloader->getPublicUrl($localScreenshotPath); $this->logProgress("Downloaded screenshot: " . $localScreenshotPath); } else { $this->logProgress("Failed to download screenshot from: " . $screenshotUrl); } } catch (Exception $e) { $this->logProgress("Exception downloading screenshot from {$screenshotUrl}: " . $e->getMessage()); } } else { $this->logProgress("Invalid screenshot URL: " . $screenshotUrl); } } // Handle actors $actors = $this->syncActors($castData); $sceneDataForDb = [ 'title' => $mappedData['title'], 'overview' => $mappedData['overview'], 'release_date' => $mappedData['release_date'], 'runtime_minutes' => $mappedData['runtime_minutes'], 'rating' => $mappedData['rating'], 'source_id' => $this->source['id'], 'external_id' => $sceneData['id'], 'metadata' => json_encode([ 'xbvr_id' => $sceneData['id'], 'xbvr_url' => $sceneData['scene_url'] ?? $sceneData['url'] ?? null, 'cast' => $castData, 'actors' => $actors, 'tags' => $mappedData['tags'], 'is_available' => $sceneData['is_available'] ?? true, 'is_watched' => $sceneData['is_watched'] ?? false, 'watch_count' => $sceneData['watch_count'] ?? 0, 'video_length' => $sceneData['videoLength'] ?? null, 'video_width' => $sceneData['video_width'] ?? null, 'video_height' => $sceneData['video_height'] ?? null, 'video_codec' => $sceneData['video_codec'] ?? null, 'file_path' => $sceneData['file_path'] ?? $sceneData['path'] ?? null, 'cover_url' => $coverUrl, 'local_cover_path' => $mappedData['local_cover_path'] ?? null, 'screenshot_url' => $screenshotUrl, 'local_screenshot_path' => $mappedData['local_screenshot_path'] ?? null, 'deoVR_format' => true, // Mark that this came from DeoVR API 'paysite' => $sceneData['paysite']['name'] ?? null, 'is3d' => $sceneData['is3d'] ?? false, 'screenType' => $sceneData['screenType'] ?? null, 'stereoMode' => $sceneData['stereoMode'] ?? null, 'fullVideoReady' => $sceneData['fullVideoReady'] ?? false, 'fullAccess' => $sceneData['fullAccess'] ?? false ]) ]; if ($existingScene) { // For existing scenes, check if we need to update images $existingMetadata = json_decode($existingScene['metadata'], true); // Only download images if they don't already exist locally if (empty($existingMetadata['local_cover_path']) && !empty($coverUrl)) { // Validate URL before attempting download if (filter_var($coverUrl, FILTER_VALIDATE_URL)) { try { $coverFilename = $this->imageDownloader->generateFilename($coverUrl, 'cover'); $localCoverPath = $this->imageDownloader->downloadImage($coverUrl, $coverFilename, 'adult_videos'); if ($localCoverPath) { $sceneDataForDb['local_cover_path'] = $this->imageDownloader->getPublicUrl($localCoverPath); $this->logProgress("Downloaded cover: " . $localCoverPath); } else { $this->logProgress("Failed to download cover from: " . $coverUrl); } } catch (Exception $e) { $this->logProgress("Exception downloading cover from {$coverUrl}: " . $e->getMessage()); } } else { $this->logProgress("Invalid cover URL: " . $coverUrl); } } else { // Keep existing local cover path $sceneDataForDb['local_cover_path'] = $existingMetadata['local_cover_path'] ?? null; if (!empty($sceneDataForDb['local_cover_path'])) { $this->logProgress("Using existing cover: " . $sceneDataForDb['local_cover_path']); } } if (empty($existingMetadata['local_screenshot_path']) && !empty($screenshotUrl)) { // Validate URL before attempting download if (filter_var($screenshotUrl, FILTER_VALIDATE_URL)) { try { $screenshotFilename = $this->imageDownloader->generateFilename($screenshotUrl, 'screenshot'); $localScreenshotPath = $this->imageDownloader->downloadImage($screenshotUrl, $screenshotFilename, 'adult_videos'); if ($localScreenshotPath) { $sceneDataForDb['local_screenshot_path'] = $this->imageDownloader->getPublicUrl($localScreenshotPath); $this->logProgress("Downloaded screenshot: " . $localScreenshotPath); } else { $this->logProgress("Failed to download screenshot from: " . $screenshotUrl); } } catch (Exception $e) { $this->logProgress("Exception downloading screenshot from {$screenshotUrl}: " . $e->getMessage()); } } else { $this->logProgress("Invalid screenshot URL: " . $screenshotUrl); } } else { // Keep existing local screenshot path $sceneDataForDb['local_screenshot_path'] = $existingMetadata['local_screenshot_path'] ?? null; if (!empty($sceneDataForDb['local_screenshot_path'])) { $this->logProgress("Using existing screenshot: " . $sceneDataForDb['local_screenshot_path']); } } $adultVideoModel->update($existingScene['id'], $sceneDataForDb); $adultVideoId = $existingScene['id']; $this->updatedCount++; // Create actor relationships for existing scene $this->createActorRelationships($adultVideoId, $actors); } else { $adultVideoModel->create($sceneDataForDb); $adultVideoId = $this->pdo->lastInsertId(); $this->newCount++; // Create actor relationships for new scene $this->createActorRelationships($adultVideoId, $actors); } } private function syncActors(array $cast): array { $actors = []; foreach ($cast as $actorData) { // Handle both string names and actor objects $actorName = is_array($actorData) ? ($actorData['name'] ?? '') : $actorData; if (empty($actorName)) continue; // Handle XBVR "aka:" format (e.g., "aka:Bella Luna,Bella Luna") $actorNames = $this->parseXbvrActorNames($actorName); $foundActor = null; foreach ($actorNames as $name) { $detailedActorData = $this->getActorDetails($name, $actorData); $detailedActorData['name'] = $name; // Ensure the name is set correctly $actor = $this->getOrCreateActor($detailedActorData); if ($actor) { $foundActor = $actor; break; // Use the first found actor } } // If no actor found with any of the names, create one with the first name if (!$foundActor && !empty($actorNames)) { $firstName = $actorNames[0]; $detailedActorData = $this->getActorDetails($firstName, $actorData); $detailedActorData['name'] = $firstName; $foundActor = $this->getOrCreateActor($detailedActorData); } if ($foundActor) { $actors[] = $foundActor; } } return $actors; } /** * Parse XBVR actor names that may contain "aka:" format * Example: "aka:Bella Luna,Bella Luna" -> ["Bella Luna", "Bella Luna"] */ private function parseXbvrActorNames(string $actorName): array { // Check if the name starts with "aka:" if (strpos($actorName, 'aka:') === 0) { // Remove "aka:" prefix and split by comma $namesPart = substr($actorName, 4); // Remove "aka:" $names = array_map('trim', explode(',', $namesPart)); // Filter out empty names $names = array_filter($names, function($name) { return !empty(trim($name)); }); if (!empty($names)) { $this->logProgress("Parsed XBVR aka format '{$actorName}' into names: " . implode(', ', $names)); return $names; } } // Return the original name if not in aka format return [$actorName]; } 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('/]+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 by name or alias $stmt = $this->pdo->prepare(" SELECT id, name, thumbnail_path, metadata FROM actors WHERE name = :name OR JSON_CONTAINS(metadata->'$.aliases', :name) "); $stmt->execute(['name' => $name]); $existingActor = $stmt->fetch(\PDO::FETCH_ASSOC); // If found by alias, log it for debugging if ($existingActor && $existingActor['name'] !== $name) { $this->logProgress("Found existing actor '{$existingActor['name']}' by alias '{$name}'"); } // 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' => $thumbnailPath ?: $existingActor['thumbnail_path'] ]; } // Create new actor with XBVR metadata try { $stmt = $this->pdo->prepare(" 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, 'metadata' => json_encode($actorMetadata) ]); $actorId = $this->pdo->lastInsertId(); $this->logProgress("Created new actor {$name} with XBVR metadata"); return [ 'id' => $actorId, 'name' => $name, 'thumbnail_path' => $thumbnailPath ]; } catch (Exception $e) { $this->logProgress("Failed to create actor {$name}: " . $e->getMessage()); return null; } } private function extractVideoList(array $mainData): array { // Try different possible keys for the video list $possibleKeys = ['Recent', 'scenes', 'content', 'videos']; foreach ($possibleKeys as $key) { if (isset($mainData[$key]) && is_array($mainData[$key])) { $this->logProgress("Found video list under key: '{$key}' with " . count($mainData[$key]) . " items"); return $mainData[$key]; } } // If no standard key found, look for arrays that might contain video data foreach ($mainData as $key => $value) { if (is_array($value) && count($value) > 0) { // Check if this looks like a video list by examining the first item $firstItem = $value[0]; if (isset($firstItem['title']) || isset($firstItem['video_url'])) { $this->logProgress("Found video list under key: '{$key}' with " . count($value) . " items"); return $value; } } } $this->logProgress("No video list found. Available keys: " . implode(', ', array_keys($mainData))); return []; } private function extractDetailUrl(array $videoItem): ?string { // Try different possible URL field names $possibleUrlFields = ['video_url', 'url', 'detail_url', 'scene_url']; foreach ($possibleUrlFields as $field) { if (!empty($videoItem[$field])) { return $videoItem[$field]; } } return null; } protected function getProcessedCount(): int { return $this->processedCount; } protected function getNewCount(): int { return $this->newCount; } protected function getUpdatedCount(): int { return $this->updatedCount; } protected function executeCleanup(): void { $this->logProgress("Starting cleanup - detecting deleted VR scenes in XBVR..."); // Clean up VR scenes //$this->cleanupScenes(); $this->logProgress("Cleanup completed. Deleted {$this->deletedCount} VR scenes."); } private function cleanupScenes(): void { $this->logProgress("Checking for deleted VR scenes..."); try { // Get all scenes from XBVR $xbvrScenes = $this->getXbvrScenesForCleanup(); $xbvrSceneIds = array_column($xbvrScenes, 'id'); $this->logProgress("Found " . count($xbvrSceneIds) . " VR scenes in XBVR"); // Get all scenes from local database for this source $stmt = $this->pdo->prepare(" SELECT id, metadata FROM adult_videos WHERE source_id = :source_id "); $stmt->execute(['source_id' => $this->source['id']]); $localScenes = $stmt->fetchAll(\PDO::FETCH_ASSOC); $this->logProgress("Found " . count($localScenes) . " VR scenes in local database"); $deletedCount = 0; foreach ($localScenes as $localScene) { $metadata = json_decode($localScene['metadata'], true); $xbvrId = $metadata['xbvr_id'] ?? null; if ($xbvrId && !in_array($xbvrId, $xbvrSceneIds)) { // Scene exists in local DB but not in XBVR - delete it $this->deleteAdultVideo($localScene['id']); $deletedCount++; $this->deletedCount++; } } $this->logProgress("Deleted {$deletedCount} VR scenes that no longer exist in XBVR"); } catch (Exception $e) { $this->logProgress("Error during VR scene cleanup: " . $e->getMessage()); } } private function getXbvrScenesForCleanup(): array { try { // Use the same DeoVR API as the main sync process to ensure consistency $scenes = $this->getXbvrScenes(); // 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']; } } $this->logProgress("Returning " . count($existingIds) . " existing scene IDs to prevent cleanup"); return $existingIds; } } private function deleteAdultVideo(int $videoId): void { try { // Delete actor relationships first $stmt = $this->pdo->prepare("DELETE FROM actor_adult_video WHERE adult_video_id = :adult_video_id"); $stmt->execute(['adult_video_id' => $videoId]); // Delete the video $stmt = $this->pdo->prepare("DELETE FROM adult_videos WHERE id = :id"); $stmt->execute(['id' => $videoId]); $this->logProgress("Deleted adult video with ID: {$videoId}"); } catch (Exception $e) { $this->logProgress("Error deleting adult video {$videoId}: " . $e->getMessage()); } } private function createActorRelationships(int $adultVideoId, array $actors): void { foreach ($actors as $actor) { if (!isset($actor['id'])) continue; try { // Insert relationship into pivot table (ignore duplicates) $stmt = $this->pdo->prepare(" INSERT IGNORE INTO actor_adult_video (adult_video_id, actor_id, created_at, updated_at) VALUES (:adult_video_id, :actor_id, NOW(), NOW()) "); $stmt->execute([ 'adult_video_id' => $adultVideoId, 'actor_id' => $actor['id'] ]); $this->logProgress("Created relationship: Adult Video {$adultVideoId} -> Actor {$actor['name']} ({$actor['id']})"); } catch (Exception $e) { $this->logProgress("Failed to create relationship for Adult Video {$adultVideoId} and Actor {$actor['name']}: " . $e->getMessage()); } } } }