Files
MediaCollectorLibary/app/Services/JellyfinSyncService.php
Lars Behrends 929ee43001 first commit
2025-10-17 13:29:28 +02:00

316 lines
11 KiB
PHP

<?php
namespace App\Services;
use App\Models\Movie;
use App\Models\TvShow;
use App\Models\TvEpisode;
use GuzzleHttp\Client;
use Exception;
class JellyfinSyncService extends BaseSyncService
{
private Client $httpClient;
private ?string $apiKey;
private string $baseUrl;
private int $processedCount = 0;
private int $newCount = 0;
private int $updatedCount = 0;
public function __construct(\PDO $pdo, array $source)
{
parent::__construct($pdo, $source);
$this->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
}
}