From 0f0fb3b41006eeea6117a41e63d1a003f15cf556 Mon Sep 17 00:00:00 2001 From: Lars Behrends Date: Thu, 6 Nov 2025 13:39:46 +0100 Subject: [PATCH] =?UTF-8?q?searcg=20revamp=20=F0=9F=98=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Controllers/AdminController.php | 390 +++++++++++++++++- app/Controllers/SearchController.php | 182 +++++++-- app/Models/Actor.php | 128 ++++++ resources/views/admin/actors/index.twig | 511 ++++++++++++++++++++++++ resources/views/admin/layout.twig | 6 + resources/views/layouts/app.twig | 59 ++- resources/views/search/index.twig | 386 +++++++++++++----- routes/web.php | 8 +- 8 files changed, 1526 insertions(+), 144 deletions(-) create mode 100644 resources/views/admin/actors/index.twig diff --git a/app/Controllers/AdminController.php b/app/Controllers/AdminController.php index 8eb272a..8728338 100644 --- a/app/Controllers/AdminController.php +++ b/app/Controllers/AdminController.php @@ -723,16 +723,400 @@ class AdminController extends AdminBaseController 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'); } + + /** + * Display actors management page with duplicate detection + */ + public function actors(Request $request, Response $response, $args) + { + $actorModel = new \App\Models\Actor($this->pdo); + + // Get query parameters + $page = max(1, (int)($request->getQueryParams()['page'] ?? 1)); + $search = trim($request->getQueryParams()['search'] ?? ''); + $showDuplicates = $request->getQueryParams()['duplicates'] ?? false; + $sort = trim($request->getQueryParams()['sort'] ?? 'name_asc'); + $perPage = 20; + + $filters = [ + 'search' => $search, + 'duplicates' => $showDuplicates, + 'sort' => $sort + ]; + + if ($showDuplicates) { + // Get duplicate actors + $actors = $this->getDuplicateActors($page, $perPage); + $totalActors = $this->getDuplicateActorsCount(); + } else { + // Get all actors with pagination + $actors = $actorModel->getPaginated($this->pdo, $page, $perPage, $search, $sort); + $totalActors = $actorModel->getTotalCount($this->pdo, $search); + } + + $totalPages = max(1, ceil($totalActors / $perPage)); + $currentPage = min($page, $totalPages); + + return $this->render($response, 'admin/actors/index.twig', [ + 'title' => 'Manage Actors', + 'actors' => $actors, + 'filters' => $filters, + 'pagination' => [ + 'current' => $currentPage, + 'total' => $totalPages, + 'per_page' => $perPage, + 'total_items' => $totalActors, + 'from' => (($currentPage - 1) * $perPage) + 1, + 'to' => min($currentPage * $perPage, $totalActors) + ] + ]); + } + + /** + * Get duplicate actors grouped by name + */ + private function getDuplicateActors(int $page = 1, int $perPage = 20): array + { + $offset = ($page - 1) * $perPage; + + $stmt = $this->pdo->prepare(" + SELECT + LOWER(TRIM(name)) as normalized_name, + COUNT(*) as duplicate_count, + GROUP_CONCAT(id ORDER BY id) as actor_ids, + GROUP_CONCAT(name ORDER BY id) as actor_names, + GROUP_CONCAT(COALESCE(thumbnail_path, '') ORDER BY id) as thumbnails, + GROUP_CONCAT(COALESCE(metadata, '{}') ORDER BY id) as metadata_list + FROM actors + GROUP BY LOWER(TRIM(name)) + HAVING COUNT(*) > 1 + ORDER BY duplicate_count DESC, normalized_name ASC + LIMIT ? OFFSET ? + "); + $stmt->execute([$perPage, $offset]); + + $duplicateGroups = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + $result = []; + foreach ($duplicateGroups as $group) { + $actorIds = explode(',', $group['actor_ids']); + $actorNames = explode(',', $group['actor_names']); + $thumbnails = explode(',', $group['thumbnails']); + $metadataList = explode(',', $group['metadata_list']); + + $actors = []; + foreach ($actorIds as $index => $actorId) { + $actors[] = [ + 'id' => (int)$actorId, + 'name' => $actorNames[$index], + 'thumbnail_path' => $thumbnails[$index] ?: null, + 'metadata' => json_decode($metadataList[$index] ?: '{}', true), + 'stats' => $this->getActorStats((int)$actorId) + ]; + } + + $result[] = [ + 'normalized_name' => $group['normalized_name'], + 'duplicate_count' => (int)$group['duplicate_count'], + 'actors' => $actors + ]; + } + + return $result; + } + + /** + * Get total count of duplicate actor groups + */ + private function getDuplicateActorsCount(): int + { + $stmt = $this->pdo->query(" + SELECT COUNT(*) as count + FROM ( + SELECT LOWER(TRIM(name)) as normalized_name + FROM actors + GROUP BY LOWER(TRIM(name)) + HAVING COUNT(*) > 1 + ) as duplicates + "); + return (int)$stmt->fetch(\PDO::FETCH_ASSOC)['count']; + } + + /** + * Get actor statistics + */ + private function getActorStats(int $actorId): array + { + $stmt = $this->pdo->prepare(" + SELECT + COUNT(DISTINCT am.movie_id) as movie_count, + COUNT(DISTINCT ats.tv_show_id) as tv_show_count, + COUNT(DISTINCT aav.adult_video_id) as adult_video_count, + COUNT(DISTINCT am.movie_id) + COUNT(DISTINCT ats.tv_show_id) + COUNT(DISTINCT aav.adult_video_id) as total_media_count + FROM actors a + LEFT JOIN actor_movie am ON a.id = am.actor_id + LEFT JOIN actor_tv_show ats ON a.id = ats.actor_id + LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id + WHERE a.id = ? + "); + $stmt->execute([$actorId]); + return $stmt->fetch(\PDO::FETCH_ASSOC); + } + + /** + * Merge duplicate actors + */ + public function mergeActors(Request $request, Response $response, $args) + { + $data = $request->getParsedBody(); + $masterActorId = (int)($data['master_actor_id'] ?? 0); + $duplicateActorIds = array_map('intval', $data['duplicate_actor_ids'] ?? []); + + if (!$masterActorId || empty($duplicateActorIds)) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Master actor ID and duplicate actor IDs are required' + ], 400); + } + + // Verify master actor exists + $masterActor = (new \App\Models\Actor($this->pdo))->find($masterActorId); + if (!$masterActor) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Master actor not found' + ], 404); + } + + $this->pdo->beginTransaction(); + + try { + $mergedCount = 0; + $errors = []; + + foreach ($duplicateActorIds as $duplicateId) { + if ($duplicateId === $masterActorId) { + continue; // Skip if trying to merge master with itself + } + + // Verify duplicate actor exists + $duplicateActor = (new \App\Models\Actor($this->pdo))->find($duplicateId); + if (!$duplicateActor) { + $errors[] = "Actor ID {$duplicateId} not found"; + continue; + } + + // Move relationships from duplicate to master + $this->moveActorRelationships($duplicateId, $masterActorId); + + // Delete the duplicate actor + $stmt = $this->pdo->prepare("DELETE FROM actors WHERE id = ?"); + $stmt->execute([$duplicateId]); + + $mergedCount++; + } + + $this->pdo->commit(); + + return $this->json($response, [ + 'success' => true, + 'message' => "Successfully merged {$mergedCount} duplicate actors", + 'merged_count' => $mergedCount, + 'errors' => $errors + ]); + + } catch (\Exception $e) { + $this->pdo->rollBack(); + return $this->json($response, [ + 'success' => false, + 'message' => 'Failed to merge actors: ' . $e->getMessage() + ], 500); + } + } + + /** + * Move all relationships from one actor to another + */ + private function moveActorRelationships(int $fromActorId, int $toActorId): void + { + // Move movie relationships + $stmt = $this->pdo->prepare(" + UPDATE actor_movie + SET actor_id = ? + WHERE actor_id = ? AND movie_id NOT IN ( + SELECT movie_id FROM actor_movie WHERE actor_id = ? + ) + "); + $stmt->execute([$toActorId, $fromActorId, $toActorId]); + + // Remove duplicate movie relationships that may have been created + $stmt = $this->pdo->prepare(" + DELETE FROM actor_movie + WHERE actor_id = ? AND movie_id IN ( + SELECT movie_id FROM ( + SELECT movie_id FROM actor_movie WHERE actor_id = ? GROUP BY movie_id HAVING COUNT(*) > 1 + ) as duplicates + ) + "); + $stmt->execute([$fromActorId, $fromActorId]); + + // Move TV show relationships + $stmt = $this->pdo->prepare(" + UPDATE actor_tv_show + SET actor_id = ? + WHERE actor_id = ? AND tv_show_id NOT IN ( + SELECT tv_show_id FROM actor_tv_show WHERE actor_id = ? + ) + "); + $stmt->execute([$toActorId, $fromActorId, $toActorId]); + + // Remove duplicate TV show relationships + $stmt = $this->pdo->prepare(" + DELETE FROM actor_tv_show + WHERE actor_id = ? AND tv_show_id IN ( + SELECT tv_show_id FROM ( + SELECT tv_show_id FROM actor_tv_show WHERE actor_id = ? GROUP BY tv_show_id HAVING COUNT(*) > 1 + ) as duplicates + ) + "); + $stmt->execute([$fromActorId, $fromActorId]); + + // Move adult video relationships + $stmt = $this->pdo->prepare(" + UPDATE actor_adult_video + SET actor_id = ? + WHERE actor_id = ? AND adult_video_id NOT IN ( + SELECT adult_video_id FROM actor_adult_video WHERE actor_id = ? + ) + "); + $stmt->execute([$toActorId, $fromActorId, $toActorId]); + + // Remove duplicate adult video relationships + $stmt = $this->pdo->prepare(" + DELETE FROM actor_adult_video + WHERE actor_id = ? AND adult_video_id IN ( + SELECT adult_video_id FROM ( + SELECT adult_video_id FROM actor_adult_video WHERE actor_id = ? GROUP BY adult_video_id HAVING COUNT(*) > 1 + ) as duplicates + ) + "); + $stmt->execute([$fromActorId, $fromActorId]); + } + + /** + * Auto-merge duplicate actors (chooses master based on media count and thumbnail) + */ + public function autoMergeActors(Request $request, Response $response, $args) + { + $data = $request->getParsedBody(); + $actorGroupIds = $data['actor_group_ids'] ?? []; + + if (empty($actorGroupIds)) { + return $this->json($response, [ + 'success' => false, + 'message' => 'No actor groups specified for auto-merge' + ], 400); + } + + $this->pdo->beginTransaction(); + + try { + $totalMerged = 0; + $groupsProcessed = 0; + + foreach ($actorGroupIds as $groupId) { + $actors = $this->getActorsByNormalizedName($groupId); + if (count($actors) <= 1) { + continue; + } + + // Choose master actor (prefer one with thumbnail, then most media associations) + $masterActor = $this->chooseMasterActor($actors); + $duplicateIds = array_filter(array_column($actors, 'id'), fn($id) => $id !== $masterActor['id']); + + // Merge duplicates into master + foreach ($duplicateIds as $duplicateId) { + $this->moveActorRelationships($duplicateId, $masterActor['id']); + + // Delete duplicate + $stmt = $this->pdo->prepare("DELETE FROM actors WHERE id = ?"); + $stmt->execute([$duplicateId]); + } + + $totalMerged += count($duplicateIds); + $groupsProcessed++; + } + + $this->pdo->commit(); + + return $this->json($response, [ + 'success' => true, + 'message' => "Auto-merged {$totalMerged} actors across {$groupsProcessed} groups", + 'merged_count' => $totalMerged, + 'groups_processed' => $groupsProcessed + ]); + + } catch (\Exception $e) { + $this->pdo->rollBack(); + return $this->json($response, [ + 'success' => false, + 'message' => 'Failed to auto-merge actors: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get actors by normalized name + */ + private function getActorsByNormalizedName(string $normalizedName): array + { + $stmt = $this->pdo->prepare(" + SELECT id, name, thumbnail_path, metadata + FROM actors + WHERE LOWER(TRIM(name)) = ? + ORDER BY id + "); + $stmt->execute([$normalizedName]); + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + } + + /** + * Choose the best master actor from a group + */ + private function chooseMasterActor(array $actors): array + { + // First, prefer actors with thumbnails + $withThumbnails = array_filter($actors, fn($actor) => !empty($actor['thumbnail_path'])); + if (!empty($withThumbnails)) { + $actors = $withThumbnails; + } + + // Then prefer the one with most media associations + $maxMediaCount = 0; + $masterActor = $actors[0]; + + foreach ($actors as $actor) { + $stats = $this->getActorStats($actor['id']); + $mediaCount = $stats['total_media_count']; + + if ($mediaCount > $maxMediaCount) { + $maxMediaCount = $mediaCount; + $masterActor = $actor; + } + } + + return $masterActor; + } } diff --git a/app/Controllers/SearchController.php b/app/Controllers/SearchController.php index 463fd79..f7d3383 100644 --- a/app/Controllers/SearchController.php +++ b/app/Controllers/SearchController.php @@ -20,44 +20,162 @@ class SearchController extends Controller { $queryParams = $request->getQueryParams(); $search = trim($queryParams['q'] ?? ''); + $type = $queryParams['type'] ?? 'all'; + $sort = $queryParams['sort'] ?? 'relevance'; + $page = (int)($queryParams['page'] ?? 1); + $perPage = 30; - if (empty($search)) { - return $this->view->render($response, 'search/index.twig', [ - 'title' => 'Search', - 'search' => $search, - 'results' => [] - ]); + $results = []; + $totalResults = 0; + + if (!empty($search)) { + $searchTerm = $this->pdo->quote("%$search%"); + $offset = ($page - 1) * $perPage; + + // Build sort clause + $sortClause = match($sort) { + 'title' => 'title ASC', + 'date' => 'created_at DESC', + 'rating' => 'rating DESC', + default => 'title ASC' + }; + + // Search movies + if ($type === 'all' || $type === 'movies') { + $movieStmt = $this->pdo->query(" + SELECT m.*, s.display_name as source_name, 'movie' as type, + m.created_at as added_date + FROM movies m + JOIN sources s ON m.source_id = s.id + WHERE (m.title LIKE $searchTerm OR m.overview LIKE $searchTerm) + ORDER BY $sortClause + LIMIT $perPage OFFSET $offset + "); + $results['movies'] = $movieStmt->fetchAll(\PDO::FETCH_ASSOC); + + // Get total count for movies + $countStmt = $this->pdo->query(" + SELECT COUNT(*) as count FROM movies m + WHERE (m.title LIKE $searchTerm OR m.overview LIKE $searchTerm) + "); + $totalResults += $countStmt->fetch(\PDO::FETCH_ASSOC)['count']; + } + + // Search games + if ($type === 'all' || $type === 'games') { + $gameStmt = $this->pdo->query(" + SELECT g.*, 'game' as type, g.created_at as added_date + FROM games g + WHERE (g.title LIKE $searchTerm OR g.description LIKE $searchTerm) + ORDER BY $sortClause + LIMIT $perPage OFFSET $offset + "); + $results['games'] = $gameStmt->fetchAll(\PDO::FETCH_ASSOC); + + // Get total count for games + $countStmt = $this->pdo->query(" + SELECT COUNT(*) as count FROM games g + WHERE (g.title LIKE $searchTerm OR g.description LIKE $searchTerm) + "); + $totalResults += $countStmt->fetch(\PDO::FETCH_ASSOC)['count']; + } + + // Search TV shows + if ($type === 'all' || $type === 'tvshows') { + $tvStmt = $this->pdo->query(" + SELECT t.*, s.display_name as source_name, 'tvshow' as type, + t.created_at as added_date + FROM tv_shows t + JOIN sources s ON t.source_id = s.id + WHERE (t.title LIKE $searchTerm OR t.overview LIKE $searchTerm) + ORDER BY $sortClause + LIMIT $perPage OFFSET $offset + "); + $results['tvshows'] = $tvStmt->fetchAll(\PDO::FETCH_ASSOC); + + // Get total count for TV shows + $countStmt = $this->pdo->query(" + SELECT COUNT(*) as count FROM tv_shows t + WHERE (t.title LIKE $searchTerm OR t.overview LIKE $searchTerm) + "); + $totalResults += $countStmt->fetch(\PDO::FETCH_ASSOC)['count']; + } + + // Search music + if ($type === 'all' || $type === 'music') { + $musicStmt = $this->pdo->query(" + SELECT m.*, s.display_name as source_name, 'music' as type, + m.created_at as added_date + FROM music_albums m + JOIN sources s ON m.source_id = s.id + WHERE (m.title LIKE $searchTerm OR m.artist_name LIKE $searchTerm) + ORDER BY $sortClause + LIMIT $perPage OFFSET $offset + "); + $results['music'] = $musicStmt->fetchAll(\PDO::FETCH_ASSOC); + + // Get total count for music + $countStmt = $this->pdo->query(" + SELECT COUNT(*) as count FROM music_albums m + WHERE (m.title LIKE $searchTerm OR m.artist_name LIKE $searchTerm) + "); + $totalResults += $countStmt->fetch(\PDO::FETCH_ASSOC)['count']; + } + + // Search adult videos + if ($type === 'all' || $type === 'adult') { + $adultStmt = $this->pdo->query(" + SELECT a.*, s.display_name as source_name, 'adult' as type, + a.created_at as added_date + FROM adult_videos a + JOIN sources s ON a.source_id = s.id + WHERE (a.title LIKE $searchTerm) + ORDER BY $sortClause + LIMIT $perPage OFFSET $offset + "); + $results['adult'] = $adultStmt->fetchAll(\PDO::FETCH_ASSOC); + + // Get total count for adult videos + $countStmt = $this->pdo->query(" + SELECT COUNT(*) as count FROM adult_videos a + WHERE (a.title LIKE $searchTerm) + "); + $totalResults += $countStmt->fetch(\PDO::FETCH_ASSOC)['count']; + } + + // Search actors + if ($type === 'all' || $type === 'actors') { + $actorStmt = $this->pdo->query(" + SELECT a.*, 'actor' as type, a.created_at as added_date, a.thumbnail_path as thumbnail, a.name as title + FROM actors a + WHERE (a.name LIKE $searchTerm) + + LIMIT $perPage OFFSET $offset + "); + $results['actors'] = $actorStmt->fetchAll(\PDO::FETCH_ASSOC); + + // Get total count for actors + $countStmt = $this->pdo->query(" + SELECT COUNT(*) as count FROM actors a + WHERE (a.name LIKE $searchTerm) + "); + $totalResults += $countStmt->fetch(\PDO::FETCH_ASSOC)['count']; + } } - // Search across different media types - $results = []; + // Calculate pagination + $totalPages = ceil($totalResults / $perPage); - // Search movies (including adult videos) - $searchTerm = $this->pdo->quote("%$search%"); - $movieStmt = $this->pdo->query(" - SELECT m.*, s.display_name as source_name, 'movie' as type - FROM movies m - JOIN sources s ON m.source_id = s.id - WHERE (m.title LIKE $searchTerm OR m.overview LIKE $searchTerm) - ORDER BY m.title - LIMIT 20 - "); - $results['movies'] = $movieStmt->fetchAll(\PDO::FETCH_ASSOC); - - // Search games - $gameStmt = $this->pdo->query(" - SELECT g.*, 'game' as type - FROM games g - WHERE (g.title LIKE $searchTerm OR g.description LIKE $searchTerm) - ORDER BY g.title - LIMIT 20 - "); - $results['games'] = $gameStmt->fetchAll(\PDO::FETCH_ASSOC); - return $this->view->render($response, 'search/index.twig', [ - 'title' => 'Search Results', + 'title' => !empty($search) ? 'Search Results' : 'Search', 'search' => $search, - 'results' => $results + 'type' => $type, + 'sort' => $sort, + 'page' => $page, + 'totalPages' => $totalPages, + 'totalResults' => $totalResults, + 'results' => $results, + 'hasResults' => !empty(array_filter($results)) ]); } } diff --git a/app/Models/Actor.php b/app/Models/Actor.php index b8cea2d..df7fc75 100644 --- a/app/Models/Actor.php +++ b/app/Models/Actor.php @@ -176,4 +176,132 @@ class Actor extends Model 'adult_video_id' => $adultVideoId ]); } + + /** + * Get paginated actors with optional search and sorting + */ + public function getPaginated(PDO $pdo, int $page = 1, int $perPage = 20, string $search = '', string $sort = 'name_asc'): array + { + $offset = ($page - 1) * $perPage; + + // Build WHERE clause for search + $whereClause = ''; + $params = []; + + if (!empty($search)) { + $whereClause = 'WHERE name LIKE :search'; + $params['search'] = '%' . $search . '%'; + } + + // Build ORDER BY clause + $orderBy = match ($sort) { + 'name_desc' => 'name DESC', + 'media_desc' => '(SELECT COUNT(*) FROM actor_movie WHERE actor_id = actors.id) + (SELECT COUNT(*) FROM actor_tv_show WHERE actor_id = actors.id) + (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = actors.id) DESC', + 'media_asc' => '(SELECT COUNT(*) FROM actor_movie WHERE actor_id = actors.id) + (SELECT COUNT(*) FROM actor_tv_show WHERE actor_id = actors.id) + (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = actors.id) ASC', + default => 'name ASC' + }; + + // Get actors with their media counts + $stmt = $pdo->prepare(" + SELECT + a.*, + (SELECT COUNT(*) FROM actor_movie WHERE actor_id = a.id) as movie_count, + (SELECT COUNT(*) FROM actor_tv_show WHERE actor_id = a.id) as tv_show_count, + (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = a.id) as adult_video_count + FROM actors a + {$whereClause} + ORDER BY {$orderBy} + LIMIT :limit OFFSET :offset + "); + + $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT); + $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); + + foreach ($params as $key => $value) { + $stmt->bindValue(':' . $key, $value); + } + + $stmt->execute(); + $actors = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Add relationships data for each actor + foreach ($actors as &$actor) { + $actor['movies'] = $this->getActorMovies($actor['id']); + $actor['tvShows'] = $this->getActorTvShows($actor['id']); + $actor['adultVideos'] = $this->getActorAdultVideos($actor['id']); + } + + return $actors; + } + + /** + * Get total count of actors with optional search + */ + public function getTotalCount(PDO $pdo, string $search = ''): int + { + $whereClause = ''; + $params = []; + + if (!empty($search)) { + $whereClause = 'WHERE name LIKE :search'; + $params['search'] = '%' . $search . '%'; + } + + $stmt = $pdo->prepare("SELECT COUNT(*) as count FROM actors {$whereClause}"); + + foreach ($params as $key => $value) { + $stmt->bindValue(':' . $key, $value); + } + + $stmt->execute(); + return (int)$stmt->fetch(PDO::FETCH_ASSOC)['count']; + } + + /** + * Get movies for a specific actor (helper method) + */ + private function getActorMovies(int $actorId): array + { + $stmt = $this->pdo->prepare(" + SELECT m.id, m.title + FROM movies m + JOIN actor_movie am ON m.id = am.movie_id + WHERE am.actor_id = ? + ORDER BY m.title + "); + $stmt->execute([$actorId]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Get TV shows for a specific actor (helper method) + */ + private function getActorTvShows(int $actorId): array + { + $stmt = $this->pdo->prepare(" + SELECT ts.id, ts.title + FROM tv_shows ts + JOIN actor_tv_show ats ON ts.id = ats.tv_show_id + WHERE ats.actor_id = ? + ORDER BY ts.title + "); + $stmt->execute([$actorId]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Get adult videos for a specific actor (helper method) + */ + private function getActorAdultVideos(int $actorId): array + { + $stmt = $this->pdo->prepare(" + SELECT av.id, av.title + FROM adult_videos av + JOIN actor_adult_video aav ON av.id = aav.adult_video_id + WHERE aav.actor_id = ? + ORDER BY av.title + "); + $stmt->execute([$actorId]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } } diff --git a/resources/views/admin/actors/index.twig b/resources/views/admin/actors/index.twig new file mode 100644 index 0000000..cf8caec --- /dev/null +++ b/resources/views/admin/actors/index.twig @@ -0,0 +1,511 @@ +{% extends 'admin/layout.twig' %} + +{% block title %}Manage Actors - Admin Panel - MediaLib{% endblock %} + +{% block content %} +
+
+

Manage Actors

+

View and manage actors, find and merge duplicates

+
+ +
+ +
+
+ {% if flash.success %} +
+ {{ flash.success }} +
+ {% endif %} + + {% if filters.duplicates %} + +
+
+
Duplicate Actor Groups
+ Actors with similar names that may need to be merged +
+ +
+ +
+ {% for group in actors %} +
+
+
+
+
{{ group.normalized_name|title }}
+ {{ group.duplicate_count }} duplicates +
+
+
+
+ {% for actor in group.actors %} +
+
+
+ +
+ {% if actor.thumbnail_path %} + {{ actor.name }} + {% else %} +
+ +
+ {% endif %} +
+
{{ actor.name }}
+ + {{ actor.stats.movie_count }} movies, + {{ actor.stats.tv_show_count }} TV shows, + {{ actor.stats.adult_video_count }} adult videos + ({{ actor.stats.total_media_count }} total) + +
+
+ ID: {{ actor.id }} + {% if actor.thumbnail_path %} + + {% endif %} +
+
+
+ {% endfor %} +
+
+ + +
+
+
+
+ {% else %} +
+
+ +
No Duplicate Actors Found
+

All actors in your database appear to be unique.

+
+
+ {% endfor %} +
+ + + {% if pagination.total > 1 %} + + {% endif %} + + {% else %} + +
+
+
+
+ + +
+
+
+ +
+
+
+ + + + +
+
+
+
+ + +
+
+ Showing {{ pagination.from }} to {{ pagination.to }} of {{ pagination.total_items }} actors +
+
+ +
+ + + + + + + + + + + + + + {% for actor in actors %} + + + + + + + + + + {% else %} + + + + {% endfor %} + +
PhotoNameMoviesTV ShowsAdult VideosTotal MediaActions
+ {% if actor.thumbnail_path %} + {{ actor.name }} + {% else %} +
+ +
+ {% endif %} +
{{ actor.name }}{{ actor.movies|length }}{{ actor.tvShows|length }}{{ actor.adultVideos|length }} + {{ actor.movies|length + actor.tvShows|length + actor.adultVideos|length }} + + +
+
No actors found.
+
+
+ + + {% if pagination.total > 1 %} + + {% endif %} + {% endif %} +
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/resources/views/admin/layout.twig b/resources/views/admin/layout.twig index 59d08f7..384cab1 100644 --- a/resources/views/admin/layout.twig +++ b/resources/views/admin/layout.twig @@ -232,6 +232,12 @@ TV Shows +