mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
976 lines
39 KiB
PHP
976 lines
39 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Utils\ImageDownloader;
|
|
use App\Models\AdultVideo;
|
|
use GuzzleHttp\Client;
|
|
use Exception;
|
|
|
|
class XbvrSyncService extends BaseSyncService
|
|
{
|
|
private Client $httpClient;
|
|
private string $baseUrl;
|
|
private ImageDownloader $imageDownloader;
|
|
private int $processedCount = 0;
|
|
private int $newCount = 0;
|
|
private int $updatedCount = 0;
|
|
|
|
public function __construct(\PDO $pdo, array $source, ?int $existingSyncLogId = null)
|
|
{
|
|
parent::__construct($pdo, $source, $existingSyncLogId);
|
|
|
|
// Initialize properties first before using them
|
|
$this->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('/<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 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());
|
|
}
|
|
}
|
|
}
|
|
}
|