httpClient = new Client([ 'timeout' => 30, 'headers' => [ 'User-Agent' => 'MediaCollector/1.0', 'X-MediaBrowser-Token' => $source['api_key'] ] ]); $this->apiKey = $source['api_key']; $this->baseUrl = rtrim($source['api_url'], '/'); } protected function executeSync(string $syncType): void { if (empty($this->apiKey) || empty($this->baseUrl)) { throw new Exception('Jellyfin API key and URL not configured'); } $this->logProgress('Starting Jellyfin library sync...'); $this->logProgress("Jellyfin URL: {$this->baseUrl}"); $this->logProgress("API Key: " . (empty($this->apiKey) ? 'NOT SET' : 'SET')); try { $userId = $this->getUserId(); $this->logProgress("User ID: {$userId}"); } catch (Exception $e) { $this->logProgress('Error getting user ID: ' . $e->getMessage()); throw $e; } // Sync 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; } 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; } // TODO: Sync TV shows and episodes when TvShow model is implemented // $this->syncTvShows(); $this->logProgress("Processed {$this->processedCount} items"); } private function syncMovies(): void { try { $movies = $this->getJellyfinItems('Movie'); foreach ($movies as $movieData) { $this->syncMovie($movieData); $this->processedCount++; } } catch (Exception $e) { $this->logProgress('Error syncing movies: ' . $e->getMessage()); } } private function syncTvShows(): void { try { $tvShows = $this->getJellyfinItems('Series'); foreach ($tvShows as $showData) { $this->syncTvShow($showData); $this->processedCount++; } } catch (Exception $e) { $this->logProgress('Error syncing TV shows: ' . $e->getMessage()); } } private function getJellyfinItems(string $type): array { try { $url = "{$this->baseUrl}/Users/{$this->getUserId()}/Items"; $this->logProgress("Fetching {$type} from: {$url}"); $response = $this->httpClient->get($url, [ 'query' => [ 'IncludeItemTypes' => $type, 'Recursive' => 'true', 'Fields' => 'ProviderIds,Overview,PremiereDate,CommunityRating,OfficialRating,Genres,Studios,RunTimeTicks' ] ]); $httpCode = $response->getStatusCode(); $this->logProgress("HTTP Response Code: {$httpCode}"); if ($httpCode !== 200) { throw new Exception("Jellyfin API returned HTTP {$httpCode}"); } $data = json_decode($response->getBody(), true); $itemCount = count($data['Items'] ?? []); $this->logProgress("Successfully fetched {$itemCount} {$type} items"); return $data['Items'] ?? []; } catch (Exception $e) { $this->logProgress('Failed to fetch Jellyfin items: ' . $e->getMessage()); throw new Exception('Failed to fetch Jellyfin items: ' . $e->getMessage()); } } private function getUserId(): string { try { $url = "{$this->baseUrl}/Users"; $this->logProgress("Getting user ID from: {$url}"); $response = $this->httpClient->get($url); $httpCode = $response->getStatusCode(); if ($httpCode !== 200) { throw new Exception("Jellyfin Users API returned HTTP {$httpCode}"); } $data = json_decode($response->getBody(), true); if (empty($data) || !isset($data[0]['Id'])) { throw new Exception('No users found in Jellyfin or invalid response format'); } $userId = $data[0]['Id']; $this->logProgress("Using Jellyfin user ID: {$userId}"); return $userId; } catch (Exception $e) { $this->logProgress('Failed to get Jellyfin user ID: ' . $e->getMessage()); throw new Exception('Failed to get Jellyfin user ID: ' . $e->getMessage()); } } private function syncMovie(array $movieData): void { $movieModel = new Movie($this->pdo); // Check if movie already exists $existingMovie = $movieModel->findAll([ 'tmdb_id' => $movieData['ProviderIds']['Tmdb'] ?? null, 'imdb_id' => $movieData['ProviderIds']['Imdb'] ?? null, 'source_id' => $this->source['id'] ]); $movieData = [ 'title' => $movieData['Name'], 'overview' => $movieData['Overview'] ?? null, 'release_date' => $movieData['PremiereDate'] ? date('Y-m-d', strtotime($movieData['PremiereDate'])) : null, 'runtime_minutes' => $movieData['RunTimeTicks'] ? intval($movieData['RunTimeTicks'] / (10000000 * 60)) : null, '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'] ?? [] ]) ]; if (empty($existingMovie)) { $movieModel->create($movieData); $this->newCount++; } else { $movieModel->update($existingMovie[0]['id'], $movieData); $this->updatedCount++; } } // TODO: Implement when TvShow model is created // private function syncTvShow(array $showData): void // { // $showModel = new TvShow($this->pdo); // // 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 // ]); // $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'] ?? [] // ]) // ]; // if (empty($existingShow)) { // $showId = $showModel->create($showData); // $this->newCount++; // } else { // $showId = $existingShow[0]['id']; // $showModel->update($showId, $showData); // $this->updatedCount++; // } // // Sync episodes for this show // $this->syncEpisodes($showId, $showData['Id']); // } // TODO: Implement when TvEpisode model is created // private function syncEpisodes(int $showId, string $jellyfinShowId): void // { // try { // $episodes = $this->getShowEpisodes($jellyfinShowId); // foreach ($episodes as $episodeData) { // $this->syncEpisode($showId, $episodeData); // } // } catch (Exception $e) { // $this->logProgress('Error syncing episodes for show ' . $jellyfinShowId . ': ' . $e->getMessage()); // } // } // TODO: Implement when TvEpisode model is created // private function syncEpisode(int $showId, array $episodeData): void // { // $episodeModel = new TvEpisode($this->pdo); // $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'] // ]) // ]; // $episodeModel->create($episodeData); // } private function getImageUrl(string $itemId, string $type): ?string { if (empty($itemId)) { return null; } return "{$this->baseUrl}/Items/{$itemId}/Images/{$type}?maxWidth=400"; } protected function getProcessedCount(): int { return $this->processedCount; } protected function getNewCount(): int { return $this->newCount; } protected function getUpdatedCount(): int { return $this->updatedCount; } protected function getDeletedCount(): int { return 0; // Jellyfin doesn't provide deletion info in this context } }