first commit

This commit is contained in:
Lars Behrends
2025-10-17 13:29:28 +02:00
commit 929ee43001
85 changed files with 10361 additions and 0 deletions

View File

@@ -0,0 +1,211 @@
<?php
namespace App\Services;
use App\Models\AdultVideo;
use GuzzleHttp\Client;
use PDO;
use Exception;
class AdultSyncService extends BaseSyncService
{
private Client $httpClient;
private array $xbvrSource;
private array $stashSource;
private int $processedCount = 0;
private int $newCount = 0;
private int $updatedCount = 0;
public function __construct(PDO $pdo, array $source)
{
parent::__construct($pdo, $source);
// Find XBVR and Stash sources
$this->xbvrSource = $this->findSourceByName('xbvr');
$this->stashSource = $this->findSourceByName('stash');
$this->httpClient = new Client([
'timeout' => 60,
'headers' => [
'User-Agent' => 'MediaCollector/1.0',
'Content-Type' => 'application/json'
]
]);
}
private function findSourceByName(string $name): ?array
{
$stmt = $this->pdo->prepare("SELECT * FROM sources WHERE name = :name AND is_active = 1 LIMIT 1");
$stmt->execute(['name' => $name]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
protected function executeSync(string $syncType): void
{
if (!$this->xbvrSource && !$this->stashSource) {
throw new Exception('No active XBVR or Stash sources found');
}
$this->logProgress('Starting adult content library sync...');
// Sync XBVR content
if ($this->xbvrSource) {
$this->syncXbvrContent();
}
// Sync Stash content
if ($this->stashSource) {
$this->syncStashContent();
}
$this->logProgress("Processed {$this->processedCount} adult content items");
}
private function syncXbvrContent(): void
{
if (!$this->xbvrSource) return;
try {
$xbvrService = new XbvrSyncService($this->pdo, $this->xbvrSource);
$xbvrService->startSync('full');
$this->logProgress("XBVR content synced directly to adult videos");
} catch (Exception $e) {
$this->logProgress("Error syncing XBVR content: " . $e->getMessage());
}
}
private function syncStashContent(): void
{
if (!$this->stashSource) return;
try {
$stashService = new StashSyncService($this->pdo, $this->stashSource);
$stashService->startSync('full');
$this->logProgress("Stash content synced directly to adult videos");
} catch (Exception $e) {
$this->logProgress("Error syncing Stash content: " . $e->getMessage());
}
}
private function migrateXbvrToAdultVideos(): void
{
// Get all movies from XBVR source
$stmt = $this->pdo->prepare("
SELECT m.* FROM movies m
WHERE m.source_id = :xbvr_source_id
");
$stmt->execute(['xbvr_source_id' => $this->xbvrSource['id']]);
$xbvrMovies = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($xbvrMovies as $movie) {
// Check if adult video already exists
$existingStmt = $this->pdo->prepare("
SELECT id FROM adult_videos
WHERE source_id = :adult_source_id AND external_id = :external_id
LIMIT 1
");
$existingStmt->execute([
'adult_source_id' => $this->source['id'],
'external_id' => $movie['id']
]);
if (!$existingStmt->fetch(PDO::FETCH_ASSOC)) {
// Create adult video from XBVR movie
$adultVideoData = [
'title' => $movie['title'],
'overview' => $movie['overview'],
'poster_url' => $movie['poster_url'],
'backdrop_url' => $movie['backdrop_url'],
'rating' => $movie['rating'],
'runtime_minutes' => $movie['runtime_minutes'],
'release_date' => $movie['release_date'],
'director' => $movie['director'],
'writer' => $movie['writer'],
'cast' => $movie['cast'],
'genre' => $movie['genre'],
'metadata' => $movie['metadata'],
'watched' => $movie['watched'],
'watch_count' => $movie['watch_count'],
'is_favorite' => $movie['is_favorite'],
'source_id' => $this->source['id'],
'external_id' => $movie['id'],
'created_at' => $movie['created_at'],
'updated_at' => $movie['updated_at']
];
$columns = array_keys($adultVideoData);
$placeholders = array_map(fn($col) => ":$col", $columns);
$sql = "INSERT INTO adult_videos (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";
$insertStmt = $this->pdo->prepare($sql);
$insertStmt->execute($adultVideoData);
$this->processedCount++;
}
}
$this->logProgress("Processed {$this->processedCount} XBVR items");
}
private function migrateStashToAdultVideos(): void
{
// Get all movies from Stash source
$stmt = $this->pdo->prepare("
SELECT m.* FROM movies m
WHERE m.source_id = :stash_source_id
");
$stmt->execute(['stash_source_id' => $this->stashSource['id']]);
$stashMovies = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($stashMovies as $movie) {
// Check if adult video already exists
$existingStmt = $this->pdo->prepare("
SELECT id FROM adult_videos
WHERE source_id = :adult_source_id AND external_id = :external_id
LIMIT 1
");
$existingStmt->execute([
'adult_source_id' => $this->source['id'],
'external_id' => $movie['id']
]);
if (!$existingStmt->fetch(PDO::FETCH_ASSOC)) {
// Create adult video from Stash movie
$adultVideoData = [
'title' => $movie['title'],
'overview' => $movie['overview'],
'poster_url' => $movie['poster_url'],
'backdrop_url' => $movie['backdrop_url'],
'rating' => $movie['rating'],
'runtime_minutes' => $movie['runtime_minutes'],
'release_date' => $movie['release_date'],
'director' => $movie['director'],
'writer' => $movie['writer'],
'cast' => $movie['cast'],
'genre' => $movie['genre'],
'metadata' => $movie['metadata'],
'watched' => $movie['watched'],
'watch_count' => $movie['watch_count'],
'is_favorite' => $movie['is_favorite'],
'source_id' => $this->source['id'],
'external_id' => $movie['id'],
'created_at' => $movie['created_at'],
'updated_at' => $movie['updated_at']
];
$columns = array_keys($adultVideoData);
$placeholders = array_map(fn($col) => ":$col", $columns);
$sql = "INSERT INTO adult_videos (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";
$insertStmt = $this->pdo->prepare($sql);
$insertStmt->execute($adultVideoData);
$this->processedCount++;
}
}
$this->logProgress("Processed {$this->processedCount} Stash items");
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace App\Services;
use App\Models\User;
use PDO;
class AuthService
{
private PDO $pdo;
private ?array $user = null;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
$this->checkSession();
}
public function checkSession(): void
{
if (isset($_SESSION['user_id'])) {
$user = User::findByUsername($this->pdo, $_SESSION['username']);
if (!$user || !$user['is_active']) {
$this->logout();
} else {
$this->user = $user;
}
}
}
public function login(string $username, string $password, string $ip = null): bool
{
$user = User::findByUsername($this->pdo, $username);
if (!$user || !$user['is_active']) {
return false;
}
// Verify password directly using the hash from database
if (!password_verify($password, $user['password'])) {
return false;
}
// Set session
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
// Update last login
$this->updateUserLastLogin($user['id'], $ip);
$this->user = $user;
return true;
}
public function logout(): void
{
unset($_SESSION['user_id']);
unset($_SESSION['username']);
unset($_SESSION['role']);
$this->user = null;
}
public function getCurrentUser(): ?array
{
return $this->user;
}
public function isLoggedIn(): bool
{
return $this->user !== null;
}
public function isAdmin(): bool
{
return $this->isLoggedIn() && $this->user['role'] === 'admin';
}
public function requireLogin(): void
{
if (!$this->isLoggedIn()) {
header('Location: /login');
exit;
}
}
public function requireAdmin(): void
{
$this->requireLogin();
if (!$this->isAdmin()) {
http_response_code(403);
echo 'Access denied. Admin privileges required.';
exit;
}
}
public function generateCSRFToken(): string
{
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
public function verifyCSRFToken(string $token): bool
{
return isset($_SESSION['csrf_token']) && $_SESSION['csrf_token'] === $token;
}
private function updateUserLastLogin(int $userId, string $ip = null): void
{
$stmt = $this->pdo->prepare("UPDATE users SET last_login_at = :last_login_at, login_ip = :login_ip WHERE id = :id");
$stmt->execute([
'id' => $userId,
'last_login_at' => date('Y-m-d H:i:s'),
'login_ip' => $ip
]);
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace App\Services;
use App\Models\SyncLog;
use Exception;
abstract class BaseSyncService
{
protected \PDO $pdo;
protected array $source;
protected SyncLog $syncLog;
protected int $sourceId;
public function __construct(\PDO $pdo, array $source)
{
$this->pdo = $pdo;
$this->source = $source;
if (!isset($source['id']) || empty($source['id'])) {
throw new \Exception('Source ID is required for sync service');
}
$this->sourceId = (int) $source['id'];
}
public function startSync(string $syncType = 'full'): int
{
// Create sync log entry
$this->syncLog = new SyncLog($this->pdo);
$syncLogId = $this->createSyncLog($syncType, 'started');
$this->syncLog->id = $syncLogId;
try {
$this->executeSync($syncType);
// Update sync log as completed
$this->updateSyncLog($syncLogId, 'completed', [
'processed_items' => $this->getProcessedCount(),
'new_items' => $this->getNewCount(),
'updated_items' => $this->getUpdatedCount(),
'deleted_items' => $this->getDeletedCount()
]);
} catch (Exception $e) {
// Update sync log as failed
$this->updateSyncLog($syncLogId, 'failed', [
'message' => $e->getMessage(),
'errors' => [$e->getMessage()]
]);
throw $e;
}
return $syncLogId;
}
private function createSyncLog(string $syncType, string $status): int
{
$data = [
'source_id' => $this->sourceId,
'sync_type' => $syncType,
'status' => $status,
'total_items' => 0,
'processed_items' => 0,
'new_items' => 0,
'updated_items' => 0,
'deleted_items' => 0,
'started_at' => date('Y-m-d H:i:s')
];
$columns = array_keys($data);
$placeholders = array_map(fn($col) => ":$col", $columns);
$sql = "INSERT INTO sync_logs (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($data);
return (int) $this->pdo->lastInsertId();
}
private function updateSyncLog(int $syncLogId, string $status, array $stats = []): bool
{
$data = [
'status' => $status,
'processed_items' => $stats['processed_items'] ?? 0,
'new_items' => $stats['new_items'] ?? 0,
'updated_items' => $stats['updated_items'] ?? 0,
'deleted_items' => $stats['deleted_items'] ?? 0,
'completed_at' => date('Y-m-d H:i:s')
];
if (!empty($stats['errors'])) {
$data['errors'] = json_encode($stats['errors']);
}
if (!empty($stats['message'])) {
$data['message'] = $stats['message'];
}
$setClause = array_map(fn($col) => "$col = :$col", array_keys($data));
$sql = "UPDATE sync_logs SET " . implode(', ', $setClause) . " WHERE id = :id";
$data['id'] = $syncLogId;
$stmt = $this->pdo->prepare($sql);
return $stmt->execute($data);
}
abstract protected function executeSync(string $syncType): void;
protected function getProcessedCount(): int
{
return 0; // Override in subclasses
}
protected function getNewCount(): int
{
return 0; // Override in subclasses
}
protected function getUpdatedCount(): int
{
return 0; // Override in subclasses
}
protected function getDeletedCount(): int
{
return 0; // Override in subclasses
}
protected function logProgress(string $message): void
{
// Update sync log with progress message
if ($this->syncLog) {
$this->updateSyncLog($this->syncLog->id, 'running', [
'message' => $message
]);
}
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace App\Services;
use App\Models\Game;
use GuzzleHttp\Client;
use Exception;
class ExophaseSyncService 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',
'Authorization' => 'Bearer ' . $source['api_key']
]
]);
$this->apiKey = $source['api_key'];
$this->baseUrl = rtrim($source['api_url'] ?? 'https://api.exophase.com', '/');
}
protected function executeSync(string $syncType): void
{
if (empty($this->apiKey)) {
throw new Exception('Exophase API key not configured');
}
$this->logProgress('Starting Exophase gaming data sync...');
// Sync games from all supported platforms
$this->syncGames();
$this->logProgress("Processed {$this->processedCount} Exophase gaming items");
}
private function syncGames(): void
{
try {
$games = $this->getExophaseGames();
foreach ($games as $gameData) {
$this->syncGame($gameData);
$this->processedCount++;
}
} catch (Exception $e) {
$this->logProgress('Error syncing games: ' . $e->getMessage());
}
}
private function getExophaseGames(): array
{
try {
// Exophase API endpoint for user's games
$response = $this->httpClient->get("{$this->baseUrl}/v1/user/games");
$data = json_decode($response->getBody(), true);
if (!isset($data['games'])) {
throw new Exception('No games found in Exophase');
}
return $data['games'];
} catch (Exception $e) {
throw new Exception('Failed to fetch Exophase games: ' . $e->getMessage());
}
}
private function syncGame(array $gameData): void
{
$gameModel = new Game($this->pdo);
// Check if game already exists
$existingGame = $gameModel->findAll([
'source_id' => $this->sourceId
]);
// Find existing game by platform-specific ID or title
foreach ($existingGame as $game) {
$metadata = json_decode($game['metadata'], true);
if (isset($metadata['exophase_game_id']) && $metadata['exophase_game_id'] === $gameData['id']) {
$existingGame = [$game];
break;
}
}
$gameData = [
'title' => $gameData['title'] ?: 'Untitled Game',
'game_key' => Game::generateGameKey($gameData['title'], $gameData['platform'] ?? null),
'platform' => $this->mapPlatform($gameData['platform'] ?? 'unknown'),
'playtime_minutes' => $gameData['playtime_minutes'] ?? 0,
'completion_percentage' => $gameData['completion_percentage'] ?? 0,
'source_id' => $this->sourceId,
'last_played_at' => isset($gameData['last_played']) ? date('Y-m-d H:i:s', strtotime($gameData['last_played'])) : null,
'metadata' => json_encode([
'exophase_game_id' => $gameData['id'],
'exophase_platform' => $gameData['platform'] ?? null,
'achievements_earned' => $gameData['achievements_earned'] ?? 0,
'achievements_total' => $gameData['achievements_total'] ?? 0,
'trophies_earned' => $gameData['trophies_earned'] ?? 0,
'trophies_total' => $gameData['trophies_total'] ?? 0,
'gamerscore_earned' => $gameData['gamerscore_earned'] ?? 0,
'gamerscore_total' => $gameData['gamerscore_total'] ?? 0,
'last_achievement' => $gameData['last_achievement'] ?? null,
'first_achievement' => $gameData['first_achievement'] ?? null,
'rating' => $gameData['rating'] ?? null,
'genre' => $gameData['genre'] ?? null,
'developer' => $gameData['developer'] ?? null,
'publisher' => $gameData['publisher'] ?? null,
'release_date' => $gameData['release_date'] ?? null
])
];
if (empty($existingGame)) {
$gameModel->create($gameData);
$this->newCount++;
} else {
$gameModel->update($existingGame[0]['id'], $gameData);
$this->updatedCount++;
}
}
private function mapPlatform(string $platform): string
{
$platformMap = [
'steam' => 'PC',
'psn' => 'PlayStation',
'ps4' => 'PlayStation 4',
'ps5' => 'PlayStation 5',
'xbox' => 'Xbox',
'xbox360' => 'Xbox 360',
'xboxone' => 'Xbox One',
'xboxseries' => 'Xbox Series X/S',
'nintendo' => 'Nintendo',
'switch' => 'Nintendo Switch',
'epic' => 'Epic Games',
'gog' => 'GOG',
'origin' => 'Origin',
'uplay' => 'Ubisoft Connect',
'battlenet' => 'Battle.net'
];
return $platformMap[$platform] ?? ucfirst($platform);
}
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; // Exophase doesn't provide deletion info in this context
}
}

View File

@@ -0,0 +1,315 @@
<?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
}
}

View File

@@ -0,0 +1,486 @@
<?php
namespace App\Services;
use App\Utils\ImageDownloader;
use App\Models\AdultVideo;
use GuzzleHttp\Client;
use PDO;
use Exception;
class StashSyncService extends BaseSyncService
{
private Client $httpClient;
private ?string $apiKey;
private string $baseUrl;
private ImageDownloader $imageDownloader;
private int $processedCount = 0;
private int $newCount = 0;
private int $updatedCount = 0;
public function __construct(PDO $pdo, array $source)
{
parent::__construct($pdo, $source);
$this->httpClient = new Client([
'timeout' => 60, // Stash can be slow
'headers' => [
'User-Agent' => 'MediaCollector/1.0',
'Content-Type' => 'application/json'
]
]);
$this->apiKey = $source['api_key'];
$this->baseUrl = rtrim($source['api_url'], '/');
$this->imageDownloader = new ImageDownloader();
}
protected function executeSync(string $syncType): void
{
if (empty($this->apiKey) || empty($this->baseUrl)) {
throw new Exception('Stash API key and URL not configured');
}
$this->logProgress('Starting Stash library sync...');
// Sync scenes (movies)
$this->syncScenes();
// Sync movies (if Stash has movie support)
$this->syncMovies();
$this->logProgress("Processed {$this->processedCount} Stash items");
}
private function syncScenes(): void
{
try {
$this->logProgress('Fetching Stash scenes...');
// Use pagination to handle large libraries
$page = 0;
$perPage = 50; // Smaller batch size for reliability
do {
$scenes = $this->getStashScenes($page * $perPage, $perPage);
$this->logProgress("Processing page {$page} with " . count($scenes) . " scenes...");
foreach ($scenes as $sceneData) {
$this->syncScene($sceneData);
$this->processedCount++;
}
$page++;
} while (count($scenes) === $perPage); // Continue if we got a full page
$this->logProgress("Completed syncing Stash scenes");
} catch (Exception $e) {
$this->logProgress('Error syncing scenes: ' . $e->getMessage());
throw $e;
}
}
private function getStashScenes(int $offset = 0, int $limit = 50): array
{
try {
$query = '
query FindScenes($filter: FindFilterType) {
findScenes(filter: $filter) {
scenes {
id
title
details
url
date
rating100
organized
o_counter
created_at
updated_at
paths {
screenshot
preview
stream
webp
vtt
sprite
funscript
caption
}
files {
size
duration
video_codec
audio_codec
width
height
}
paths {
screenshot
}
performers {
id
name
disambiguation
url
gender
birthdate
ethnicity
country
eye_color
height_cm
measurements
fake_tits
penis_length
circumcised
career_length
tattoos
piercings
alias_list
favorite
ignore_auto_tag
created_at
updated_at
details
death_date
hair_color
weight
image_path
scene_count
}
}
count
}
}
';
$variables = [
'filter' => [
'per_page' => $limit,
'page' => $offset / $limit + 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']['scenes'])) {
$this->logProgress('No scenes data in response');
return [];
}
return $data['data']['findScenes']['scenes'];
} catch (Exception $e) {
$this->logProgress('Failed to fetch Stash scenes: ' . $e->getMessage());
throw new Exception('Failed to fetch Stash scenes: ' . $e->getMessage());
}
}
private function syncMovies(): void
{
try {
$movies = $this->getStashMovies();
foreach ($movies as $movieData) {
$this->syncMovie($movieData);
$this->processedCount++;
}
} catch (Exception $e) {
$this->logProgress('Error syncing movies: ' . $e->getMessage());
}
}
private function getStashMovies(): array
{
try {
$query = '
query FindMovies($filter: FindFilterType) {
findMovies(filter: $filter) {
movies {
id
name
aliases
duration
date
rating100
director
synopsis
url
created_at
updated_at
front_image_path
back_image_path
}
count
}
}
';
$variables = [
'filter' => [
'per_page' => 100,
'sort' => 'created_at',
'direction' => 'DESC'
]
];
$response = $this->httpClient->post("{$this->baseUrl}/graphql", [
'json' => [
'query' => $query,
'variables' => $variables
]
]);
$data = json_decode($response->getBody(), true);
if (!isset($data['data']['findMovies']['movies'])) {
return []; // No movies found
}
return $data['data']['findMovies']['movies'];
} catch (Exception $e) {
// Return empty array if movies can't be fetched
return [];
}
}
private function syncScene(array $sceneData): void
{
$adultVideoModel = new AdultVideo($this->pdo);
// Check if scene already exists by stash_id in metadata
$stmt = $this->pdo->prepare("
SELECT id, metadata FROM adult_videos
WHERE source_id = :source_id
");
$stmt->execute(['source_id' => $this->source['id']]);
$existingScenes = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$existingScene = null;
foreach ($existingScenes as $scene) {
$metadata = json_decode($scene['metadata'], true);
if (isset($metadata['stash_id']) && $metadata['stash_id'] === $sceneData['id']) {
$existingScene = $scene;
break;
}
}
// Download images locally
$coverFilename = null;
$screenshotFilename = null;
// Extract image URLs from Stash API response
$coverUrl = null;
$screenshotUrl = null;
// Stash provides paths.screenshot for screenshot
if (!empty($sceneData['paths']['screenshot'])) {
// Convert relative path to full URL
$screenshotUrl = "{$this->baseUrl}/" . ltrim($sceneData['paths']['screenshot'], '/');
}
// For cover, we might need to use a different approach or check if there's a primary image
// For now, we'll use the screenshot as cover if available
if ($screenshotUrl) {
$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);
}
}
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);
}
}
// Handle performers/actors
$performers = $sceneData['performers'] ?? [];
$actorNames = [];
$performerImages = [];
foreach ($performers as $performer) {
$actorNames[] = $performer['name'];
$performerImages[$performer['name']] = $performer['image_path'] ?? null;
}
$actors = $this->syncActors($actorNames, $performerImages);
$sceneData = [
'title' => $sceneData['title'] ?: 'Untitled Scene',
'overview' => $sceneData['details'] ?? null,
'release_date' => $sceneData['date'] ? date('Y-m-d', strtotime($sceneData['date'])) : null,
'runtime_minutes' => !empty($sceneData['files'][0]['duration']) ? round($sceneData['files'][0]['duration'] / 60) : null,
'rating' => $sceneData['rating100'] ? $sceneData['rating100'] / 100 : null, // Convert from 0-100 to 0-10
'source_id' => $this->source['id'],
'external_id' => $sceneData['id'],
'metadata' => json_encode([
'stash_id' => $sceneData['id'],
'stash_url' => $sceneData['url'] ?? null,
'organized' => $sceneData['organized'] ?? false,
'o_counter' => $sceneData['o_counter'] ?? 0,
'performers' => $performers,
'actors' => $actors,
'file_info' => $sceneData['files'][0] ?? null,
'paths' => $sceneData['paths'] ?? null,
'cover_url' => $coverUrl,
'local_cover_path' => $sceneData['local_cover_path'] ?? null,
'screenshot_url' => $screenshotUrl,
'local_screenshot_path' => $sceneData['local_screenshot_path'] ?? null
])
];
if ($existingScene) {
$adultVideoModel->update($existingScene['id'], $sceneData);
$this->updatedCount++;
} else {
$adultVideoModel->create($sceneData);
$this->newCount++;
}
}
private function syncMovie(array $movieData): void
{
$adultVideoModel = new AdultVideo($this->pdo);
// Check if movie already exists by stash_movie_id in metadata
$stmt = $this->pdo->prepare("
SELECT id, metadata FROM adult_videos
WHERE source_id = :source_id
");
$stmt->execute(['source_id' => $this->source['id']]);
$existingMovies = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$existingMovie = null;
foreach ($existingMovies as $movie) {
$metadata = json_decode($movie['metadata'], true);
if (isset($metadata['stash_movie_id']) && $metadata['stash_movie_id'] === $movieData['id']) {
$existingMovie = $movie;
break;
}
}
$movieData = [
'title' => $movieData['name'] ?: 'Untitled Movie',
'overview' => $movieData['synopsis'] ?? null,
'director' => $movieData['director'] ?? null,
'release_date' => $movieData['date'] ? date('Y-m-d', strtotime($movieData['date'])) : null,
'runtime_minutes' => $movieData['duration'] ?? null,
'rating' => $movieData['rating100'] ? $movieData['rating100'] / 100 : null,
'source_id' => $this->source['id'],
'external_id' => $movieData['id'],
'metadata' => json_encode([
'stash_movie_id' => $movieData['id'],
'aliases' => $movieData['aliases'] ?? null,
'url' => $movieData['url'] ?? null
])
];
if ($existingMovie) {
$adultVideoModel->update($existingMovie['id'], $movieData);
$this->updatedCount++;
} else {
$adultVideoModel->create($movieData);
$this->newCount++;
}
}
private function syncActors(array $actorNames, array $performerImages = []): array
{
$actors = [];
foreach ($actorNames as $actorName) {
if (empty($actorName)) continue;
$imagePath = $performerImages[$actorName] ?? null;
$actor = $this->getOrCreateActor($actorName, $imagePath);
if ($actor) {
$actors[] = $actor;
}
}
return $actors;
}
private function getOrCreateActor(string $name, ?string $imagePath = null): ?array
{
// Check if actor already exists
$stmt = $this->pdo->prepare("
SELECT id, name, thumbnail_path FROM actors WHERE name = :name
");
$stmt->execute(['name' => $name]);
$existingActor = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existingActor) {
return [
'id' => $existingActor['id'],
'name' => $existingActor['name'],
'thumbnail_path' => $existingActor['thumbnail_path']
];
}
// Try to download performer image if available
$thumbnailPath = null;
if ($imagePath) {
$imageUrl = "{$this->baseUrl}/" . ltrim($imagePath, '/');
$thumbnailFilename = $this->imageDownloader->generateFilename($imageUrl, 'actor');
$localImagePath = $this->imageDownloader->downloadImage($imageUrl, $thumbnailFilename, 'actors');
if ($localImagePath) {
$thumbnailPath = $this->imageDownloader->getPublicUrl($localImagePath);
}
}
try {
$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 actor {$name}: " . $e->getMessage());
return null;
}
}
protected function getProcessedCount(): int
{
return $this->processedCount;
}
protected function getNewCount(): int
{
return $this->newCount;
}
protected function getUpdatedCount(): int
{
return $this->updatedCount;
}
protected function getDeletedCount(): int
{
return 0; // Stash doesn't provide deletion info in this context
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace App\Services;
use App\Models\Game;
use GuzzleHttp\Client;
use Exception;
class SteamSyncService extends BaseSyncService
{
private Client $httpClient;
private ?string $apiKey;
private string $steamId;
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'
]
]);
$this->apiKey = $source['api_key'];
// Steam ID can be configured in source config or use a default test account
$this->steamId = $source['config']['steam_id'] ?? '76561198000000000'; // Default test Steam ID
}
protected function executeSync(string $syncType): void
{
if (empty($this->apiKey)) {
throw new Exception('Steam API key not configured');
}
$this->logProgress('Starting Steam library sync...');
// Get Steam user game library
$games = $this->getSteamLibrary();
foreach ($games as $gameData) {
$this->syncGame($gameData);
$this->processedCount++;
}
$this->logProgress("Processed {$this->processedCount} Steam games");
}
private function getSteamLibrary(): array
{
try {
// Steam Web API: GetOwnedGames
$response = $this->httpClient->get('https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/', [
'query' => [
'key' => $this->apiKey,
'steamid' => $this->steamId,
'format' => 'json',
'include_appinfo' => 'true',
'include_played_free_games' => 'true'
]
]);
$data = json_decode($response->getBody(), true);
if (!isset($data['response']['games'])) {
throw new Exception('No games found in Steam library');
}
return $data['response']['games'];
} catch (Exception $e) {
throw new Exception('Failed to fetch Steam library: ' . $e->getMessage());
}
}
private function syncGame(array $gameData): void
{
$gameModel = new Game($this->pdo);
// Check if game already exists
$existingGame = $gameModel->findAll([
'steam_app_id' => $gameData['appid'],
'source_id' => $this->source['id']
]);
// Get additional game details from Steam API
$gameDetails = $this->getGameDetails($gameData['appid']);
$gameData = [
'title' => $gameData['name'],
'game_key' => Game::generateGameKey($gameData['name'], 'steam'),
'steam_app_id' => $gameData['appid'],
'playtime_minutes' => intval($gameData['playtime_forever']),
'platform' => 'PC',
'source_id' => $this->source['id'],
'last_played_at' => isset($gameData['rt_time_last_played']) && $gameData['rt_time_last_played'] > 0
? date('Y-m-d H:i:s', $gameData['rt_time_last_played'])
: null,
'metadata' => json_encode([
'appid' => $gameData['appid'],
'playtime_windows' => $gameData['playtime_windows_forever'] ?? 0,
'playtime_mac' => $gameData['playtime_mac_forever'] ?? 0,
'playtime_linux' => $gameData['playtime_linux_forever'] ?? 0,
'img_icon_url' => $gameDetails['img_icon_url'] ?? null,
'img_logo_url' => $gameDetails['img_logo_url'] ?? null,
'has_community_visible_stats' => $gameDetails['has_community_visible_stats'] ?? false
])
];
if (empty($existingGame)) {
$gameModel->create($gameData);
$this->newCount++;
} else {
$gameModel->update($existingGame[0]['id'], $gameData);
$this->updatedCount++;
}
}
private function getGameDetails(int $appId): array
{
try {
// Steam Web API: GetAppDetails
$response = $this->httpClient->get('https://store.steampowered.com/api/appdetails/', [
'query' => [
'appids' => $appId,
'cc' => 'US',
'l' => 'english'
]
]);
$data = json_decode($response->getBody(), true);
$appData = $data[$appId] ?? [];
if (!$appData['success']) {
return [];
}
return $appData['data'] ?? [];
} catch (Exception $e) {
// Return empty array if details can't be fetched
return [];
}
}
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; // Steam doesn't provide deletion info in this context
}
}

View File

@@ -0,0 +1,257 @@
<?php
namespace App\Services;
use App\Utils\ImageDownloader;
use App\Models\AdultVideo;
use GuzzleHttp\Client;
use Exception;
class XbvrSyncService extends BaseSyncService
{
private Client $httpClient;
private ?string $apiKey;
private string $baseUrl;
private ImageDownloader $imageDownloader;
private int $processedCount = 0;
private int $newCount = 0;
private int $updatedCount = 0;
public function __construct(\PDO $pdo, array $source)
{
parent::__construct($pdo, $source);
$this->httpClient = new Client([
'timeout' => 30,
'headers' => [
'User-Agent' => 'MediaCollector/1.0',
'X-API-Key' => $source['api_key']
]
]);
$this->apiKey = $source['api_key'];
$this->baseUrl = rtrim($source['api_url'], '/');
$this->imageDownloader = new ImageDownloader();
}
protected function executeSync(string $syncType): void
{
if (empty($this->apiKey) || empty($this->baseUrl)) {
throw new Exception('XBVR API key and URL not configured');
}
$this->logProgress('Starting XBVR library sync...');
// Sync VR scenes
$this->syncScenes();
$this->logProgress("Processed {$this->processedCount} XBVR items");
}
private function syncScenes(): void
{
try {
$scenes = $this->getXbvrScenes();
foreach ($scenes as $sceneData) {
$this->syncScene($sceneData);
$this->processedCount++;
}
} catch (Exception $e) {
$this->logProgress('Error syncing XBVR scenes: ' . $e->getMessage());
}
}
private function getXbvrScenes(): array
{
try {
// XBVR API endpoint for scenes
$response = $this->httpClient->get("{$this->baseUrl}/api/scene");
$data = json_decode($response->getBody(), true);
if (!isset($data['scenes'])) {
throw new Exception('No scenes found in XBVR');
}
return $data['scenes'];
} catch (Exception $e) {
throw new Exception('Failed to fetch XBVR scenes: ' . $e->getMessage());
}
}
private function syncScene(array $sceneData): void
{
$adultVideoModel = new AdultVideo($this->pdo);
// Check if scene already exists by xbvr_id in metadata
$stmt = $this->pdo->prepare("
SELECT id, metadata FROM adult_videos
WHERE source_id = :source_id
");
$stmt->execute(['source_id' => $this->source['id']]);
$existingScenes = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$existingScene = null;
foreach ($existingScenes as $scene) {
$metadata = json_decode($scene['metadata'], true);
if (isset($metadata['xbvr_id']) && $metadata['xbvr_id'] === $sceneData['id']) {
$existingScene = $scene;
break;
}
}
private function syncScene(array $sceneData): void
{
$adultVideoModel = new AdultVideo($this->pdo);
// Check if scene already exists by xbvr_id in metadata
$stmt = $this->pdo->prepare("
SELECT id, metadata FROM adult_videos
WHERE source_id = :source_id
");
$stmt->execute(['source_id' => $this->source['id']]);
$existingScenes = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$existingScene = null;
foreach ($existingScenes as $scene) {
$metadata = json_decode($scene['metadata'], true);
if (isset($metadata['xbvr_id']) && $metadata['xbvr_id'] === $sceneData['id']) {
$existingScene = $scene;
break;
}
}
// Download images locally
$coverFilename = null;
$screenshotFilename = null;
if (!empty($sceneData['cover_url'])) {
$coverFilename = $this->imageDownloader->generateFilename($sceneData['cover_url'], 'cover');
$localCoverPath = $this->imageDownloader->downloadImage($sceneData['cover_url'], $coverFilename, 'adult_videos');
if ($localCoverPath) {
$sceneData['local_cover_path'] = $this->imageDownloader->getPublicUrl($localCoverPath);
}
}
if (!empty($sceneData['screenshot_url'])) {
$screenshotFilename = $this->imageDownloader->generateFilename($sceneData['screenshot_url'], 'screenshot');
$localScreenshotPath = $this->imageDownloader->downloadImage($sceneData['screenshot_url'], $screenshotFilename, 'adult_videos');
if ($localScreenshotPath) {
$sceneData['local_screenshot_path'] = $this->imageDownloader->getPublicUrl($localScreenshotPath);
}
}
// Handle actors
$actors = $this->syncActors($sceneData['cast'] ?? []);
$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,
'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'] ?? [],
'actors' => $actors,
'tags' => $sceneData['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_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
])
];
if ($existingScene) {
$adultVideoModel->update($existingScene['id'], $sceneData);
$this->updatedCount++;
} else {
$adultVideoModel->create($sceneData);
$this->newCount++;
}
}
private function syncActors(array $cast): array
{
$actors = [];
foreach ($cast as $actorName) {
if (empty($actorName)) continue;
$actor = $this->getOrCreateActor($actorName);
if ($actor) {
$actors[] = $actor;
}
}
return $actors;
}
private function getOrCreateActor(string $name): ?array
{
// Check if actor already exists
$stmt = $this->pdo->prepare("
SELECT id, name, thumbnail_path FROM actors WHERE name = :name
");
$stmt->execute(['name' => $name]);
$existingActor = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($existingActor) {
return [
'id' => $existingActor['id'],
'name' => $existingActor['name'],
'thumbnail_path' => $existingActor['thumbnail_path']
];
}
// For now, we'll create actor without thumbnail
// In a full implementation, you'd fetch actor details from XBVR API
try {
$stmt = $this->pdo->prepare("
INSERT INTO actors (name, created_at, updated_at)
VALUES (:name, NOW(), NOW())
");
$stmt->execute(['name' => $name]);
$actorId = $this->pdo->lastInsertId();
return [
'id' => $actorId,
'name' => $name,
'thumbnail_path' => null
];
} catch (Exception $e) {
$this->logProgress("Failed to create actor {$name}: " . $e->getMessage());
return null;
}
}
protected function getProcessedCount(): int
{
return $this->processedCount;
}
protected function getNewCount(): int
{
return $this->newCount;
}
protected function getUpdatedCount(): int
{
return $this->updatedCount;
}
protected function getDeletedCount(): int
{
return 0; // XBVR doesn't provide deletion info in this context
}
}