mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
...
This commit is contained in:
@@ -12,6 +12,9 @@ abstract class BaseSyncService
|
||||
protected SyncLog $syncLog;
|
||||
protected int $sourceId;
|
||||
|
||||
protected $logFileHandle;
|
||||
protected $logFilePath;
|
||||
|
||||
public function __construct(\PDO $pdo, array $source)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
@@ -22,17 +25,50 @@ abstract class BaseSyncService
|
||||
}
|
||||
|
||||
$this->sourceId = (int) $source['id'];
|
||||
|
||||
// Create log file for this sync operation
|
||||
$this->initializeLogFile();
|
||||
}
|
||||
|
||||
private function initializeLogFile(): void
|
||||
{
|
||||
$timestamp = date('Y-m-d_H-i-s');
|
||||
$sourceName = strtolower($this->source['name'] ?? 'unknown');
|
||||
$this->logFilePath = "logs/{$sourceName}_sync_{$timestamp}.log";
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
$logDir = dirname($this->logFilePath);
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
$this->logFileHandle = fopen($this->logFilePath, 'w');
|
||||
if ($this->logFileHandle) {
|
||||
$this->logProgress("=== Starting {$sourceName} sync at " . date('Y-m-d H:i:s') . " ===");
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->logFileHandle) {
|
||||
$this->logProgress("=== Sync completed at " . date('Y-m-d H:i:s') . " ===");
|
||||
fclose($this->logFileHandle);
|
||||
}
|
||||
}
|
||||
|
||||
public function startSync(string $syncType = 'full'): int
|
||||
{
|
||||
// Create sync log entry
|
||||
$this->syncLog = new SyncLog($this->pdo);
|
||||
$syncLogId = $this->createSyncLog($syncType, 'started');
|
||||
// Set higher limits for long-running syncs
|
||||
ini_set('max_execution_time', 3600); // 1 hour
|
||||
ini_set('memory_limit', '512M');
|
||||
|
||||
$this->syncLog->id = $syncLogId;
|
||||
// Create sync log entry
|
||||
$syncLogId = $this->createSyncLog($syncType, 'started');
|
||||
$this->currentSyncLogId = $syncLogId;
|
||||
|
||||
try {
|
||||
$this->logProgress("Starting {$syncType} sync for source: " . ($this->source['display_name'] ?? $this->source['name']));
|
||||
|
||||
$this->executeSync($syncType);
|
||||
|
||||
// Update sync log as completed
|
||||
@@ -40,14 +76,31 @@ abstract class BaseSyncService
|
||||
'processed_items' => $this->getProcessedCount(),
|
||||
'new_items' => $this->getNewCount(),
|
||||
'updated_items' => $this->getUpdatedCount(),
|
||||
'deleted_items' => $this->getDeletedCount()
|
||||
'deleted_items' => $this->getDeletedCount(),
|
||||
'message' => "Successfully completed sync"
|
||||
]);
|
||||
|
||||
$this->logProgress("Sync completed successfully");
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Update sync log as failed
|
||||
// Log the full error details
|
||||
$errorMessage = $e->getMessage();
|
||||
$errorFile = $e->getFile();
|
||||
$errorLine = $e->getLine();
|
||||
$errorTrace = $e->getTraceAsString();
|
||||
|
||||
$this->logProgress("CRITICAL ERROR - Sync failed: {$errorMessage}");
|
||||
$this->logProgress("Error location: {$errorFile}:{$errorLine}");
|
||||
$this->logProgress("Stack trace: {$errorTrace}");
|
||||
|
||||
// Update sync log as failed with full error details
|
||||
$this->updateSyncLog($syncLogId, 'failed', [
|
||||
'message' => $e->getMessage(),
|
||||
'errors' => [$e->getMessage()]
|
||||
'message' => $errorMessage,
|
||||
'errors' => [
|
||||
$errorMessage,
|
||||
"File: {$errorFile}:{$errorLine}",
|
||||
"Stack: " . substr($errorTrace, 0, 1000) // Limit trace size
|
||||
]
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
@@ -80,7 +133,7 @@ abstract class BaseSyncService
|
||||
return (int) $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
private function updateSyncLog(int $syncLogId, string $status, array $stats = []): bool
|
||||
protected function updateSyncLog(int $syncLogId, string $status, array $stats = []): bool
|
||||
{
|
||||
$data = [
|
||||
'status' => $status,
|
||||
@@ -129,13 +182,31 @@ abstract class BaseSyncService
|
||||
return 0; // Override in subclasses
|
||||
}
|
||||
|
||||
protected $currentSyncLogId = null;
|
||||
|
||||
protected function logProgress(string $message): void
|
||||
{
|
||||
// Update sync log with progress message
|
||||
if ($this->syncLog) {
|
||||
$this->updateSyncLog($this->syncLog->id, 'running', [
|
||||
$timestamp = date('H:i:s');
|
||||
$logMessage = "[{$timestamp}] {$message}\n";
|
||||
|
||||
// Write to log file if available
|
||||
if ($this->logFileHandle) {
|
||||
fwrite($this->logFileHandle, $logMessage);
|
||||
}
|
||||
|
||||
// Also write to error log for immediate visibility
|
||||
error_log($message);
|
||||
|
||||
// Update sync log with progress message if we have a current sync log
|
||||
if ($this->currentSyncLogId) {
|
||||
$this->updateSyncLog($this->currentSyncLogId, 'running', [
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getLogFilePath(): string
|
||||
{
|
||||
return $this->logFilePath ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\Movie;
|
||||
use App\Models\TvShow;
|
||||
use App\Models\TvEpisode;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Exception;
|
||||
|
||||
class JellyfinSyncService extends BaseSyncService
|
||||
@@ -31,7 +32,7 @@ class JellyfinSyncService extends BaseSyncService
|
||||
$this->baseUrl = rtrim($source['api_url'], '/');
|
||||
}
|
||||
|
||||
protected function executeSync(string $syncType): void
|
||||
protected function executeSync(string $syncType = 'all'): void
|
||||
{
|
||||
if (empty($this->apiKey) || empty($this->baseUrl)) {
|
||||
throw new Exception('Jellyfin API key and URL not configured');
|
||||
@@ -40,6 +41,7 @@ class JellyfinSyncService extends BaseSyncService
|
||||
$this->logProgress('Starting Jellyfin library sync...');
|
||||
$this->logProgress("Jellyfin URL: {$this->baseUrl}");
|
||||
$this->logProgress("API Key: " . (empty($this->apiKey) ? 'NOT SET' : 'SET'));
|
||||
$this->logProgress("Sync Type: {$syncType}");
|
||||
|
||||
try {
|
||||
$userId = $this->getUserId();
|
||||
@@ -49,32 +51,49 @@ class JellyfinSyncService extends BaseSyncService
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Sync movies
|
||||
try {
|
||||
$this->logProgress('Fetching movies from Jellyfin...');
|
||||
$movies = $this->getJellyfinItems('Movie');
|
||||
$this->logProgress("Found " . count($movies) . " movies in Jellyfin");
|
||||
// Sync movies if requested
|
||||
if (in_array($syncType, ['all', 'movies'])) {
|
||||
try {
|
||||
$this->logProgress('Fetching movies from Jellyfin...');
|
||||
$movies = $this->getJellyfinItems('Movie');
|
||||
$this->logProgress("Found " . count($movies) . " movies in Jellyfin");
|
||||
|
||||
if (empty($movies)) {
|
||||
$this->logProgress('No movies found in Jellyfin library');
|
||||
$this->logProgress("Processed {$this->processedCount} items");
|
||||
return;
|
||||
if (empty($movies)) {
|
||||
$this->logProgress('No movies found in Jellyfin library');
|
||||
} else {
|
||||
foreach ($movies as $movieData) {
|
||||
$this->syncMovie($movieData);
|
||||
$this->processedCount++;
|
||||
}
|
||||
$this->logProgress("Successfully processed {$this->processedCount} movies");
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress('Error syncing movies: ' . $e->getMessage());
|
||||
if ($syncType === 'movies') {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($movies as $movieData) {
|
||||
$this->syncMovie($movieData);
|
||||
$this->processedCount++;
|
||||
}
|
||||
|
||||
$this->logProgress("Successfully processed {$this->processedCount} movies");
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress('Error syncing movies: ' . $e->getMessage());
|
||||
throw $e;
|
||||
} else {
|
||||
$this->logProgress('Skipping movies sync (sync type: ' . $syncType . ')');
|
||||
}
|
||||
|
||||
// TODO: Sync TV shows and episodes when TvShow model is implemented
|
||||
// $this->syncTvShows();
|
||||
// Sync TV shows and episodes if requested
|
||||
if (in_array($syncType, ['all', 'tvshows'])) {
|
||||
try {
|
||||
$this->syncTvShows();
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress('Error syncing TV shows: ' . $e->getMessage());
|
||||
if ($syncType === 'tvshows') {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->logProgress('Skipping TV shows sync (sync type: ' . $syncType . ')');
|
||||
}
|
||||
|
||||
// Sync music (artists, albums, tracks) - TODO: Implement when music models are created
|
||||
// $this->syncMusic();
|
||||
|
||||
$this->logProgress("Processed {$this->processedCount} items");
|
||||
}
|
||||
@@ -96,14 +115,43 @@ class JellyfinSyncService extends BaseSyncService
|
||||
private function syncTvShows(): void
|
||||
{
|
||||
try {
|
||||
$this->logProgress('=== Starting TV Shows Sync ===');
|
||||
$this->logProgress('Fetching TV shows from Jellyfin...');
|
||||
$tvShows = $this->getJellyfinItems('Series');
|
||||
$this->logProgress("Found " . count($tvShows) . " TV shows in Jellyfin");
|
||||
|
||||
if (empty($tvShows)) {
|
||||
$this->logProgress('No TV shows found in Jellyfin library');
|
||||
return;
|
||||
}
|
||||
|
||||
$processedShows = 0;
|
||||
$successfulShows = 0;
|
||||
$failedShows = 0;
|
||||
|
||||
foreach ($tvShows as $showData) {
|
||||
$this->syncTvShow($showData);
|
||||
$this->processedCount++;
|
||||
$processedShows++;
|
||||
$this->logProgress("Processing TV show {$processedShows}/" . count($tvShows) . ": {$showData['Name']} (ID: {$showData['Id']})");
|
||||
|
||||
try {
|
||||
$this->syncTvShow($showData);
|
||||
$successfulShows++;
|
||||
$this->logProgress("✓ Successfully synced TV show: {$showData['Name']}");
|
||||
} catch (Exception $e) {
|
||||
$failedShows++;
|
||||
$this->logProgress("✗ Failed to sync TV show {$showData['Name']}: " . $e->getMessage());
|
||||
$this->logProgress("Stack trace: " . $e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
$this->logProgress("=== TV Shows Sync Summary ===");
|
||||
$this->logProgress("Processed: {$processedShows}, Successful: {$successfulShows}, Failed: {$failedShows}");
|
||||
$this->logProgress("Successfully processed {$this->processedCount} TV shows");
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress('Error syncing TV shows: ' . $e->getMessage());
|
||||
$this->logProgress('CRITICAL ERROR in TV shows sync: ' . $e->getMessage());
|
||||
$this->logProgress('Stack trace: ' . $e->getTraceAsString());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +227,7 @@ class JellyfinSyncService extends BaseSyncService
|
||||
'source_id' => $this->source['id']
|
||||
]);
|
||||
|
||||
$movieData = [
|
||||
$movieDataForDb = [
|
||||
'title' => $movieData['Name'],
|
||||
'overview' => $movieData['Overview'] ?? null,
|
||||
'release_date' => $movieData['PremiereDate'] ? date('Y-m-d', strtotime($movieData['PremiereDate'])) : null,
|
||||
@@ -187,102 +235,527 @@ class JellyfinSyncService extends BaseSyncService
|
||||
'rating' => $movieData['CommunityRating'] ?? null,
|
||||
'imdb_id' => $movieData['ProviderIds']['Imdb'] ?? null,
|
||||
'tmdb_id' => $movieData['ProviderIds']['Tmdb'] ?? null,
|
||||
'poster_url' => $this->getImageUrl($movieData['Id'], 'Primary'),
|
||||
'backdrop_url' => $this->getImageUrl($movieData['Id'], 'Backdrop'),
|
||||
'source_id' => $this->source['id'],
|
||||
'metadata' => json_encode([
|
||||
'jellyfin_id' => $movieData['Id'],
|
||||
'genres' => $movieData['Genres'] ?? [],
|
||||
'studios' => $movieData['Studios'] ?? []
|
||||
'studios' => $movieData['Studios'] ?? [],
|
||||
'poster_url' => $this->getImageUrl($movieData['Id'], 'Primary'),
|
||||
'backdrop_url' => $this->getImageUrl($movieData['Id'], 'Backdrop')
|
||||
])
|
||||
];
|
||||
|
||||
// Download poster image
|
||||
$posterPath = $this->downloadPosterImage($movieData['Id'], $movieData['Name']);
|
||||
if ($posterPath) {
|
||||
$movieDataForDb['poster_url'] = $posterPath;
|
||||
} else {
|
||||
$movieDataForDb['poster_url'] = $this->getImageUrl($movieData['Id'], 'Primary');
|
||||
}
|
||||
|
||||
// Download backdrop image
|
||||
$backdropPath = $this->downloadBackdropImage($movieData['Id'], $movieData['Name']);
|
||||
if ($backdropPath) {
|
||||
$movieDataForDb['backdrop_url'] = $backdropPath;
|
||||
} else {
|
||||
$movieDataForDb['backdrop_url'] = $this->getImageUrl($movieData['Id'], 'Backdrop');
|
||||
}
|
||||
|
||||
if (empty($existingMovie)) {
|
||||
$movieModel->create($movieData);
|
||||
$movieModel->create($movieDataForDb);
|
||||
$this->newCount++;
|
||||
} else {
|
||||
$movieModel->update($existingMovie[0]['id'], $movieData);
|
||||
$movieModel->update($existingMovie[0]['id'], $movieDataForDb);
|
||||
$this->updatedCount++;
|
||||
}
|
||||
|
||||
// Sync actors for this movie and create relationships
|
||||
try {
|
||||
$actors = $this->syncActors($movieData);
|
||||
$this->createMovieActorRelationships($existingMovie ? $existingMovie[0]['id'] : $this->pdo->lastInsertId(), $actors);
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Warning: Failed to sync actors for movie {$movieData['Name']}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement when TvShow model is created
|
||||
// private function syncTvShow(array $showData): void
|
||||
// {
|
||||
// $showModel = new TvShow($this->pdo);
|
||||
private function syncTvShow(array $showData): void
|
||||
{
|
||||
$showName = $showData['Name'] ?? 'Unknown Show';
|
||||
$this->logProgress("--- Starting sync for TV show: {$showName} ---");
|
||||
|
||||
// // Check if show already exists
|
||||
// $existingShow = $showModel->findAll([
|
||||
// 'tmdb_id' => $showData['ProviderIds']['Tmdb'] ?? null,
|
||||
// 'imdb_id' => $showData['ProviderIds']['Imdb'] ?? null,
|
||||
// 'source_id' => $this->source->id
|
||||
// ]);
|
||||
$showModel = new TvShow($this->pdo);
|
||||
|
||||
// $showData = [
|
||||
// 'title' => $showData['Name'],
|
||||
// 'overview' => $showData['Overview'] ?? null,
|
||||
// 'first_air_date' => $showData['PremiereDate'] ? date('Y-m-d', strtotime($showData['PremiereDate'])) : null,
|
||||
// 'rating' => $showData['CommunityRating'] ?? null,
|
||||
// 'imdb_id' => $showData['ProviderIds']['Imdb'] ?? null,
|
||||
// 'tmdb_id' => $showData['ProviderIds']['Tmdb'] ?? null,
|
||||
// 'poster_url' => $this->getImageUrl($showData['Id'], 'Primary'),
|
||||
// 'backdrop_url' => $this->getImageUrl($showData['Id'], 'Backdrop'),
|
||||
// 'source_id' => $this->source->id,
|
||||
// 'metadata' => json_encode([
|
||||
// 'jellyfin_id' => $showData['Id'],
|
||||
// 'genres' => $showData['Genres'] ?? []
|
||||
// ])
|
||||
// ];
|
||||
// Check if show already exists
|
||||
$this->logProgress("Checking if TV show already exists in database...");
|
||||
$existingShow = $showModel->findAll([
|
||||
'tmdb_id' => $showData['ProviderIds']['Tmdb'] ?? null,
|
||||
'imdb_id' => $showData['ProviderIds']['Imdb'] ?? null,
|
||||
'tvdb_id' => $showData['ProviderIds']['Tvdb'] ?? null,
|
||||
'source_id' => $this->source['id']
|
||||
]);
|
||||
|
||||
// if (empty($existingShow)) {
|
||||
// $showId = $showModel->create($showData);
|
||||
// $this->newCount++;
|
||||
// } else {
|
||||
// $showId = $existingShow[0]['id'];
|
||||
// $showModel->update($showId, $showData);
|
||||
// $this->updatedCount++;
|
||||
// }
|
||||
$this->logProgress("Found " . count($existingShow) . " existing TV show(s) in database");
|
||||
|
||||
// // Sync episodes for this show
|
||||
// $this->syncEpisodes($showId, $showData['Id']);
|
||||
// }
|
||||
// Prepare show data for database
|
||||
$this->logProgress("Preparing TV show data for database...");
|
||||
$showDataForDb = [
|
||||
'title' => $showData['Name'],
|
||||
'overview' => $showData['Overview'] ?? null,
|
||||
'first_air_date' => $showData['PremiereDate'] ? date('Y-m-d', strtotime($showData['PremiereDate'])) : null,
|
||||
'rating' => $showData['CommunityRating'] ?? null,
|
||||
'imdb_id' => $showData['ProviderIds']['Imdb'] ?? null,
|
||||
'tmdb_id' => $showData['ProviderIds']['Tmdb'] ?? null,
|
||||
'tvdb_id' => $showData['ProviderIds']['Tvdb'] ?? null,
|
||||
'source_id' => $this->source['id'],
|
||||
'metadata' => json_encode([
|
||||
'jellyfin_id' => $showData['Id'],
|
||||
'genres' => $showData['Genres'] ?? []
|
||||
])
|
||||
];
|
||||
|
||||
// TODO: Implement when TvEpisode model is created
|
||||
// private function syncEpisodes(int $showId, string $jellyfinShowId): void
|
||||
// {
|
||||
// try {
|
||||
// $episodes = $this->getShowEpisodes($jellyfinShowId);
|
||||
// Download poster image
|
||||
$this->logProgress("Downloading poster image for {$showName}...");
|
||||
$posterPath = $this->downloadPosterImage($showData['Id'], $showData['Name']);
|
||||
if ($posterPath) {
|
||||
$showDataForDb['poster_url'] = $posterPath;
|
||||
$this->logProgress("✓ Poster downloaded successfully: {$posterPath}");
|
||||
} else {
|
||||
$showDataForDb['poster_url'] = $this->getImageUrl($showData['Id'], 'Primary');
|
||||
$this->logProgress("⚠ Poster download failed, using URL instead");
|
||||
}
|
||||
|
||||
// foreach ($episodes as $episodeData) {
|
||||
// $this->syncEpisode($showId, $episodeData);
|
||||
// }
|
||||
// } catch (Exception $e) {
|
||||
// $this->logProgress('Error syncing episodes for show ' . $jellyfinShowId . ': ' . $e->getMessage());
|
||||
// }
|
||||
// }
|
||||
// Download backdrop image
|
||||
$this->logProgress("Downloading backdrop image for {$showName}...");
|
||||
$backdropPath = $this->downloadBackdropImage($showData['Id'], $showData['Name']);
|
||||
if ($backdropPath) {
|
||||
$showDataForDb['backdrop_url'] = $backdropPath;
|
||||
$this->logProgress("✓ Backdrop downloaded successfully: {$backdropPath}");
|
||||
} else {
|
||||
$showDataForDb['backdrop_url'] = $this->getImageUrl($showData['Id'], 'Backdrop');
|
||||
$this->logProgress("⚠ Backdrop download failed, using URL instead");
|
||||
}
|
||||
|
||||
// TODO: Implement when TvEpisode model is created
|
||||
// private function syncEpisode(int $showId, array $episodeData): void
|
||||
// {
|
||||
// $episodeModel = new TvEpisode($this->pdo);
|
||||
try {
|
||||
if (empty($existingShow)) {
|
||||
$this->logProgress("Creating new TV show in database...");
|
||||
$showId = $showModel->create($showDataForDb);
|
||||
$this->newCount++;
|
||||
$this->logProgress("✓ Created new TV show with ID: {$showId}");
|
||||
} else {
|
||||
$showId = $existingShow[0]['id'];
|
||||
$this->logProgress("Updating existing TV show (ID: {$showId})...");
|
||||
$showModel->update($showId, $showDataForDb);
|
||||
$this->updatedCount++;
|
||||
$this->logProgress("✓ Updated existing TV show");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("✗ Failed to save TV show {$showName} to database: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// $episodeData = [
|
||||
// 'title' => $episodeData['Name'],
|
||||
// 'overview' => $episodeData['Overview'] ?? null,
|
||||
// 'season_number' => $episodeData['ParentIndexNumber'] ?? 1,
|
||||
// 'episode_number' => $episodeData['IndexNumber'] ?? 1,
|
||||
// 'air_date' => $episodeData['PremiereDate'] ? date('Y-m-d', strtotime($episodeData['PremiereDate'])) : null,
|
||||
// 'runtime_minutes' => $episodeData['RunTimeTicks'] ? intval($episodeData['RunTimeTicks'] / (10000000 * 60)) : null,
|
||||
// 'rating' => $episodeData['CommunityRating'] ?? null,
|
||||
// 'tv_show_id' => $showId,
|
||||
// 'source_id' => $this->source->id,
|
||||
// 'metadata' => json_encode([
|
||||
// 'jellyfin_id' => $episodeData['Id']
|
||||
// ])
|
||||
// ];
|
||||
// Sync actors for this show and create relationships
|
||||
try {
|
||||
$this->logProgress("Syncing actors for {$showName}...");
|
||||
$actors = $this->syncActors($showData);
|
||||
$this->logProgress("Found " . count($actors) . " actors for {$showName}");
|
||||
$this->createShowActorRelationships($showId, $actors);
|
||||
$this->logProgress("✓ Actor relationships created for {$showName}");
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Warning: Failed to sync actors for TV show {$showName}: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// $episodeModel->create($episodeData);
|
||||
// }
|
||||
// Sync episodes for this show
|
||||
try {
|
||||
$this->logProgress("Syncing episodes for {$showName}...");
|
||||
$this->syncEpisodes($showId, $showData['Id']);
|
||||
$this->logProgress("✓ Episodes sync completed for {$showName}");
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("✗ Failed to sync episodes for {$showName}: " . $e->getMessage());
|
||||
$this->logProgress("Stack trace: " . $e->getTraceAsString());
|
||||
}
|
||||
|
||||
$this->logProgress("--- Completed sync for TV show: {$showName} ---");
|
||||
}
|
||||
|
||||
private function syncEpisodes(int $showId, string $jellyfinShowId): void
|
||||
{
|
||||
try {
|
||||
$this->logProgress("=== Starting episodes sync for show ID: {$jellyfinShowId} ===");
|
||||
|
||||
$episodes = $this->getShowEpisodes($jellyfinShowId);
|
||||
$episodeCount = count($episodes);
|
||||
$this->logProgress("Found {$episodeCount} episodes for show ID: {$jellyfinShowId}");
|
||||
|
||||
if (empty($episodes)) {
|
||||
$this->logProgress("No episodes found for show ID: {$jellyfinShowId}");
|
||||
return;
|
||||
}
|
||||
|
||||
$processedEpisodes = 0;
|
||||
$successfulEpisodes = 0;
|
||||
$failedEpisodes = 0;
|
||||
|
||||
foreach ($episodes as $episodeData) {
|
||||
$processedEpisodes++;
|
||||
$episodeName = $episodeData['Name'] ?? 'Unknown Episode';
|
||||
$this->logProgress("Processing episode {$processedEpisodes}/{$episodeCount}: {$episodeName}");
|
||||
|
||||
try {
|
||||
$this->syncEpisode($showId, $episodeData);
|
||||
$successfulEpisodes++;
|
||||
$this->logProgress("✓ Successfully synced episode: {$episodeName}");
|
||||
} catch (Exception $e) {
|
||||
$failedEpisodes++;
|
||||
$this->logProgress("✗ Failed to sync episode {$episodeName}: " . $e->getMessage());
|
||||
$this->logProgress("Stack trace: " . $e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
$this->logProgress("=== Episodes Sync Summary ===");
|
||||
$this->logProgress("Processed: {$processedEpisodes}, Successful: {$successfulEpisodes}, Failed: {$failedEpisodes}");
|
||||
$this->logProgress("Successfully processed {$this->processedCount} episodes");
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress('CRITICAL ERROR in episodes sync: ' . $e->getMessage());
|
||||
$this->logProgress('Stack trace: ' . $e->getTraceAsString());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function syncEpisode(int $showId, array $episodeData): void
|
||||
{
|
||||
$episodeName = $episodeData['Name'] ?? 'Unknown Episode';
|
||||
$episodeSeason = $episodeData['ParentIndexNumber'] ?? 1;
|
||||
$episodeNumber = $episodeData['IndexNumber'] ?? 1;
|
||||
$this->logProgress("--- Starting sync for episode: S{$episodeSeason}E{$episodeNumber} - {$episodeName} ---");
|
||||
|
||||
$episodeModel = new TvEpisode($this->pdo);
|
||||
|
||||
// Check if episode already exists by jellyfin_id in metadata
|
||||
$this->logProgress("Checking if episode already exists in database...");
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT id, metadata FROM tv_episodes
|
||||
WHERE tv_show_id = :tv_show_id AND source_id = :source_id
|
||||
");
|
||||
$stmt->execute([
|
||||
'tv_show_id' => $showId,
|
||||
'source_id' => $this->source['id']
|
||||
]);
|
||||
$existingEpisodes = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
$this->logProgress("Found " . count($existingEpisodes) . " existing episodes for this show");
|
||||
|
||||
$existingEpisode = null;
|
||||
foreach ($existingEpisodes as $episode) {
|
||||
$metadata = json_decode($episode['metadata'], true);
|
||||
if (isset($metadata['jellyfin_id']) && $metadata['jellyfin_id'] === $episodeData['Id']) {
|
||||
$existingEpisode = $episode;
|
||||
$this->logProgress("Found existing episode with Jellyfin ID: {$episodeData['Id']}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$existingEpisode) {
|
||||
$this->logProgress("Episode not found, will create new episode");
|
||||
}
|
||||
|
||||
// Prepare episode data for database
|
||||
$this->logProgress("Preparing episode data for database...");
|
||||
$episodeDataForDb = [
|
||||
'title' => $episodeData['Name'],
|
||||
'overview' => $episodeData['Overview'] ?? null,
|
||||
'season_number' => $episodeSeason,
|
||||
'episode_number' => $episodeNumber,
|
||||
'air_date' => $episodeData['PremiereDate'] ? date('Y-m-d', strtotime($episodeData['PremiereDate'])) : null,
|
||||
'runtime_minutes' => $episodeData['RunTimeTicks'] ? intval($episodeData['RunTimeTicks'] / (10000000 * 60)) : null,
|
||||
'rating' => $episodeData['CommunityRating'] ?? null,
|
||||
'tv_show_id' => $showId,
|
||||
'source_id' => $this->source['id'],
|
||||
'metadata' => json_encode([
|
||||
'jellyfin_id' => $episodeData['Id'],
|
||||
'tmdb_id' => $episodeData['ProviderIds']['Tmdb'] ?? null,
|
||||
'imdb_id' => $episodeData['ProviderIds']['Imdb'] ?? null,
|
||||
'tvdb_id' => $episodeData['ProviderIds']['Tvdb'] ?? null
|
||||
// Note: Episodes don't have dedicated provider ID columns in the database,
|
||||
// so we store them in metadata for reference
|
||||
])
|
||||
];
|
||||
|
||||
try {
|
||||
if ($existingEpisode) {
|
||||
$this->logProgress("Updating existing episode in database...");
|
||||
$episodeModel->update($existingEpisode['id'], $episodeDataForDb);
|
||||
$episodeId = $existingEpisode['id'];
|
||||
$this->updatedCount++;
|
||||
$this->logProgress("✓ Updated episode: {$episodeName}");
|
||||
} else {
|
||||
$this->logProgress("Creating new episode in database...");
|
||||
$episodeModel->create($episodeDataForDb);
|
||||
$episodeId = $this->pdo->lastInsertId();
|
||||
$this->newCount++;
|
||||
$this->logProgress("✓ Created new episode with ID: {$episodeId}");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("✗ Failed to save episode {$episodeName} to database: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Sync actors for this episode and create relationships
|
||||
try {
|
||||
$this->logProgress("Syncing actors for episode {$episodeName}...");
|
||||
$actors = $this->syncActors($episodeData);
|
||||
$this->logProgress("Found " . count($actors) . " actors for episode {$episodeName}");
|
||||
$this->createActorRelationships($episodeId, $actors);
|
||||
$this->logProgress("✓ Actor relationships created for episode {$episodeName}");
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Warning: Failed to sync actors for episode {$episodeName}: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->logProgress("--- Completed sync for episode: {$episodeName} ---");
|
||||
}
|
||||
|
||||
private function getShowEpisodes(string $jellyfinShowId): array
|
||||
{
|
||||
$this->logProgress("--- Fetching episodes for show ID: {$jellyfinShowId} ---");
|
||||
|
||||
try {
|
||||
$url = "{$this->baseUrl}/Shows/{$jellyfinShowId}/Episodes";
|
||||
$this->logProgress("Fetching episodes from Jellyfin API: {$url}");
|
||||
|
||||
$response = $this->httpClient->get($url, [
|
||||
'query' => [
|
||||
'Fields' => 'ProviderIds,Overview,PremiereDate,CommunityRating,OfficialRating,RunTimeTicks,People'
|
||||
]
|
||||
]);
|
||||
|
||||
$httpCode = $response->getStatusCode();
|
||||
$this->logProgress("Jellyfin API response code: {$httpCode}");
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
$errorMsg = "Jellyfin Episodes API returned HTTP {$httpCode}";
|
||||
$this->logProgress("✗ {$errorMsg}");
|
||||
throw new Exception($errorMsg);
|
||||
}
|
||||
|
||||
$data = json_decode($response->getBody(), true);
|
||||
$episodeCount = count($data['Items'] ?? []);
|
||||
$this->logProgress("✓ Successfully fetched {$episodeCount} episodes from Jellyfin");
|
||||
|
||||
if ($episodeCount === 0) {
|
||||
$this->logProgress("⚠ No episodes found in Jellyfin response");
|
||||
}
|
||||
|
||||
$episodes = $data['Items'] ?? [];
|
||||
|
||||
// Log episode details for debugging
|
||||
if (!empty($episodes)) {
|
||||
$this->logProgress("Episode details:");
|
||||
foreach ($episodes as $index => $episode) {
|
||||
$episodeName = $episode['Name'] ?? 'Unknown';
|
||||
$episodeId = $episode['Id'] ?? 'No ID';
|
||||
$season = $episode['ParentIndexNumber'] ?? 'No season';
|
||||
$episodeNum = $episode['IndexNumber'] ?? 'No number';
|
||||
$this->logProgress(" " . ($index + 1) . ". {$episodeName} (S{$season}E{$episodeNum}) - ID: {$episodeId}");
|
||||
}
|
||||
}
|
||||
|
||||
return $episodes;
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress('✗ Failed to fetch episodes: ' . $e->getMessage());
|
||||
$this->logProgress('Stack trace: ' . $e->getTraceAsString());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function syncActors(array $mediaData): array
|
||||
{
|
||||
// Jellyfin doesn't have a direct actors API, so we extract from media data
|
||||
// This is a simplified implementation - in a full implementation,
|
||||
// you'd need to fetch detailed cast information from Jellyfin
|
||||
|
||||
$cast = [];
|
||||
|
||||
// Try to extract cast information from different fields
|
||||
if (isset($mediaData['People']) && is_array($mediaData['People'])) {
|
||||
foreach ($mediaData['People'] as $person) {
|
||||
if (isset($person['Type']) && $person['Type'] === 'Actor') {
|
||||
$cast[] = [
|
||||
'name' => $person['Name'],
|
||||
'jellyfin_id' => $person['Id'] ?? null,
|
||||
'image_url' => isset($person['PrimaryImageTag']) ? $this->getActorImageUrl($person['Id'], $person['PrimaryImageTag']) : null
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no cast found in People array, try other fields
|
||||
if (empty($cast)) {
|
||||
if (isset($mediaData['Cast']) && is_array($mediaData['Cast'])) {
|
||||
foreach ($mediaData['Cast'] as $actorName) {
|
||||
if (empty($actorName)) continue;
|
||||
$cast[] = [
|
||||
'name' => $actorName,
|
||||
'jellyfin_id' => null,
|
||||
'image_url' => null
|
||||
];
|
||||
}
|
||||
} elseif (isset($mediaData['Actors']) && is_array($mediaData['Actors'])) {
|
||||
foreach ($mediaData['Actors'] as $actorName) {
|
||||
if (empty($actorName)) continue;
|
||||
$cast[] = [
|
||||
'name' => $actorName,
|
||||
'jellyfin_id' => null,
|
||||
'image_url' => null
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create/sync actors and return actor objects
|
||||
$actors = [];
|
||||
foreach ($cast as $actorData) {
|
||||
if (empty($actorData['name'])) continue;
|
||||
|
||||
$actor = $this->getOrCreateActor($actorData['name'], $actorData['jellyfin_id'], $actorData['image_url']);
|
||||
if ($actor) {
|
||||
$actors[] = $actor;
|
||||
}
|
||||
}
|
||||
|
||||
return $actors;
|
||||
}
|
||||
|
||||
private function getOrCreateActor(string $name, ?string $jellyfinId = null, ?string $imageUrl = null): ?array
|
||||
{
|
||||
try {
|
||||
// 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) {
|
||||
// Update thumbnail if we have a new image URL and no existing thumbnail
|
||||
if ($imageUrl && empty($existingActor['thumbnail_path'])) {
|
||||
$thumbnailPath = $this->downloadImage($imageUrl, 'actors', $name);
|
||||
if ($thumbnailPath) {
|
||||
try {
|
||||
$updateStmt = $this->pdo->prepare('
|
||||
UPDATE actors SET thumbnail_path = :thumbnail_path, updated_at = NOW() WHERE id = :id
|
||||
');
|
||||
$updateStmt->execute([
|
||||
'thumbnail_path' => $thumbnailPath,
|
||||
'id' => $existingActor['id']
|
||||
]);
|
||||
$existingActor['thumbnail_path'] = $thumbnailPath;
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Warning: Could not update thumbnail for existing actor {$name}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
'id' => $existingActor['id'],
|
||||
'name' => $existingActor['name'],
|
||||
'thumbnail_path' => $existingActor['thumbnail_path']
|
||||
];
|
||||
}
|
||||
|
||||
// Create new actor
|
||||
$thumbnailPath = null;
|
||||
if ($imageUrl) {
|
||||
$thumbnailPath = $this->downloadImage($imageUrl, 'actors', $name);
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare('
|
||||
INSERT INTO actors (name, thumbnail_path, created_at, updated_at)
|
||||
VALUES (:name, :thumbnail_path, NOW(), NOW())
|
||||
');
|
||||
$stmt->execute([
|
||||
'name' => $name,
|
||||
'thumbnail_path' => $thumbnailPath
|
||||
]);
|
||||
$actorId = $this->pdo->lastInsertId();
|
||||
|
||||
return [
|
||||
'id' => $actorId,
|
||||
'name' => $name,
|
||||
'thumbnail_path' => $thumbnailPath
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Failed to create/find actor {$name}: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function createActorRelationships(int $episodeId, 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_tv_episode (tv_episode_id, actor_id, created_at, updated_at)
|
||||
VALUES (:tv_episode_id, :actor_id, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
'tv_episode_id' => $episodeId,
|
||||
'actor_id' => $actor['id']
|
||||
]);
|
||||
|
||||
$this->logProgress("Created relationship: TV Episode {$episodeId} -> Actor {$actor['name']} ({$actor['id']})");
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Failed to create relationship for TV Episode {$episodeId} and Actor {$actor['name']}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createShowActorRelationships(int $showId, 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_tv_show (tv_show_id, actor_id, created_at, updated_at)
|
||||
VALUES (:tv_show_id, :actor_id, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
'tv_show_id' => $showId,
|
||||
'actor_id' => $actor['id']
|
||||
]);
|
||||
|
||||
$this->logProgress("Created relationship: TV Show {$showId} -> Actor {$actor['name']} ({$actor['id']})");
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Failed to create relationship for TV Show {$showId} and Actor {$actor['name']}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createMovieActorRelationships(int $movieId, 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_movie (movie_id, actor_id, created_at, updated_at)
|
||||
VALUES (:movie_id, :actor_id, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
'movie_id' => $movieId,
|
||||
'actor_id' => $actor['id']
|
||||
]);
|
||||
|
||||
$this->logProgress("Created relationship: Movie {$movieId} -> Actor {$actor['name']} ({$actor['id']})");
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Failed to create relationship for Movie {$movieId} and Actor {$actor['name']}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getImageUrl(string $itemId, string $type): ?string
|
||||
{
|
||||
@@ -293,6 +766,83 @@ class JellyfinSyncService extends BaseSyncService
|
||||
return "{$this->baseUrl}/Items/{$itemId}/Images/{$type}?maxWidth=400";
|
||||
}
|
||||
|
||||
private function getActorImageUrl(string $personId, string $imageTag): ?string
|
||||
{
|
||||
if (empty($personId) || empty($imageTag)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure baseUrl doesn't have trailing slash
|
||||
$baseUrl = rtrim($this->baseUrl, '/');
|
||||
return "{$baseUrl}/Items/{$personId}/Images/Primary?maxWidth=300&tag={$imageTag}&quality=90";
|
||||
}
|
||||
|
||||
private function downloadImage(string $imageUrl, string $type, string $itemName): ?string
|
||||
{
|
||||
if (empty($imageUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create images directory structure if it doesn't exist
|
||||
$imagesDir = "public/images/{$type}";
|
||||
if (!is_dir($imagesDir)) {
|
||||
if (!mkdir($imagesDir, 0755, true)) {
|
||||
$this->logProgress("Warning: Could not create images directory: {$imagesDir}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a safe filename from item name and hash of URL for consistency
|
||||
$safeName = preg_replace('/[^a-zA-Z0-9_-]/', '_', $itemName);
|
||||
if ($safeName === null) {
|
||||
$safeName = $type . '_unknown';
|
||||
}
|
||||
$safeName = substr($safeName, 0, 30); // Limit length for hash part
|
||||
|
||||
// Use hash of the image URL to ensure same image always gets same filename
|
||||
$urlHash = substr(md5($imageUrl), 0, 8);
|
||||
$filename = $safeName . '_' . $urlHash . '.jpg';
|
||||
$filepath = $imagesDir . '/' . $filename;
|
||||
|
||||
// Check if file already exists
|
||||
if (file_exists($filepath)) {
|
||||
$this->logProgress("Image already exists for {$itemName}, skipping download: {$filepath}");
|
||||
return "{$type}/{$filename}";
|
||||
}
|
||||
|
||||
// Download the image
|
||||
$this->logProgress("Downloading {$type} image for {$itemName} from: {$imageUrl}");
|
||||
|
||||
$response = $this->httpClient->get($imageUrl, [
|
||||
'sink' => $filepath
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$this->logProgress("Successfully downloaded {$type} image for {$itemName} to: {$filepath}");
|
||||
return "{$type}/{$filename}";
|
||||
} else {
|
||||
$this->logProgress("Failed to download {$type} image for {$itemName}: HTTP " . $response->getStatusCode());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Error downloading {$type} image for {$itemName}: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function downloadPosterImage(string $itemId, string $itemName): ?string
|
||||
{
|
||||
$posterUrl = $this->getImageUrl($itemId, 'Primary');
|
||||
return $this->downloadImage($posterUrl, 'posters', $itemName);
|
||||
}
|
||||
|
||||
private function downloadBackdropImage(string $itemId, string $itemName): ?string
|
||||
{
|
||||
$backdropUrl = $this->getImageUrl($itemId, 'Backdrop');
|
||||
return $this->downloadImage($backdropUrl, 'backdrops', $itemName);
|
||||
}
|
||||
|
||||
protected function getProcessedCount(): int
|
||||
{
|
||||
return $this->processedCount;
|
||||
|
||||
@@ -31,8 +31,9 @@ class StashSyncService extends BaseSyncService
|
||||
'headers' => [
|
||||
'User-Agent' => 'MediaCollector/1.0',
|
||||
'Content-Type' => 'application/json',
|
||||
'ApiKey' => $this->apiKey // Now safe to access
|
||||
]
|
||||
'ApiKey' => $this->apiKey // Stash API key for authentication
|
||||
],
|
||||
'verify' => false // Disable SSL verification for problematic servers
|
||||
]);
|
||||
|
||||
$this->imageDownloader = new ImageDownloader('public/images', $this->apiKey);
|
||||
@@ -60,21 +61,70 @@ class StashSyncService extends BaseSyncService
|
||||
try {
|
||||
$this->logProgress('Fetching Stash scenes...');
|
||||
|
||||
// First, get the total count to determine how many pages we need
|
||||
$totalCount = $this->getStashScenesCount();
|
||||
|
||||
if ($totalCount === 0) {
|
||||
$this->logProgress('No scenes found in Stash');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logProgress("Found {$totalCount} scenes in Stash");
|
||||
|
||||
// Use pagination to handle large libraries
|
||||
$page = 0;
|
||||
$perPage = 50; // Smaller batch size for reliability
|
||||
$totalPages = ceil($totalCount / $perPage);
|
||||
|
||||
do {
|
||||
$scenes = $this->getStashScenes($page * $perPage, $perPage);
|
||||
$this->logProgress("Processing page {$page} with " . count($scenes) . " scenes...");
|
||||
for ($page = 0; $page < $totalPages; $page++) {
|
||||
try {
|
||||
$offset = $page * $perPage;
|
||||
$scenes = $this->getStashScenes($offset, $perPage);
|
||||
|
||||
foreach ($scenes as $sceneData) {
|
||||
$this->syncScene($sceneData);
|
||||
$this->processedCount++;
|
||||
if (empty($scenes)) {
|
||||
$this->logProgress("No scenes returned for page {$page}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->logProgress("Processing page {$page} with " . count($scenes) . " scenes...");
|
||||
|
||||
foreach ($scenes as $sceneData) {
|
||||
try {
|
||||
$this->logProgress("Processing scene: {$sceneData['title']} (ID: {$sceneData['id']})");
|
||||
$this->syncScene($sceneData);
|
||||
$this->processedCount++;
|
||||
|
||||
// Update progress in real-time
|
||||
$this->updateSyncLog($this->currentSyncLogId, 'running', [
|
||||
'processed_items' => $this->processedCount,
|
||||
'new_items' => $this->newCount,
|
||||
'updated_items' => $this->updatedCount,
|
||||
'message' => "Processed {$this->processedCount} of ~{$totalCount} scenes"
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Error processing scene {$sceneData['id']} ({$sceneData['title']}): " . $e->getMessage());
|
||||
$this->processedCount++; // Still count as processed even if failed
|
||||
|
||||
// Update progress even for failed items
|
||||
$this->updateSyncLog($this->currentSyncLogId, 'running', [
|
||||
'processed_items' => $this->processedCount,
|
||||
'message' => "Error on scene {$sceneData['id']}: " . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Error fetching page {$page}: " . $e->getMessage());
|
||||
// Continue with next page even if this page fails
|
||||
$this->updateSyncLog($this->currentSyncLogId, 'running', [
|
||||
'message' => "Failed to fetch page {$page}, continuing with next page"
|
||||
]);
|
||||
}
|
||||
|
||||
$page++;
|
||||
} while (count($scenes) === $perPage); // Continue if we got a full page
|
||||
// Add a small delay between pages to avoid overwhelming the server
|
||||
if ($page < $totalPages - 1) {
|
||||
sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
$this->logProgress("Completed syncing Stash scenes");
|
||||
} catch (Exception $e) {
|
||||
@@ -83,6 +133,48 @@ class StashSyncService extends BaseSyncService
|
||||
}
|
||||
}
|
||||
|
||||
private function getStashScenesCount(): int
|
||||
{
|
||||
try {
|
||||
$query = '
|
||||
query FindScenes($filter: FindFilterType) {
|
||||
findScenes(filter: $filter) {
|
||||
count
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$variables = [
|
||||
'filter' => [
|
||||
'per_page' => 1,
|
||||
'page' => 1,
|
||||
'sort' => 'created_at',
|
||||
'direction' => 'DESC'
|
||||
]
|
||||
];
|
||||
|
||||
$response = $this->httpClient->post("{$this->baseUrl}/graphql", [
|
||||
'json' => [
|
||||
'query' => $query,
|
||||
'variables' => $variables
|
||||
],
|
||||
'timeout' => 30
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody(), true);
|
||||
|
||||
if (!isset($data['data']['findScenes']['count'])) {
|
||||
$this->logProgress('No count data in Stash response');
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) $data['data']['findScenes']['count'];
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress('Failed to get Stash scenes count: ' . $e->getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private function getStashScenes(int $offset = 0, int $limit = 50): array
|
||||
{
|
||||
try {
|
||||
@@ -118,15 +210,12 @@ class StashSyncService extends BaseSyncService
|
||||
width
|
||||
height
|
||||
}
|
||||
paths {
|
||||
screenshot
|
||||
}
|
||||
performers {
|
||||
id
|
||||
name
|
||||
disambiguation
|
||||
url
|
||||
gender
|
||||
gender
|
||||
birthdate
|
||||
ethnicity
|
||||
country
|
||||
@@ -166,24 +255,31 @@ class StashSyncService extends BaseSyncService
|
||||
]
|
||||
];
|
||||
|
||||
$this->logProgress("Fetching Stash scenes: offset={$offset}, limit={$limit}");
|
||||
|
||||
$response = $this->httpClient->post("{$this->baseUrl}/graphql", [
|
||||
'json' => [
|
||||
'query' => $query,
|
||||
'variables' => $variables
|
||||
],
|
||||
'timeout' => 30
|
||||
'timeout' => 60, // Increased timeout
|
||||
'connect_timeout' => 30
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody(), true);
|
||||
|
||||
if (!isset($data['data']['findScenes']['scenes'])) {
|
||||
$this->logProgress('No scenes data in response');
|
||||
$this->logProgress('No scenes data in Stash response');
|
||||
return [];
|
||||
}
|
||||
|
||||
return $data['data']['findScenes']['scenes'];
|
||||
$scenes = $data['data']['findScenes']['scenes'];
|
||||
$this->logProgress("Received " . count($scenes) . " scenes from Stash");
|
||||
|
||||
return $scenes;
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress('Failed to fetch Stash scenes: ' . $e->getMessage());
|
||||
$this->logProgress('Request details: ' . $e->getMessage());
|
||||
throw new Exception('Failed to fetch Stash scenes: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
@@ -309,28 +405,66 @@ class StashSyncService extends BaseSyncService
|
||||
$coverUrl = $screenshotUrl;
|
||||
}
|
||||
|
||||
if (!empty($coverUrl)) {
|
||||
$coverFilename = $this->imageDownloader->generateFilename($coverUrl, 'cover');
|
||||
$localCoverPath = $this->imageDownloader->downloadImage($coverUrl, $coverFilename, 'adult_videos');
|
||||
if ($localCoverPath) {
|
||||
$sceneData['local_cover_path'] = $this->imageDownloader->getPublicUrl($localCoverPath);
|
||||
$this->logProgress("Downloaded cover: " . $localCoverPath);
|
||||
// Check if this is an existing scene and if images already exist
|
||||
$shouldDownloadImages = true;
|
||||
if ($existingScene) {
|
||||
$existingMetadata = json_decode($existingScene['metadata'], true);
|
||||
$hasExistingCover = !empty($existingMetadata['local_cover_path']);
|
||||
$hasExistingScreenshot = !empty($existingMetadata['local_screenshot_path']);
|
||||
|
||||
if ($hasExistingCover && $hasExistingScreenshot) {
|
||||
$shouldDownloadImages = false;
|
||||
$this->logProgress("Scene {$sceneData['id']} already has images, skipping download");
|
||||
} else {
|
||||
$this->logProgress("Failed to download cover from: " . $coverUrl);
|
||||
$this->logProgress("Scene {$sceneData['id']} missing images - downloading");
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($screenshotUrl)) {
|
||||
$screenshotFilename = $this->imageDownloader->generateFilename($screenshotUrl, 'screenshot');
|
||||
$localScreenshotPath = $this->imageDownloader->downloadImage($screenshotUrl, $screenshotFilename, 'adult_videos');
|
||||
if ($localScreenshotPath) {
|
||||
$sceneData['local_screenshot_path'] = $this->imageDownloader->getPublicUrl($localScreenshotPath);
|
||||
$this->logProgress("Downloaded screenshot: " . $localScreenshotPath);
|
||||
} else {
|
||||
$this->logProgress("Failed to download screenshot from: " . $screenshotUrl);
|
||||
if ($shouldDownloadImages) {
|
||||
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) {
|
||||
$sceneData['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) {
|
||||
$sceneData['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 {
|
||||
// Use existing image paths
|
||||
$sceneData['local_cover_path'] = $existingMetadata['local_cover_path'] ?? null;
|
||||
$sceneData['local_screenshot_path'] = $existingMetadata['local_screenshot_path'] ?? null;
|
||||
}
|
||||
// Handle performers/actors
|
||||
$performers = $sceneData['performers'] ?? [];
|
||||
$actorNames = [];
|
||||
@@ -366,11 +500,75 @@ class StashSyncService extends BaseSyncService
|
||||
];
|
||||
|
||||
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) {
|
||||
$sceneData['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
|
||||
$sceneData['local_cover_path'] = $existingMetadata['local_cover_path'] ?? null;
|
||||
if (!empty($sceneData['local_cover_path'])) {
|
||||
$this->logProgress("Using existing cover: " . $sceneData['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) {
|
||||
$sceneData['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
|
||||
$sceneData['local_screenshot_path'] = $existingMetadata['local_screenshot_path'] ?? null;
|
||||
if (!empty($sceneData['local_screenshot_path'])) {
|
||||
$this->logProgress("Using existing screenshot: " . $sceneData['local_screenshot_path']);
|
||||
}
|
||||
}
|
||||
|
||||
$adultVideoModel->update($existingScene['id'], $sceneData);
|
||||
$adultVideoId = $existingScene['id'];
|
||||
$this->updatedCount++;
|
||||
|
||||
// Create actor relationships for existing scene
|
||||
$this->createActorRelationships($adultVideoId, $actors);
|
||||
} else {
|
||||
$adultVideoModel->create($sceneData);
|
||||
$adultVideoId = $this->pdo->lastInsertId();
|
||||
$this->newCount++;
|
||||
|
||||
// Create actor relationships for new scene
|
||||
$this->createActorRelationships($adultVideoId, $actors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,26 +655,38 @@ class StashSyncService extends BaseSyncService
|
||||
// Try to download performer image if available
|
||||
$thumbnailPath = null;
|
||||
if ($imagePath) {
|
||||
// Handle different image path formats from Stash
|
||||
if (strpos($imagePath, 'http') === 0) {
|
||||
// Already a full URL
|
||||
$imageUrl = $imagePath;
|
||||
} elseif (strpos($imagePath, '/') === 0) {
|
||||
// Absolute path from Stash root
|
||||
$imageUrl = "{$this->baseUrl}" . $imagePath;
|
||||
} else {
|
||||
// Relative path - assume it's in performer images directory
|
||||
$imageUrl = "{$this->baseUrl}/performer/" . $imagePath;
|
||||
}
|
||||
// Validate image path before constructing URL
|
||||
if (!empty(trim($imagePath))) {
|
||||
try {
|
||||
// Handle different image path formats from Stash
|
||||
if (strpos($imagePath, 'http') === 0) {
|
||||
// Already a full URL
|
||||
$imageUrl = $imagePath;
|
||||
} elseif (strpos($imagePath, '/') === 0) {
|
||||
// Absolute path from Stash root
|
||||
$imageUrl = "{$this->baseUrl}" . $imagePath;
|
||||
} else {
|
||||
// Relative path - assume it's in performer images directory
|
||||
$imageUrl = "{$this->baseUrl}/performer/" . $imagePath;
|
||||
}
|
||||
|
||||
$this->logProgress("Performer 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 performer image: " . $localImagePath);
|
||||
} else {
|
||||
$this->logProgress("Failed to download performer image from: " . $imageUrl);
|
||||
// Validate the constructed URL
|
||||
if (filter_var($imageUrl, FILTER_VALIDATE_URL)) {
|
||||
$this->logProgress("Performer 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 performer image: " . $localImagePath);
|
||||
} else {
|
||||
$this->logProgress("Failed to download performer image from: " . $imageUrl);
|
||||
}
|
||||
} else {
|
||||
$this->logProgress("Invalid performer image URL constructed: " . $imageUrl);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Exception downloading performer image for {$name} from {$imagePath}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,6 +727,29 @@ class StashSyncService extends BaseSyncService
|
||||
return $this->updatedCount;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getDeletedCount(): int
|
||||
{
|
||||
return 0; // Stash doesn't provide deletion info in this context
|
||||
|
||||
@@ -10,7 +10,6 @@ use Exception;
|
||||
class XbvrSyncService extends BaseSyncService
|
||||
{
|
||||
private Client $httpClient;
|
||||
private ?string $apiKey;
|
||||
private string $baseUrl;
|
||||
private ImageDownloader $imageDownloader;
|
||||
private int $processedCount = 0;
|
||||
@@ -22,24 +21,22 @@ class XbvrSyncService extends BaseSyncService
|
||||
parent::__construct($pdo, $source);
|
||||
|
||||
// Initialize properties first before using them
|
||||
$this->apiKey = $source['api_key'];
|
||||
$this->baseUrl = rtrim($source['api_url'], '/');
|
||||
|
||||
$this->httpClient = new Client([
|
||||
'timeout' => 30,
|
||||
'headers' => [
|
||||
'User-Agent' => 'MediaCollector/1.0',
|
||||
'X-API-Key' => $source['api_key']
|
||||
'User-Agent' => 'MediaCollector/1.0'
|
||||
]
|
||||
]);
|
||||
|
||||
$this->imageDownloader = new ImageDownloader('public/images', $this->apiKey);
|
||||
$this->imageDownloader = new ImageDownloader('public/images');
|
||||
}
|
||||
|
||||
protected function executeSync(string $syncType): void
|
||||
{
|
||||
if (empty($this->apiKey) || empty($this->baseUrl)) {
|
||||
throw new Exception('XBVR API key and URL not configured');
|
||||
if (empty($this->baseUrl)) {
|
||||
throw new Exception('XBVR URL not configured');
|
||||
}
|
||||
|
||||
$this->logProgress('Starting XBVR library sync...');
|
||||
@@ -52,12 +49,17 @@ class XbvrSyncService extends BaseSyncService
|
||||
|
||||
private function syncScenes(): void
|
||||
{
|
||||
try {
|
||||
try {
|
||||
$scenes = $this->getXbvrScenes();
|
||||
|
||||
foreach ($scenes as $sceneData) {
|
||||
$this->syncScene($sceneData);
|
||||
$this->processedCount++;
|
||||
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());
|
||||
@@ -67,16 +69,73 @@ class XbvrSyncService extends BaseSyncService
|
||||
private function getXbvrScenes(): array
|
||||
{
|
||||
try {
|
||||
// XBVR API endpoint for scenes
|
||||
$response = $this->httpClient->get("{$this->baseUrl}/api/scene");
|
||||
$this->logProgress("Fetching XBVR DeoVR main response from: {$this->baseUrl}/deovr");
|
||||
|
||||
$data = json_decode($response->getBody(), true);
|
||||
// Step 1: Fetch the main DeoVR response containing the video list
|
||||
$response = $this->httpClient->get("{$this->baseUrl}/deovr", [
|
||||
'timeout' => 30,
|
||||
'connect_timeout' => 10
|
||||
]);
|
||||
|
||||
if (!isset($data['scenes'])) {
|
||||
throw new Exception('No scenes found in XBVR');
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
throw new Exception("XBVR DeoVR API returned status: " . $response->getStatusCode());
|
||||
}
|
||||
|
||||
return $data['scenes'];
|
||||
$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());
|
||||
}
|
||||
@@ -84,6 +143,8 @@ class XbvrSyncService extends BaseSyncService
|
||||
|
||||
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
|
||||
@@ -103,84 +164,221 @@ class XbvrSyncService extends BaseSyncService
|
||||
}
|
||||
}
|
||||
|
||||
// Download images locally
|
||||
$coverFilename = null;
|
||||
$screenshotFilename = null;
|
||||
// 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
|
||||
];
|
||||
|
||||
// Extract image URLs from XBVR API response
|
||||
// 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;
|
||||
|
||||
if (!empty($sceneData['cover_url'])) {
|
||||
$coverUrl = $sceneData['cover_url'];
|
||||
$this->logProgress("Cover URL: " . $coverUrl);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($sceneData['screenshot_url'])) {
|
||||
$screenshotUrl = $sceneData['screenshot_url'];
|
||||
$this->logProgress("Screenshot URL: " . $screenshotUrl);
|
||||
// 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)) {
|
||||
$coverFilename = $this->imageDownloader->generateFilename($coverUrl, 'cover');
|
||||
$localCoverPath = $this->imageDownloader->downloadImage($coverUrl, $coverFilename, 'adult_videos');
|
||||
if ($localCoverPath) {
|
||||
$sceneData['local_cover_path'] = $this->imageDownloader->getPublicUrl($localCoverPath);
|
||||
$this->logProgress("Downloaded cover: " . $localCoverPath);
|
||||
$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("Failed to download cover from: " . $coverUrl);
|
||||
$this->logProgress("Invalid cover URL: " . $coverUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($screenshotUrl)) {
|
||||
$screenshotFilename = $this->imageDownloader->generateFilename($screenshotUrl, 'screenshot');
|
||||
$localScreenshotPath = $this->imageDownloader->downloadImage($screenshotUrl, $screenshotFilename, 'adult_videos');
|
||||
if ($localScreenshotPath) {
|
||||
$sceneData['local_screenshot_path'] = $this->imageDownloader->getPublicUrl($localScreenshotPath);
|
||||
$this->logProgress("Downloaded screenshot: " . $localScreenshotPath);
|
||||
// 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("Failed to download screenshot from: " . $screenshotUrl);
|
||||
$this->logProgress("Invalid screenshot URL: " . $screenshotUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle actors
|
||||
$actors = $this->syncActors($sceneData['cast'] ?? []);
|
||||
$actors = $this->syncActors($castData);
|
||||
|
||||
$sceneData = [
|
||||
'title' => $sceneData['title'] ?: 'Untitled VR Scene',
|
||||
'overview' => $sceneData['synopsis'] ?? null,
|
||||
'release_date' => $sceneData['release_date'] ? date('Y-m-d', strtotime($sceneData['release_date'])) : null,
|
||||
'runtime_minutes' => $sceneData['duration'] ?? null,
|
||||
'rating' => $sceneData['rating'] ?? null,
|
||||
$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'] ?? null,
|
||||
'cast' => $sceneData['cast'] ?? [],
|
||||
'xbvr_url' => $sceneData['scene_url'] ?? $sceneData['url'] ?? null,
|
||||
'cast' => $castData,
|
||||
'actors' => $actors,
|
||||
'tags' => $sceneData['tags'] ?? [],
|
||||
'tags' => $mappedData['tags'],
|
||||
'is_available' => $sceneData['is_available'] ?? true,
|
||||
'is_watched' => $sceneData['is_watched'] ?? false,
|
||||
'watch_count' => $sceneData['watch_count'] ?? 0,
|
||||
'video_length' => $sceneData['video_length'] ?? null,
|
||||
'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'] ?? null,
|
||||
'cover_url' => $sceneData['cover_url'] ?? null,
|
||||
'local_cover_path' => $sceneData['local_cover_path'] ?? null,
|
||||
'screenshot_url' => $sceneData['screenshot_url'] ?? null,
|
||||
'local_screenshot_path' => $sceneData['local_screenshot_path'] ?? 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) {
|
||||
$adultVideoModel->update($existingScene['id'], $sceneData);
|
||||
// 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($sceneData);
|
||||
$adultVideoModel->create($sceneDataForDb);
|
||||
$adultVideoId = $this->pdo->lastInsertId();
|
||||
$this->newCount++;
|
||||
|
||||
// Create actor relationships for new scene
|
||||
$this->createActorRelationships($adultVideoId, $actors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +436,48 @@ class XbvrSyncService extends BaseSyncService
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -257,4 +497,27 @@ class XbvrSyncService extends BaseSyncService
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user