mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
799 lines
32 KiB
PHP
799 lines
32 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use Psr\Http\Message\ResponseInterface as Response;
|
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
use PDO;
|
|
use Slim\Views\Twig;
|
|
|
|
class ActorController extends Controller
|
|
{
|
|
private PDO $pdo;
|
|
|
|
public function __construct(PDO $pdo, Twig $view)
|
|
{
|
|
parent::__construct($view);
|
|
$this->pdo = $pdo;
|
|
}
|
|
|
|
public function show(Request $request, Response $response, $args)
|
|
{
|
|
$actorId = $args['id'];
|
|
|
|
// Get actor details with counts from all media types (including episode actors)
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT a.*,
|
|
COUNT(DISTINCT am.movie_id) as movie_count,
|
|
COUNT(DISTINCT CASE WHEN ats.tv_show_id IS NOT NULL THEN ats.tv_show_id
|
|
WHEN ate.tv_episode_id IS NOT NULL THEN te.tv_show_id END) as tv_show_count,
|
|
COUNT(DISTINCT aav.adult_video_id) as adult_video_count,
|
|
(COUNT(DISTINCT am.movie_id) +
|
|
COUNT(DISTINCT CASE WHEN ats.tv_show_id IS NOT NULL THEN ats.tv_show_id
|
|
WHEN ate.tv_episode_id IS NOT NULL THEN te.tv_show_id END) +
|
|
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_tv_episode ate ON a.id = ate.actor_id
|
|
LEFT JOIN tv_episodes te ON ate.tv_episode_id = te.id
|
|
LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id
|
|
WHERE a.id = :actor_id
|
|
GROUP BY a.id
|
|
");
|
|
$stmt->execute(['actor_id' => $actorId]);
|
|
$actor = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$actor) {
|
|
return $response->withStatus(404)->withHeader('Content-Type', 'text/html');
|
|
}
|
|
|
|
// Get actor's adult videos (scenes)
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT av.*, s.display_name as source_name
|
|
FROM adult_videos av
|
|
JOIN sources s ON av.source_id = s.id
|
|
JOIN actor_adult_video aav ON av.id = aav.adult_video_id
|
|
WHERE aav.actor_id = :actor_id
|
|
ORDER BY av.release_date DESC, av.title ASC
|
|
");
|
|
$stmt->execute(['actor_id' => $actorId]);
|
|
$scenes = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Get actor's movies
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT m.*, s.display_name as source_name
|
|
FROM movies m
|
|
JOIN sources s ON m.source_id = s.id
|
|
JOIN actor_movie am ON m.id = am.movie_id
|
|
WHERE am.actor_id = :actor_id
|
|
ORDER BY m.release_date DESC, m.title ASC
|
|
");
|
|
$stmt->execute(['actor_id' => $actorId]);
|
|
$movies = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Get actor's TV shows (from main cast and episodes)
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT DISTINCT ts.*, s.display_name as source_name
|
|
FROM tv_shows ts
|
|
JOIN sources s ON ts.source_id = s.id
|
|
LEFT JOIN actor_tv_show ats ON ts.id = ats.tv_show_id AND ats.actor_id = :actor_id
|
|
LEFT JOIN tv_episodes te ON ts.id = te.tv_show_id
|
|
LEFT JOIN actor_tv_episode ate ON te.id = ate.tv_episode_id AND ate.actor_id = :actor_id2
|
|
WHERE ats.actor_id = :actor_id4 OR ate.actor_id = :actor_id3
|
|
ORDER BY ts.first_air_date DESC, ts.title ASC
|
|
");
|
|
$stmt->execute(['actor_id' => $actorId,'actor_id2' => $actorId,'actor_id3' => $actorId, 'actor_id4' => $actorId]);
|
|
$tvShows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
|
|
|
|
|
|
foreach ($scenes as &$scene) {
|
|
if (!empty($scene['metadata'])) {
|
|
$metadata = json_decode($scene['metadata'], true);
|
|
|
|
// Use local cover path if available, otherwise fall back to original URL
|
|
if (!empty($metadata['local_cover_path'])) {
|
|
$scene['poster_url'] = $metadata['local_cover_path'];
|
|
} elseif (!empty($metadata['cover_url'])) {
|
|
$scene['poster_url'] = $metadata['cover_url'];
|
|
}
|
|
|
|
// Add actors data if available
|
|
if (!empty($metadata['actors'])) {
|
|
$scene['actors'] = $metadata['actors'];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return $this->view->render($response, 'actor/show.twig', [
|
|
'title' => $actor['name'],
|
|
'actor' => $actor,
|
|
'scenes' => $scenes,
|
|
'movies' => $movies,
|
|
'tv_shows' => $tvShows
|
|
]);
|
|
}
|
|
|
|
public function edit(Request $request, Response $response, $args)
|
|
{
|
|
$actorId = $args['id'];
|
|
|
|
// Get actor details
|
|
$stmt = $this->pdo->prepare("SELECT * FROM actors WHERE id = :id");
|
|
$stmt->execute(['id' => $actorId]);
|
|
$actor = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$actor) {
|
|
return $response->withStatus(404)->withHeader('Content-Type', 'text/html');
|
|
}
|
|
|
|
// Decode metadata for form population
|
|
$metadata = json_decode($actor['metadata'] ?? '{}', true);
|
|
|
|
// Handle POST request (form submission)
|
|
if ($request->getMethod() === 'POST') {
|
|
$data = $request->getParsedBody();
|
|
$uploadedFiles = $request->getUploadedFiles();
|
|
|
|
// Validate required fields
|
|
$name = trim($data['name'] ?? '');
|
|
if (empty($name)) {
|
|
return $this->view->render($response, 'actor/edit.twig', [
|
|
'title' => 'Edit Actor',
|
|
'actor' => $actor,
|
|
'metadata' => $metadata,
|
|
'error' => 'Name is required'
|
|
]);
|
|
}
|
|
|
|
// Handle image upload/download
|
|
$thumbnailPath = $actor['thumbnail_path']; // Keep existing by default
|
|
$imageSource = $data['image_source'] ?? 'upload';
|
|
|
|
if ($imageSource === 'upload') {
|
|
// Handle file upload
|
|
if (!empty($uploadedFiles['thumbnail']) && $uploadedFiles['thumbnail']->getError() === UPLOAD_ERR_OK) {
|
|
$uploadedFile = $uploadedFiles['thumbnail'];
|
|
|
|
// Validate file type
|
|
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
if (!in_array($uploadedFile->getClientMediaType(), $allowedTypes)) {
|
|
return $this->view->render($response, 'actor/edit.twig', [
|
|
'title' => 'Edit Actor',
|
|
'actor' => $actor,
|
|
'metadata' => $metadata,
|
|
'error' => 'Invalid image type. Only JPEG, PNG, GIF, and WebP are allowed.'
|
|
]);
|
|
}
|
|
|
|
// Generate filename and move file
|
|
$extension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION);
|
|
$filename = 'actor_' . $actorId . '_' . time() . '.' . $extension;
|
|
$uploadPath = __DIR__ . '/../../public/images/actors/' . $filename;
|
|
|
|
// Create directory if it doesn't exist
|
|
$uploadDir = dirname($uploadPath);
|
|
if (!is_dir($uploadDir)) {
|
|
mkdir($uploadDir, 0755, true);
|
|
}
|
|
|
|
$uploadedFile->moveTo($uploadPath);
|
|
$thumbnailPath = '/images/actors/' . $filename;
|
|
}
|
|
} elseif ($imageSource === 'url') {
|
|
// Handle URL download
|
|
$imageUrl = trim($data['thumbnail_url'] ?? '');
|
|
if (!empty($imageUrl)) {
|
|
// Validate URL
|
|
if (!filter_var($imageUrl, FILTER_VALIDATE_URL)) {
|
|
return $this->view->render($response, 'actor/edit.twig', [
|
|
'title' => 'Edit Actor',
|
|
'actor' => $actor,
|
|
'metadata' => $metadata,
|
|
'error' => 'Invalid image URL provided.'
|
|
]);
|
|
}
|
|
|
|
try {
|
|
// Download image from URL
|
|
$imageData = file_get_contents($imageUrl);
|
|
if ($imageData === false) {
|
|
return $this->view->render($response, 'actor/edit.twig', [
|
|
'title' => 'Edit Actor',
|
|
'actor' => $actor,
|
|
'metadata' => $metadata,
|
|
'error' => 'Failed to download image from the provided URL.'
|
|
]);
|
|
}
|
|
|
|
// Get image info to validate type and determine extension
|
|
$imageInfo = getimagesizefromstring($imageData);
|
|
if (!$imageInfo) {
|
|
return $this->view->render($response, 'actor/edit.twig', [
|
|
'title' => 'Edit Actor',
|
|
'actor' => $actor,
|
|
'metadata' => $metadata,
|
|
'error' => 'The URL does not point to a valid image.'
|
|
]);
|
|
}
|
|
|
|
// Validate MIME type
|
|
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
if (!in_array($imageInfo['mime'], $allowedTypes)) {
|
|
return $this->view->render($response, 'actor/edit.twig', [
|
|
'title' => 'Edit Actor',
|
|
'actor' => $actor,
|
|
'metadata' => $metadata,
|
|
'error' => 'Invalid image type. Only JPEG, PNG, GIF, and WebP are allowed.'
|
|
]);
|
|
}
|
|
|
|
// Determine extension from MIME type
|
|
$extension = '';
|
|
switch ($imageInfo['mime']) {
|
|
case 'image/jpeg':
|
|
$extension = 'jpg';
|
|
break;
|
|
case 'image/png':
|
|
$extension = 'png';
|
|
break;
|
|
case 'image/gif':
|
|
$extension = 'gif';
|
|
break;
|
|
case 'image/webp':
|
|
$extension = 'webp';
|
|
break;
|
|
}
|
|
|
|
// Generate filename and save file
|
|
$filename = 'actor_' . $actorId . '_' . time() . '.' . $extension;
|
|
$uploadPath = __DIR__ . '/../../public/images/actors/' . $filename;
|
|
|
|
// Create directory if it doesn't exist
|
|
$uploadDir = dirname($uploadPath);
|
|
if (!is_dir($uploadDir)) {
|
|
mkdir($uploadDir, 0755, true);
|
|
}
|
|
|
|
// Save the downloaded image
|
|
if (file_put_contents($uploadPath, $imageData) === false) {
|
|
return $this->view->render($response, 'actor/edit.twig', [
|
|
'title' => 'Edit Actor',
|
|
'actor' => $actor,
|
|
'metadata' => $metadata,
|
|
'error' => 'Failed to save the downloaded image.'
|
|
]);
|
|
}
|
|
|
|
$thumbnailPath = '/images/actors/' . $filename;
|
|
} catch (Exception $e) {
|
|
return $this->view->render($response, 'actor/edit.twig', [
|
|
'title' => 'Edit Actor',
|
|
'actor' => $actor,
|
|
'metadata' => $metadata,
|
|
'error' => 'Error downloading image: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prepare metadata
|
|
$actorMetadata = [
|
|
'biography' => trim($data['biography'] ?? ''),
|
|
'birth_date' => trim($data['birth_date'] ?? ''),
|
|
'death_date' => trim($data['death_date'] ?? ''),
|
|
'birth_place' => trim($data['birth_place'] ?? ''),
|
|
'nationality' => trim($data['nationality'] ?? ''),
|
|
'gender' => trim($data['gender'] ?? ''),
|
|
'ethnicity' => trim($data['ethnicity'] ?? ''),
|
|
'country' => trim($data['nationality'] ?? ''), // Map nationality to country for Stash compatibility
|
|
'height' => trim($data['height'] ?? ''),
|
|
'measurements' => trim($data['measurements'] ?? ''),
|
|
'cup_size' => trim($data['cup_size'] ?? ''),
|
|
'piercings' => trim($data['piercings'] ?? ''),
|
|
'tattoos' => trim($data['tattoos'] ?? ''),
|
|
'hair_color' => trim($data['hair_color'] ?? ''),
|
|
'eye_color' => trim($data['eye_color'] ?? ''),
|
|
'weight' => trim($data['weight'] ?? ''),
|
|
'fake_tits' => trim($data['fake_tits'] ?? ''),
|
|
'penis_length' => trim($data['penis_length'] ?? ''),
|
|
'circumcised' => trim($data['circumcised'] ?? ''),
|
|
'career_length' => trim($data['career_length'] ?? ''),
|
|
'aliases' => array_filter(array_map('trim', explode(',', $data['aliases'] ?? ''))),
|
|
'favorite' => isset($data['favorite']) ? true : false,
|
|
'ignore_auto_tag' => isset($data['ignore_auto_tag']) ? true : false,
|
|
'scene_count' => (int)($data['scene_count'] ?? 0),
|
|
'details' => trim($data['details'] ?? ''),
|
|
'social_media' => [
|
|
'twitter' => trim($data['twitter'] ?? ''),
|
|
'instagram' => trim($data['instagram'] ?? ''),
|
|
'onlyfans' => trim($data['onlyfans'] ?? ''),
|
|
'website' => trim($data['website'] ?? '')
|
|
],
|
|
'adult_specific' => [
|
|
'debut_year' => trim($data['debut_year'] ?? ''),
|
|
'retirement_year' => trim($data['retirement_year'] ?? ''),
|
|
'active' => isset($data['active']) ? true : false,
|
|
'genres' => array_filter(array_map('trim', explode(',', $data['adult_genres'] ?? ''))),
|
|
'specialties' => array_filter(array_map('trim', explode(',', $data['specialties'] ?? '')))
|
|
]
|
|
];
|
|
|
|
// Update actor
|
|
$stmt = $this->pdo->prepare("
|
|
UPDATE actors
|
|
SET name = :name, thumbnail_path = :thumbnail_path, metadata = :metadata, updated_at = NOW()
|
|
WHERE id = :id
|
|
");
|
|
$stmt->execute([
|
|
'id' => $actorId,
|
|
'name' => $name,
|
|
'thumbnail_path' => $thumbnailPath,
|
|
'metadata' => json_encode($actorMetadata)
|
|
]);
|
|
|
|
// Redirect back to actor show page
|
|
return $response->withHeader('Location', '/media/actors/' . $actorId)->withStatus(302);
|
|
}
|
|
|
|
// GET request - show edit form
|
|
return $this->view->render($response, 'actor/edit.twig', [
|
|
'title' => 'Edit Actor',
|
|
'actor' => $actor,
|
|
'metadata' => $metadata
|
|
]);
|
|
}
|
|
public function fetchStashData(Request $request, Response $response, $args)
|
|
{
|
|
ini_set('memory_limit', '2G');
|
|
|
|
try {
|
|
// Parse JSON body for API requests
|
|
$rawBody = $request->getBody()->getContents();
|
|
$data = json_decode($rawBody, true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
return $this->jsonResponse($response->withStatus(400), [
|
|
'error' => 'Invalid JSON in request body'
|
|
]);
|
|
}
|
|
|
|
$query = trim($data['query'] ?? '');
|
|
|
|
if (empty($query)) {
|
|
return $this->jsonResponse($response->withStatus(400), [
|
|
'error' => 'Missing required parameter: query'
|
|
]);
|
|
}
|
|
|
|
// Get Stash configuration from database
|
|
$stmt = $this->pdo->prepare('SELECT * FROM sources WHERE name = ?');
|
|
$stmt->execute(['stash']);
|
|
$stashSource = $stmt->fetch(\PDO::FETCH_ASSOC);
|
|
|
|
if (!$stashSource) {
|
|
return $this->jsonResponse($response->withStatus(500), [
|
|
'error' => 'Stash source not configured in database'
|
|
]);
|
|
}
|
|
|
|
$stashUrl = trim($stashSource['api_url'] ?? '');
|
|
$stashApiKey = trim($stashSource['api_key'] ?? '');
|
|
|
|
if (empty($stashUrl)) {
|
|
return $this->jsonResponse($response->withStatus(500), [
|
|
'error' => 'Stash API URL not configured'
|
|
]);
|
|
}
|
|
|
|
// Create HTTP client for Stash API
|
|
$client = new \GuzzleHttp\Client([
|
|
'timeout' => 30,
|
|
'verify' => false,
|
|
'headers' => [
|
|
'User-Agent' => 'MediaCollector/1.0',
|
|
'ApiKey' => $stashApiKey,
|
|
'Content-Type' => 'application/json'
|
|
]
|
|
]);
|
|
|
|
// Build GraphQL query to search for performers
|
|
$graphqlQuery = '
|
|
query FindPerformers($filter: FindFilterType) {
|
|
findPerformers(filter: $filter) {
|
|
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' => [
|
|
'q' => $query,
|
|
'per_page' => 10, // Limit results
|
|
'sort' => 'name',
|
|
'direction' => 'ASC'
|
|
]
|
|
];
|
|
|
|
// Make the API call
|
|
$apiResponse = $client->post(rtrim($stashUrl, '/') . '/graphql', [
|
|
'json' => [
|
|
'query' => $graphqlQuery,
|
|
'variables' => $variables
|
|
]
|
|
]);
|
|
|
|
$result = json_decode($apiResponse->getBody(), true);
|
|
|
|
if (!isset($result['data']['findPerformers']['performers'])) {
|
|
return $this->jsonResponse($response, [
|
|
'performers' => [],
|
|
'message' => 'No performers found'
|
|
]);
|
|
}
|
|
|
|
$performers = $result['data']['findPerformers']['performers'];
|
|
|
|
return $this->jsonResponse($response, [
|
|
'performers' => $performers,
|
|
'count' => count($performers),
|
|
'stash_url' => $stashUrl
|
|
]);
|
|
|
|
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
|
$errorMessage = 'Failed to connect to Stash server';
|
|
if ($e->hasResponse()) {
|
|
$statusCode = $e->getResponse()->getStatusCode();
|
|
$errorMessage .= ": HTTP {$statusCode}";
|
|
}
|
|
return $this->jsonResponse($response->withStatus(500), [
|
|
'error' => $errorMessage
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return $this->jsonResponse($response->withStatus(500), [
|
|
'error' => 'Internal server error: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function getMissingStashReports(Request $request, Response $response, $args)
|
|
{
|
|
try {
|
|
$logDir = __DIR__ . '/../../storage/logs';
|
|
$reports = [];
|
|
|
|
if (is_dir($logDir)) {
|
|
$files = glob($logDir . '/missing_stash_actors_*.json');
|
|
|
|
foreach ($files as $file) {
|
|
$filename = basename($file);
|
|
$fileData = json_decode(file_get_contents($file), true);
|
|
|
|
if ($fileData) {
|
|
$reports[] = [
|
|
'filename' => $filename,
|
|
'generated_at' => $fileData['generated_at'] ?? 'Unknown',
|
|
'total_missing' => $fileData['total_missing'] ?? 0,
|
|
'description' => $fileData['description'] ?? '',
|
|
'file_path' => $file
|
|
];
|
|
}
|
|
}
|
|
|
|
// Sort by generation date (newest first)
|
|
usort($reports, function($a, $b) {
|
|
return strtotime($b['generated_at']) <=> strtotime($a['generated_at']);
|
|
});
|
|
}
|
|
|
|
return $this->jsonResponse($response, [
|
|
'reports' => $reports,
|
|
'total_reports' => count($reports)
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
return $this->jsonResponse($response->withStatus(500), [
|
|
'error' => 'Internal server error: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function syncExistingPerformers(Request $request, Response $response, $args)
|
|
{
|
|
// Set PHP timeouts for long-running operations
|
|
ini_set('memory_limit', '2G');
|
|
ini_set('max_execution_time', 0); // Allow unlimited execution time
|
|
ini_set('max_input_time', 3600); // 1 hour for input processing
|
|
ini_set('default_socket_timeout', 3600); // 1 hour for socket operations
|
|
|
|
// Set headers to prevent timeouts
|
|
$response = $response->withHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
|
|
|
|
try {
|
|
// Get Stash configuration from database
|
|
$stmt = $this->pdo->prepare('SELECT * FROM sources WHERE name = ?');
|
|
$stmt->execute(['stash']);
|
|
$stashSource = $stmt->fetch(\PDO::FETCH_ASSOC);
|
|
|
|
if (!$stashSource) {
|
|
return $this->jsonResponse($response->withStatus(500), [
|
|
'error' => 'Stash source not configured in database'
|
|
]);
|
|
}
|
|
|
|
// Create Stash sync service
|
|
$stashSyncService = new \App\Services\StashSyncService($this->pdo, $stashSource);
|
|
|
|
// Run the existing performers sync
|
|
$results = $stashSyncService->syncExistingPerformers();
|
|
|
|
return $this->jsonResponse($response, [
|
|
'success' => true,
|
|
'message' => 'Existing performers sync completed',
|
|
'results' => $results
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
return $this->jsonResponse($response->withStatus(500), [
|
|
'error' => 'Internal server error: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function index(Request $request, Response $response, $args)
|
|
{
|
|
$queryParams = $request->getQueryParams();
|
|
|
|
// Get pagination parameters
|
|
$page = max(1, (int)($queryParams['page'] ?? 1));
|
|
$perPage = max(12, min(100, (int)($queryParams['per_page'] ?? 48)));
|
|
|
|
// Get search parameters
|
|
$search = trim($queryParams['search'] ?? '');
|
|
|
|
// Get filter parameters
|
|
$hasMovies = $queryParams['has_movies'] ?? null;
|
|
$hasTvShows = $queryParams['has_tv_shows'] ?? null;
|
|
$hasAdultVideos = $queryParams['has_adult_videos'] ?? null;
|
|
|
|
// Get sort parameter
|
|
$sort = $queryParams['sort'] ?? 'total_media_desc'; // total_media_desc, total_media_asc, name_asc, name_desc
|
|
|
|
// Build the base query - simplified to ensure all actors are found
|
|
$sql = "
|
|
SELECT a.id, a.name, a.thumbnail_path,
|
|
COALESCE(adult_counts.adult_video_count, 0) as adult_video_count,
|
|
COALESCE(movie_counts.movie_count, 0) as movie_count,
|
|
COALESCE(tv_counts.tv_show_count, 0) as tv_show_count,
|
|
(COALESCE(adult_counts.adult_video_count, 0) + COALESCE(movie_counts.movie_count, 0) + COALESCE(tv_counts.tv_show_count, 0)) as total_media_count,
|
|
GREATEST(
|
|
COALESCE(adult_dates.latest_adult, '1900-01-01'),
|
|
COALESCE(movie_dates.latest_movie, '1900-01-01'),
|
|
COALESCE(tv_dates.latest_tv, '1900-01-01')
|
|
) as latest_media_date
|
|
FROM actors a
|
|
LEFT JOIN (
|
|
SELECT actor_id, COUNT(DISTINCT adult_video_id) as adult_video_count
|
|
FROM actor_adult_video
|
|
GROUP BY actor_id
|
|
) adult_counts ON a.id = adult_counts.actor_id
|
|
LEFT JOIN (
|
|
SELECT actor_id, COUNT(DISTINCT movie_id) as movie_count
|
|
FROM actor_movie
|
|
GROUP BY actor_id
|
|
) movie_counts ON a.id = movie_counts.actor_id
|
|
LEFT JOIN (
|
|
SELECT actor_id, COUNT(DISTINCT tv_show_id) as tv_show_count
|
|
FROM (
|
|
SELECT actor_id, tv_show_id FROM actor_tv_show
|
|
UNION
|
|
SELECT ate.actor_id, te.tv_show_id
|
|
FROM actor_tv_episode ate
|
|
JOIN tv_episodes te ON ate.tv_episode_id = te.id
|
|
) combined_tv
|
|
GROUP BY actor_id
|
|
) tv_counts ON a.id = tv_counts.actor_id
|
|
LEFT JOIN (
|
|
SELECT aav.actor_id, MAX(av.release_date) as latest_adult
|
|
FROM actor_adult_video aav
|
|
JOIN adult_videos av ON aav.adult_video_id = av.id
|
|
GROUP BY aav.actor_id
|
|
) adult_dates ON a.id = adult_dates.actor_id
|
|
LEFT JOIN (
|
|
SELECT am.actor_id, MAX(m.release_date) as latest_movie
|
|
FROM actor_movie am
|
|
JOIN movies m ON am.movie_id = m.id
|
|
GROUP BY am.actor_id
|
|
) movie_dates ON a.id = movie_dates.actor_id
|
|
LEFT JOIN (
|
|
SELECT combined_tv.actor_id, MAX(ts.first_air_date) as latest_tv
|
|
FROM (
|
|
SELECT actor_id, tv_show_id FROM actor_tv_show
|
|
UNION
|
|
SELECT ate.actor_id, te.tv_show_id
|
|
FROM actor_tv_episode ate
|
|
JOIN tv_episodes te ON ate.tv_episode_id = te.id
|
|
) combined_tv
|
|
JOIN tv_shows ts ON combined_tv.tv_show_id = ts.id
|
|
GROUP BY combined_tv.actor_id
|
|
) tv_dates ON a.id = tv_dates.actor_id
|
|
";
|
|
|
|
$params = [];
|
|
$whereClauses = [];
|
|
|
|
// Add search filter
|
|
if (!empty($search)) {
|
|
$whereClauses[] = "a.name LIKE :search";
|
|
$params['search'] = "%{$search}%";
|
|
}
|
|
|
|
if (!empty($whereClauses)) {
|
|
$sql .= ' WHERE ' . implode(' AND ', $whereClauses);
|
|
}
|
|
|
|
$sql .= " GROUP BY a.id";
|
|
|
|
// Add HAVING clause for filters that require aggregation
|
|
$havingClauses = [];
|
|
if ($hasMovies === '1') {
|
|
$havingClauses[] = "movie_count > 0";
|
|
}
|
|
if ($hasTvShows === '1') {
|
|
$havingClauses[] = "tv_show_count > 0";
|
|
}
|
|
if ($hasAdultVideos === '1') {
|
|
$havingClauses[] = "adult_video_count > 0";
|
|
}
|
|
|
|
if (!empty($havingClauses)) {
|
|
$sql .= ' HAVING ' . implode(' AND ', $havingClauses);
|
|
}
|
|
|
|
// Add sorting
|
|
$sortMap = [
|
|
'total_media_desc' => 'total_media_count DESC, a.name ASC',
|
|
'total_media_asc' => 'total_media_count ASC, a.name ASC',
|
|
'name_asc' => 'a.name ASC',
|
|
'name_desc' => 'a.name DESC',
|
|
'latest_desc' => 'latest_media_date DESC NULLS LAST, a.name ASC',
|
|
];
|
|
$orderBy = $sortMap[$sort] ?? 'total_media_count DESC, a.name ASC';
|
|
$sql .= " ORDER BY {$orderBy}";
|
|
|
|
// Get total count for pagination - use a subquery to count the results
|
|
$countSql = "
|
|
SELECT COUNT(*) as count FROM (
|
|
SELECT a.id,
|
|
COALESCE(adult_counts.adult_video_count, 0) as adult_video_count,
|
|
COALESCE(movie_counts.movie_count, 0) as movie_count,
|
|
COALESCE(tv_counts.tv_show_count, 0) as tv_show_count
|
|
FROM actors a
|
|
LEFT JOIN (
|
|
SELECT actor_id, COUNT(DISTINCT adult_video_id) as adult_video_count
|
|
FROM actor_adult_video
|
|
GROUP BY actor_id
|
|
) adult_counts ON a.id = adult_counts.actor_id
|
|
LEFT JOIN (
|
|
SELECT actor_id, COUNT(DISTINCT movie_id) as movie_count
|
|
FROM actor_movie
|
|
GROUP BY actor_id
|
|
) movie_counts ON a.id = movie_counts.actor_id
|
|
LEFT JOIN (
|
|
SELECT actor_id, COUNT(DISTINCT tv_show_id) as tv_show_count
|
|
FROM (
|
|
SELECT actor_id, tv_show_id FROM actor_tv_show
|
|
UNION
|
|
SELECT ate.actor_id, te.tv_show_id
|
|
FROM actor_tv_episode ate
|
|
JOIN tv_episodes te ON ate.tv_episode_id = te.id
|
|
) combined_tv
|
|
GROUP BY actor_id
|
|
) tv_counts ON a.id = tv_counts.actor_id
|
|
";
|
|
|
|
// Add search filter
|
|
if (!empty($whereClauses)) {
|
|
$countSql .= ' WHERE ' . implode(' AND ', $whereClauses);
|
|
}
|
|
|
|
$countSql .= " GROUP BY a.id";
|
|
|
|
// Add HAVING clause for filters
|
|
if (!empty($havingClauses)) {
|
|
$countSql .= ' HAVING ' . implode(' AND ', $havingClauses);
|
|
}
|
|
|
|
$countSql .= ") as filtered_actors";
|
|
|
|
$countStmt = $this->pdo->prepare($countSql);
|
|
foreach ($params as $key => $value) {
|
|
$countStmt->bindValue($key, $value);
|
|
}
|
|
$countStmt->execute();
|
|
$countResult = $countStmt->fetch(PDO::FETCH_ASSOC);
|
|
$totalCount = (int) ($countResult['count'] ?? 0);
|
|
|
|
// Add pagination
|
|
$offset = ($page - 1) * $perPage;
|
|
$sql .= " LIMIT :limit OFFSET :offset";
|
|
|
|
// Execute main query
|
|
$stmt = $this->pdo->prepare($sql);
|
|
$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);
|
|
|
|
// Calculate pagination info
|
|
$totalPages = ceil($totalCount / $perPage);
|
|
$hasNextPage = $page < $totalPages;
|
|
$hasPrevPage = $page > 1;
|
|
|
|
return $this->view->render($response, 'actor/index.twig', [
|
|
'title' => 'Actors & Performers',
|
|
'actors' => $actors,
|
|
'pagination' => [
|
|
'current_page' => $page,
|
|
'per_page' => $perPage,
|
|
'total_pages' => $totalPages,
|
|
'total_items' => $totalCount,
|
|
'has_next' => $hasNextPage,
|
|
'has_prev' => $hasPrevPage,
|
|
'next_page' => $page + 1,
|
|
'prev_page' => $page - 1
|
|
],
|
|
'search' => $search,
|
|
'sort' => $sort,
|
|
'sort_options' => [
|
|
'total_media_desc' => 'Most Media',
|
|
'total_media_asc' => 'Least Media',
|
|
'name_asc' => 'Name A-Z',
|
|
'name_desc' => 'Name Z-A',
|
|
'latest_desc' => 'Recently Active'
|
|
],
|
|
'filters' => [
|
|
'has_movies' => $hasMovies,
|
|
'has_tv_shows' => $hasTvShows,
|
|
'has_adult_videos' => $hasAdultVideos
|
|
]
|
|
]);
|
|
}
|
|
}
|