actor merging!

This commit is contained in:
Lars Behrends
2025-11-06 18:45:20 +01:00
parent 0f0fb3b410
commit 5d86fcfda9
6 changed files with 38 additions and 44 deletions

View File

@@ -880,7 +880,14 @@ class AdminController extends AdminBaseController
*/ */
public function mergeActors(Request $request, Response $response, $args) public function mergeActors(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(); $data = $request->getParsedBody();
}
$masterActorId = (int)($data['master_actor_id'] ?? 0); $masterActorId = (int)($data['master_actor_id'] ?? 0);
$duplicateActorIds = array_map('intval', $data['duplicate_actor_ids'] ?? []); $duplicateActorIds = array_map('intval', $data['duplicate_actor_ids'] ?? []);
@@ -1020,7 +1027,14 @@ class AdminController extends AdminBaseController
*/ */
public function autoMergeActors(Request $request, Response $response, $args) public function autoMergeActors(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(); $data = $request->getParsedBody();
}
$actorGroupIds = $data['actor_group_ids'] ?? []; $actorGroupIds = $data['actor_group_ids'] ?? [];
if (empty($actorGroupIds)) { if (empty($actorGroupIds)) {

View File

@@ -180,7 +180,7 @@ class Actor extends Model
/** /**
* Get paginated actors with optional search and sorting * 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 public function getPaginated(\PDO $pdo, int $page = 1, int $perPage = 20, string $search = '', string $sort = 'name_asc'): array
{ {
$offset = ($page - 1) * $perPage; $offset = ($page - 1) * $perPage;
@@ -214,15 +214,15 @@ class Actor extends Model
LIMIT :limit OFFSET :offset LIMIT :limit OFFSET :offset
"); ");
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT); $stmt->bindValue(':limit', $perPage, \PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, \PDO::PARAM_INT);
foreach ($params as $key => $value) { foreach ($params as $key => $value) {
$stmt->bindValue(':' . $key, $value); $stmt->bindValue(':' . $key, $value);
} }
$stmt->execute(); $stmt->execute();
$actors = $stmt->fetchAll(PDO::FETCH_ASSOC); $actors = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Add relationships data for each actor // Add relationships data for each actor
foreach ($actors as &$actor) { foreach ($actors as &$actor) {
@@ -237,7 +237,7 @@ class Actor extends Model
/** /**
* Get total count of actors with optional search * Get total count of actors with optional search
*/ */
public function getTotalCount(PDO $pdo, string $search = ''): int public function getTotalCount(\PDO $pdo, string $search = ''): int
{ {
$whereClause = ''; $whereClause = '';
$params = []; $params = [];
@@ -254,7 +254,7 @@ class Actor extends Model
} }
$stmt->execute(); $stmt->execute();
return (int)$stmt->fetch(PDO::FETCH_ASSOC)['count']; return (int)$stmt->fetch(\PDO::FETCH_ASSOC)['count'];
} }
/** /**
@@ -270,7 +270,7 @@ class Actor extends Model
ORDER BY m.title ORDER BY m.title
"); ");
$stmt->execute([$actorId]); $stmt->execute([$actorId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(\PDO::FETCH_ASSOC);
} }
/** /**
@@ -286,7 +286,7 @@ class Actor extends Model
ORDER BY ts.title ORDER BY ts.title
"); ");
$stmt->execute([$actorId]); $stmt->execute([$actorId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(\PDO::FETCH_ASSOC);
} }
/** /**
@@ -302,6 +302,6 @@ class Actor extends Model
ORDER BY av.title ORDER BY av.title
"); ");
$stmt->execute([$actorId]); $stmt->execute([$actorId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(\PDO::FETCH_ASSOC);
} }
} }

View File

@@ -9,10 +9,10 @@
<p class="text-muted mb-0">View and manage actors, find and merge duplicates</p> <p class="text-muted mb-0">View and manage actors, find and merge duplicates</p>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<a href="{{ path_for('admin.actors.index') }}" class="btn btn-outline-primary {% if not filters.duplicates %}active{% endif %}"> <a href="{{ path_for('admin.actors') }}" class="btn btn-outline-primary {% if not filters.duplicates %}active{% endif %}">
<i class="bi bi-people me-2"></i>All Actors <i class="bi bi-people me-2"></i>All Actors
</a> </a>
<a href="{{ path_for('admin.actors.index', {}, {'duplicates': 1}) }}" class="btn btn-outline-warning {% if filters.duplicates %}active{% endif %}"> <a href="{{ path_for('admin.actors', {}, {'duplicates': 1}) }}" class="btn btn-outline-warning {% if filters.duplicates %}active{% endif %}">
<i class="bi bi-exclamation-triangle me-2"></i>Duplicates <i class="bi bi-exclamation-triangle me-2"></i>Duplicates
</a> </a>
</div> </div>
@@ -61,7 +61,7 @@
data-group="{{ group.normalized_name }}"> data-group="{{ group.normalized_name }}">
</div> </div>
{% if actor.thumbnail_path %} {% if actor.thumbnail_path %}
<img src="/images/{{ actor.thumbnail_path }}" <img src="{{ actor.thumbnail_path }}"
alt="{{ actor.name }}" alt="{{ actor.name }}"
class="rounded me-3" class="rounded me-3"
style="width: 40px; height: 40px; object-fit: cover;"> style="width: 40px; height: 40px; object-fit: cover;">
@@ -268,7 +268,7 @@
<tr> <tr>
<td> <td>
{% if actor.thumbnail_path %} {% if actor.thumbnail_path %}
<img src="/images/{{ actor.thumbnail_path }}" alt="{{ actor.name }}" style="width: 50px; height: 50px; object-fit: cover; border-radius: 50%;"> <img src="{{ actor.thumbnail_path }}" alt="{{ actor.name }}" style="width: 50px; height: 50px; object-fit: cover; border-radius: 50%;">
{% else %} {% else %}
<div class="bg-light d-flex align-items-center justify-content-center rounded-circle" style="width: 50px; height: 50px;"> <div class="bg-light d-flex align-items-center justify-content-center rounded-circle" style="width: 50px; height: 50px;">
<i class="bi bi-person text-muted"></i> <i class="bi bi-person text-muted"></i>
@@ -442,7 +442,7 @@ function autoMergeGroup(groupName) {
return; return;
} }
fetch('{{ path_for("admin.actors.auto_merge") }}', { fetch('{{ path_for("admin.actors.auto-merge") }}', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -484,7 +484,7 @@ function autoMergeAll() {
return; return;
} }
fetch('{{ path_for("admin.actors.auto_merge") }}', { fetch('{{ path_for("admin.actors.auto-merge") }}', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@@ -359,7 +359,7 @@
{% for movie in movies %} {% for movie in movies %}
<div class="group relative bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1"> <div class="group relative bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1">
{% if movie.poster_url %} {% if movie.poster_url %}
<div class="relative {{ movie.poster_aspect_ratio ? 'aspect-custom' : 'aspect-[2/3]' }} overflow-hidden"{% if movie.poster_aspect_ratio %} style="padding-bottom: {{ (1 / movie.poster_aspect_ratio) * 100 }}%;"{% endif %}> <div class="relative {{ movie.poster_aspect_ratio ? 'aspect-custom' : 'aspect-[2/3]' }} overflow-hidden">
<img src="{{ movie.poster_url }}" alt="{{ movie.title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"> <img src="{{ movie.poster_url }}" alt="{{ movie.title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300">
<!-- Overlay with movie info --> <!-- Overlay with movie info -->
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"> <div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">

View File

@@ -3,9 +3,8 @@
{% block content %} {% block content %}
<!-- Hero Section with Backdrop --> <!-- Hero Section with Backdrop -->
<div class="relative"> <div class="relative">
{% if movie.backdrop_url %}
<div class="h-96 md:h-[500px] relative overflow-hidden"> <div class="h-96 md:h-[500px] relative overflow-hidden">
<img src="/images/{{ movie.backdrop_url }}" alt="{{ movie.title }} backdrop" class="w-full h-full object-cover"> <img src="{{ movie.backdrop_url ? movie.backdrop_url : movie.poster_url }}" alt="{{ movie.title }} backdrop" class="w-full h-full object-cover">
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div> <div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div>
<div class="absolute inset-0 bg-gradient-to-r from-black/80 via-black/40 to-transparent"></div> <div class="absolute inset-0 bg-gradient-to-r from-black/80 via-black/40 to-transparent"></div>
@@ -114,27 +113,8 @@
</div> </div>
</div> </div>
</div> </div>
{% else %}
<!-- Fallback hero without backdrop -->
<div class="bg-gradient-to-r from-blue-900 to-purple-900 h-64 relative">
<div class="absolute top-4 left-4 z-10">
<a href="{{ path_for('adult.index') }}" class="inline-flex items-center text-white hover:text-gray-300 transition-colors">
<svg class="mr-2" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Back to Adult Videos
</a>
</div>
<div class="absolute bottom-0 left-0 right-0 p-6">
<h1 class="text-4xl font-bold text-white mb-2">{{ movie.title }}</h1>
{% if movie.tagline %}
<p class="text-xl text-gray-200">{{ movie.tagline }}</p>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
<!-- Main Content --> <!-- Main Content -->
<div class="max-w-7xl mx-auto px-4 py-8"> <div class="max-w-7xl mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
@@ -143,7 +123,7 @@
<div class="sticky top-4"> <div class="sticky top-4">
<!-- Poster --> <!-- Poster -->
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-6"> <div class="bg-white rounded-xl shadow-lg overflow-hidden mb-6">
<div class="aspect-[2/3]"> <div class="{{ movie.poster_aspect_ratio ? 'aspect-custom' : '' }}">
{% if movie.poster_url %} {% if movie.poster_url %}
<img src="{{ movie.poster_url }}" alt="{{ movie.title }}" class="w-full h-full object-cover"> <img src="{{ movie.poster_url }}" alt="{{ movie.title }}" class="w-full h-full object-cover">
{% else %} {% else %}
@@ -239,11 +219,11 @@
{% endif %} {% endif %}
<!-- Performers with thumbnails --> <!-- Performers with thumbnails -->
{% if movie.actors %} {% if actors %}
<div class="mt-6"> <div class="mt-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Performers</h3> <h3 class="text-lg font-semibold text-gray-900 mb-4">Performers</h3>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4"> <div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{% for actor in movie.actors %} {% for actor in actors %}
<a href="{{ path_for('actors.show', {'id': actor.id}) }}" class="flex flex-col items-center hover:bg-gray-50 rounded-lg p-2 transition-colors"> <a href="{{ path_for('actors.show', {'id': actor.id}) }}" class="flex flex-col items-center hover:bg-gray-50 rounded-lg p-2 transition-colors">
{% if actor.thumbnail_path %} {% if actor.thumbnail_path %}
<img src="{{ actor.thumbnail_path }}" alt="{{ actor.name }}" class="w-16 h-16 rounded-full object-cover mb-2"> <img src="{{ actor.thumbnail_path }}" alt="{{ actor.name }}" class="w-16 h-16 rounded-full object-cover mb-2">

View File

@@ -116,7 +116,7 @@ $app->group('', function (RouteCollectorProxy $group) {
$adminGroup->group('/actors', function (RouteCollectorProxy $group) { $adminGroup->group('/actors', function (RouteCollectorProxy $group) {
$group->get('', AdminController::class . ':actors')->setName('admin.actors.index'); $group->get('', AdminController::class . ':actors')->setName('admin.actors.index');
$group->post('/merge', AdminController::class . ':mergeActors')->setName('admin.actors.merge'); $group->post('/merge', AdminController::class . ':mergeActors')->setName('admin.actors.merge');
$group->post('/auto-merge', AdminController::class . ':autoMergeActors')->setName('admin.actors.auto_merge'); $group->post('/auto-merge', AdminController::class . ':autoMergeActors')->setName('admin.actors.auto-merge');
}); });
// Media Sources // Media Sources