pdo = $pdo; } public function index(Request $request, Response $response, $args) { $sourceModel = new Source($this->pdo); $sources = $sourceModel->findAll(); $syncLogModel = new SyncLog($this->pdo); $recentSyncs = SyncLog::getRecent($this->pdo, 10); return $this->render($response, 'admin/index.twig', [ 'title' => 'Admin Dashboard', 'sources' => $sources, 'recent_syncs' => $recentSyncs ]); } /** * Media Management */ // Movies Management public function movies(Request $request, Response $response, $args) { $movieModel = new \App\Models\Movie($this->pdo); // Get query parameters with defaults $page = max(1, (int)($request->getQueryParams()['page'] ?? 1)); $search = trim($request->getQueryParams()['search'] ?? ''); $genre = trim($request->getQueryParams()['genre'] ?? ''); $director = trim($request->getQueryParams()['director'] ?? ''); $sort = trim($request->getQueryParams()['sort'] ?? 'title_asc'); $perPage = 20; // Prepare filters for the view $filters = [ 'search' => $search, 'genre' => $genre, 'director' => $director, 'sort' => $sort ]; // Get paginated and filtered movies $movies = $movieModel->getPaginated( $this->pdo, $page, $perPage, $search, $genre ? [$genre] : [], $sort ); // Get available genres and directors for filters $genres = $movieModel->getGenres($this->pdo); $directors = $movieModel->getDirectors($this->pdo); // Calculate pagination data $totalMovies = $movieModel->getTotalCount( $this->pdo, $search, $genre ? [$genre] : [] ); $totalPages = max(1, ceil($totalMovies / $perPage)); $currentPage = min($page, $totalPages); // Get flash messages if any // $successMessages = $this->container->get('flash')->getMessage('success'); return $this->render($response, 'admin/movies/index.twig', [ 'title' => 'Manage Movies', 'movies' => $movies, 'genres' => $genres, 'directors' => $directors, 'filters' => $filters, 'pagination' => [ 'current' => $currentPage, 'total' => $totalPages, 'per_page' => $perPage, 'total_items' => $totalMovies, 'from' => (($currentPage - 1) * $perPage) + 1, 'to' => min($currentPage * $perPage, $totalMovies) ] ]); } public function editMovie(Request $request, Response $response, $args) { $id = $args['id'] ?? null; $movieModel = new \App\Models\Movie($this->pdo); if ($request->getMethod() === 'POST') { $data = $request->getParsedBody(); if ($id) { // Update existing movie $movieModel->update($id, $data); //$this->flash->addMessage('success', 'Movie updated successfully'); } else { // Create new movie $id = $movieModel->create($data); // $this->flash->addMessage('success', 'Movie created successfully'); } return $response->withHeader('Location', '/admin/movies/' . $id . '/edit') ->withStatus(302); } $movie = $id ? $movieModel->find($id) : null; return $this->render($response, 'admin/movies/edit.twig', [ 'title' => $id ? 'Edit Movie' : 'Add New Movie', 'movie' => $movie ]); } public function deleteMovie(Request $request, Response $response, $args) { $id = $args['id']; $movieModel = new \App\Models\Movie($this->pdo); $movieModel->delete($id); $this->flash->addMessage('success', 'Movie deleted successfully'); return $response->withHeader('Location', '/admin/movies') ->withStatus(302); } // Games Management public function games(Request $request, Response $response, $args) { $gameModel = new \App\Models\Game($this->pdo); // Get query parameters $page = (int)($request->getQueryParams()['page'] ?? 1); $search = $request->getQueryParams()['search'] ?? ''; $platform = $request->getQueryParams()['platform'] ?? ''; $genre = $request->getQueryParams()['genre'] ?? ''; $isInstalled = $request->getQueryParams()['installed'] ?? ''; $isFavorite = $request->getQueryParams()['favorite'] ?? ''; $sort = $request->getQueryParams()['sort'] ?? 'title_asc'; $perPage = 20; // Items per page // Prepare filters $filters = [ 'search' => $search, 'platform' => $platform, 'genre' => $genre, 'is_installed' => $isInstalled, 'is_favorite' => $isFavorite, 'sort' => $sort ]; // Get paginated and filtered games $result = $gameModel->getGroupedGamesWithPagination( $this->pdo, $page, $perPage, $search, $genre ? [$genre] : [], $platform ? [$platform] : [] ); // Get available platforms and genres for filters $platforms = $gameModel->getPlatforms(); $genres = $gameModel->getGenres(); // Calculate pagination data $totalGames = $gameModel->getTotalCount( $this->pdo, $search, $genre ? [$genre] : [], $platform ? [$platform] : [] ); $totalPages = ceil($totalGames / $perPage); return $this->render($response, 'admin/games/index.twig', [ 'title' => 'Manage Games', 'games' => $result, 'platforms' => $platforms, 'genres' => $genres, 'filters' => $filters, 'pagination' => [ 'current' => $page, 'total' => $totalPages, 'per_page' => $perPage, 'total_items' => $totalGames ] ]); } public function editGame(Request $request, Response $response, $args) { $id = $args['id'] ?? null; $gameModel = new \App\Models\Game($this->pdo); if ($request->getMethod() === 'POST') { $data = $request->getParsedBody(); if ($id) { $gameModel->update($id, $data); } else { $id = $gameModel->create($data); } return $response->withHeader('Location', '/admin/games/' . $id . '/edit') ->withStatus(302); } $game = $id ? $gameModel->find($id) : null; return $this->render($response, 'admin/games/edit.twig', [ 'title' => $id ? 'Edit Game' : 'Add New Game', 'game' => $game ]); } public function deleteGame(Request $request, Response $response, $args) { $id = $args['id']; $gameModel = new \App\Models\Game($this->pdo); $gameModel->delete($id); return $response->withHeader('Location', '/admin/games') ->withStatus(302); } // TV Shows Management public function shows(Request $request, Response $response, $args) { $showModel = new \App\Models\TvShow($this->pdo); // Get query parameters with defaults $page = max(1, (int)($request->getQueryParams()['page'] ?? 1)); $search = trim($request->getQueryParams()['search'] ?? ''); $genre = trim($request->getQueryParams()['genre'] ?? ''); $network = trim($request->getQueryParams()['network'] ?? ''); $status = trim($request->getQueryParams()['status'] ?? ''); $sort = trim($request->getQueryParams()['sort'] ?? 'name_asc'); $perPage = 20; // Prepare filters for the view $filters = [ 'search' => $search, 'genre' => $genre, 'network' => $network, 'status' => $status, 'sort' => $sort ]; // Get paginated and filtered shows $shows = $showModel->getPaginated( $this->pdo, $page, $perPage, $search, $genre ? [$genre] : [], $network ? [$network] : [], $status ? [$status] : [], $sort ); // Get available filters $genres = $showModel->getGenres($this->pdo); //$networks = $showModel->getNetworks($this->pdo); $statuses = ['Returning Series', 'Ended', 'Canceled', 'In Production']; // Calculate pagination data $totalShows = $showModel->getTotalCount( $this->pdo, $search, $genre ? [$genre] : [], $network ? [$network] : [], $status ? [$status] : [] ); $totalPages = max(1, ceil($totalShows / $perPage)); $currentPage = min($page, $totalPages); return $this->render($response, 'admin/shows/index.twig', [ 'title' => 'Manage TV Shows', 'shows' => $shows, 'genres' => $genres, //'networks' => $networks, 'statuses' => $statuses, 'filters' => $filters, 'pagination' => [ 'current' => $currentPage, 'total' => $totalPages, 'per_page' => $perPage, 'total_items' => $totalShows, 'from' => (($currentPage - 1) * $perPage) + 1, 'to' => min($currentPage * $perPage, $totalShows) ] ]); } public function editShow(Request $request, Response $response, $args) { $id = $args['id'] ?? null; $showModel = new \App\Models\TvShow($this->pdo); if ($request->getMethod() === 'POST') { $data = $request->getParsedBody(); if ($id) { $showModel->update($id, $data); } else { $id = $showModel->create($data); } return $response->withHeader('Location', '/admin/shows/' . $id . '/edit') ->withStatus(302); } $show = $id ? $showModel->find($id) : null; return $this->render($response, 'admin/shows/edit.twig', [ 'title' => $id ? 'Edit TV Show' : 'Add New TV Show', 'show' => $show ]); } public function deleteShow(Request $request, Response $response, $args) { $id = $args['id']; $showModel = new \App\Models\TvShow($this->pdo); $showModel->delete($id); return $response->withHeader('Location', '/admin/shows') ->withStatus(302); } /** * Display a listing of adult videos with pagination and filters */ public function adultVideos(Request $request, Response $response, $args) { $adultVideoModel = new \App\Models\AdultVideo($this->pdo); // Get query parameters with defaults $page = max(1, (int)($request->getQueryParams()['page'] ?? 1)); $search = trim($request->getQueryParams()['search'] ?? ''); $genre = trim($request->getQueryParams()['genre'] ?? ''); $director = trim($request->getQueryParams()['director'] ?? ''); $sort = trim($request->getQueryParams()['sort'] ?? 'newest'); $perPage = 20; // Prepare filters for the view $filters = [ 'search' => $search, 'genre' => $genre, 'director' => $director, 'sort' => $sort ]; // Get available filters $genres = $adultVideoModel::getAvailableGenres($this->pdo); $directors = $adultVideoModel::getAvailableDirectors($this->pdo); // Get paginated and filtered adult videos $videos = $adultVideoModel::getAllWithPagination( $this->pdo, $page, $perPage, $search, $genre ? [$genre] : [], $director ? [$director] : [] ); // Get total count for pagination $totalVideos = $adultVideoModel::getTotalCount( $this->pdo, $search, $genre ? [$genre] : [], $director ? [$director] : [] ); $totalPages = max(1, ceil($totalVideos / $perPage)); $currentPage = min($page, $totalPages); return $this->render($response, 'admin/adult/index.twig', [ 'title' => 'Manage Adult Videos', 'videos' => $videos, 'genres' => $genres, 'directors' => $directors, 'filters' => $filters, 'pagination' => [ 'current' => $currentPage, 'total' => $totalPages, 'per_page' => $perPage, 'total_items' => $totalVideos, 'from' => (($currentPage - 1) * $perPage) + 1, 'to' => min($currentPage * $perPage, $totalVideos) ] ]); } public function editAdultVideo(Request $request, Response $response, $args) { $id = $args['id'] ?? null; $adultModel = new \App\Models\AdultVideo($this->pdo); if ($request->getMethod() === 'POST') { $data = $request->getParsedBody(); if ($id) { $adultModel->update($id, $data); } else { $id = $adultModel->create($data); } return $response->withHeader('Location', '/admin/adult/' . $id . '/edit') ->withStatus(302); } $video = $id ? $adultModel->find($id) : null; return $this->render($response, 'admin/adult/edit.twig', [ 'title' => $id ? 'Edit Adult Video' : 'Add New Adult Video', 'video' => $video ]); } public function deleteAdultVideo(Request $request, Response $response, $args) { $id = $args['id']; $adultModel = new \App\Models\AdultVideo($this->pdo); $adultModel->delete($id); return $response->withHeader('Location', '/admin/adult') ->withStatus(302); } public function syncSource(Request $request, Response $response, $args) { $sourceId = $args['id']; $syncType = $request->getQueryParams()['type'] ?? 'full'; $sourceModel = new Source($this->pdo); $source = $sourceModel->find($sourceId); if (!$source) { return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); } // Validate sync type based on source type if ($source['name'] === 'jellyfin') { $validSyncTypes = ['full', 'incremental', 'all', 'movies', 'tvshows', 'cleanup']; if (!in_array($syncType, $validSyncTypes)) { return $this->json($response, [ 'success' => false, 'message' => 'Invalid sync type for Jellyfin source. Valid types: ' . implode(', ', $validSyncTypes) ], 400); } } else { // For other sources, only allow full/incremental $validSyncTypes = ['full', 'incremental']; if (!in_array($syncType, $validSyncTypes)) { return $this->json($response, [ 'success' => false, 'message' => 'Invalid sync type. Valid types: ' . implode(', ', $validSyncTypes) ], 400); } } // Start sync in background (simplified - in production you'd use queues) $syncLogId = $this->startSync($source, $syncType); return $this->json($response, [ 'success' => true, 'sync_log_id' => $syncLogId, 'message' => 'Sync started successfully' ]); } public function syncStatus(Request $request, Response $response, $args) { $syncLogId = $args['id']; $syncLogModel = new SyncLog($this->pdo); $syncLog = $syncLogModel->find($syncLogId); if (!$syncLog) { return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); } return $this->json($response, [ 'id' => $syncLog['id'], 'status' => $syncLog['status'], 'sync_type' => $syncLog['sync_type'], 'total_items' => $syncLog['total_items'] ?? 0, 'processed_items' => $syncLog['processed_items'], 'new_items' => $syncLog['new_items'], 'updated_items' => $syncLog['updated_items'], 'deleted_items' => $syncLog['deleted_items'], 'started_at' => $syncLog['started_at'], 'completed_at' => $syncLog['completed_at'], 'message' => $syncLog['message'], 'errors' => $syncLog['errors'] ? json_decode($syncLog['errors'], true) : [], 'progress_percentage' => $this->calculateProgressPercentage($syncLog) ]); } private function calculateProgressPercentage(array $syncLog): float { $total = $syncLog['total_items'] ?? 0; if ($total <= 0) return 0; $processed = $syncLog['processed_items'] ?? 0; return min(100, round(($processed / $total) * 100, 2)); } public function settings(Request $request, Response $response, $args) { return $this->render($response, 'admin/settings.twig', [ 'title' => 'Admin Settings', 'current_route' => 'settings' ]); } public function sources(Request $request, Response $response, $args) { $sourceModel = new Source($this->pdo); $sources = $sourceModel->findAll(); return $this->render($response, 'admin/sources.twig', [ 'title' => 'Source Management', 'sources' => $sources, 'current_route' => 'sources' ]); } private function startSync(array $source, string $syncType): int { // Create sync log entry first $syncLogId = $this->createSyncLog($source, $syncType); // Start sync in background process $this->startBackgroundSync($source['id'], $syncType, $syncLogId); return $syncLogId; } private function createSyncLog(array $source, string $syncType): int { $data = [ 'source_id' => $source['id'], 'sync_type' => $syncType, 'status' => 'started', '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 startBackgroundSync(int $sourceId, string $syncType, int $syncLogId): void { $scriptPath = __DIR__ . '/../../sync-runner.php'; $command = sprintf( 'php %s %d %s %d > /dev/null 2>&1 &', escapeshellarg($scriptPath), $sourceId, escapeshellarg($syncType), $syncLogId ); // Execute the command in background if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { // Windows pclose(popen('start /B ' . $command, 'r')); } else { // Unix-like systems exec($command); } // */ // Update sync log to indicate it's running (this will be updated again by the script) $syncLogModel = new SyncLog($this->pdo); $syncLogModel->update($syncLogId, [ 'status' => 'running', 'message' => 'Sync process starting in background' ]); } /** * Get actors for a specific adult video */ public function getAdultVideoActors(Request $request, Response $response, $args) { $adultVideo = new \App\Models\AdultVideo($this->pdo); $video = $adultVideo->find($args['id']); if (!$video) { $response->getBody()->write(json_encode(['error' => 'Video not found'])); return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); } $actors = $adultVideo->actors($args['id']); $response->getBody()->write(json_encode(['data' => $actors])); return $response->withHeader('Content-Type', 'application/json'); } /** * Add an actor to an adult video */ public function addActorToAdultVideo(Request $request, Response $response, $args) { $contentType = $request->getHeaderLine('Content-Type'); if (strstr($contentType, 'application/json')) { $data = json_decode((string)$request->getBody(), true); } else { $data = $request->getParsedBody(); } $actorId = $data['actor_id'] ?? null; if (!$actorId) { $response->getBody()->write(json_encode(['error' => 'Actor ID is required'])); return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); } $adultVideo = new \App\Models\AdultVideo($this->pdo); $video = $adultVideo->find($args['id']); if (!$video) { $response->getBody()->write(json_encode(['error' => 'Video not found'])); return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); } $success = $adultVideo->addActor($actorId); if ($success) { $adultVideo->updateCastField(); $response->getBody()->write(json_encode([ 'success' => true, 'message' => 'Actor added successfully' ])); return $response->withHeader('Content-Type', 'application/json'); } $response->getBody()->write(json_encode(['error' => 'Failed to add actor'])); return $response->withStatus(500)->withHeader('Content-Type', 'application/json'); } /** * Remove an actor from an adult video */ public function removeActorFromAdultVideo(Request $request, Response $response, $args) { $actorId = $args['actorId'] ?? null; if (!$actorId) { $response->getBody()->write(json_encode(['error' => 'Actor ID is required'])); return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); } $adultVideo = new \App\Models\AdultVideo($this->pdo); $video = $adultVideo->find($args['id']); if (!$video) { $response->getBody()->write(json_encode(['error' => 'Video not found'])); return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); } $success = $adultVideo->removeActor($actorId); if ($success) { $adultVideo->updateCastField(); $response->getBody()->write(json_encode([ 'success' => true, 'message' => 'Actor removed successfully' ])); return $response->withHeader('Content-Type', 'application/json'); } $response->getBody()->write(json_encode(['error' => 'Failed to remove actor'])); return $response->withStatus(500)->withHeader('Content-Type', 'application/json'); } /** * Search actors by name */ public function searchActors(Request $request, Response $response, $args) { $query = $request->getQueryParams()['q'] ?? ''; if (empty($query)) { $response->getBody()->write(json_encode(['data' => []])); return $response->withHeader('Content-Type', 'application/json'); } $adultVideo = new \App\Models\AdultVideo($this->pdo); $actors = $adultVideo->searchActors($this->pdo, $query); $response->getBody()->write(json_encode(['data' => $actors])); return $response->withHeader('Content-Type', 'application/json'); } }