searcg revamp 😧

This commit is contained in:
Lars Behrends
2025-11-06 13:39:46 +01:00
parent a44c311e89
commit 0f0fb3b410
8 changed files with 1526 additions and 144 deletions

View File

@@ -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;
}
}