baseUrl = rtrim($source['api_url'], '/'); $this->httpClient = new Client([ 'timeout' => 30, 'headers' => [ 'User-Agent' => 'MediaCollector/1.0' ] ]); $this->imageDownloader = new ImageDownloader('public/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 $actorName) { if (empty($actorName)) continue; $actor = $this->getOrCreateActor($actorName); if ($actor) { $actors[] = $actor; } } return $actors; } private function getOrCreateActor(string $name): ?array { // Check if actor already exists $stmt = $this->pdo->prepare(" SELECT id, name, thumbnail_path 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'] ]; } // For now, we'll create actor without thumbnail // In a full implementation, you'd fetch actor details from XBVR API try { $stmt = $this->pdo->prepare(" INSERT INTO actors (name, created_at, updated_at) VALUES (:name, NOW(), NOW()) "); $stmt->execute(['name' => $name]); $actorId = $this->pdo->lastInsertId(); return [ 'id' => $actorId, 'name' => $name, 'thumbnail_path' => null ]; } 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 getDeletedCount(): int { return 0; // XBVR doesn't provide deletion info in this context } 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()); } } } }