diff --git a/app/Controllers/ActorController.php b/app/Controllers/ActorController.php index 2d4b7f6..a5965d2 100644 --- a/app/Controllers/ActorController.php +++ b/app/Controllers/ActorController.php @@ -21,16 +21,22 @@ class ActorController extends Controller { $actorId = $args['id']; - // Get actor details with counts from all media types + // 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 ats.tv_show_id) as tv_show_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 ats.tv_show_id) + COUNT(DISTINCT aav.adult_video_id)) as total_media_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 @@ -66,16 +72,18 @@ class ActorController extends Controller $stmt->execute(['actor_id' => $actorId]); $movies = $stmt->fetchAll(PDO::FETCH_ASSOC); - // Get actor's TV shows + // Get actor's TV shows (from main cast and episodes) $stmt = $this->pdo->prepare(" - SELECT ts.*, s.display_name as source_name + SELECT DISTINCT ts.*, s.display_name as source_name FROM tv_shows ts JOIN sources s ON ts.source_id = s.id - JOIN actor_tv_show ats ON ts.id = ats.tv_show_id - WHERE ats.actor_id = :actor_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]); + $stmt->execute(['actor_id' => $actorId,'actor_id2' => $actorId,'actor_id3' => $actorId, 'actor_id4' => $actorId]); $tvShows = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -108,33 +116,314 @@ class ActorController extends Controller '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 + $thumbnailPath = $actor['thumbnail_path']; // Keep existing by default + 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; + } + + // 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 index(Request $request, Response $response, $args) { - // Get all actors with their media counts from all types - $stmt = $this->pdo->prepare(" - SELECT a.*, - COUNT(DISTINCT aav.adult_video_id) as adult_video_count, - COUNT(DISTINCT am.movie_id) as movie_count, - COUNT(DISTINCT ats.tv_show_id) as tv_show_count, - (COUNT(DISTINCT aav.adult_video_id) + COUNT(DISTINCT am.movie_id) + COUNT(DISTINCT ats.tv_show_id)) as total_media_count, - MAX(COALESCE(av.release_date, m.release_date, ts.first_air_date)) as latest_media_date + $queryParams = $request->getQueryParams(); + + // Get pagination parameters + $page = max(1, (int)($queryParams['page'] ?? 1)); + $perPage = max(12, min(100, (int)($queryParams['per_page'] ?? 24))); + + // 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 actor_adult_video aav ON a.id = aav.actor_id - LEFT JOIN adult_videos av ON aav.adult_video_id = av.id - LEFT JOIN actor_movie am ON a.id = am.actor_id - LEFT JOIN movies m ON am.movie_id = m.id - LEFT JOIN actor_tv_show ats ON a.id = ats.actor_id - LEFT JOIN tv_shows ts ON ats.tv_show_id = ts.id - GROUP BY a.id - ORDER BY total_media_count DESC, a.name ASC - LIMIT 50 - "); + 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 + $countSql = str_replace('SELECT a.id, a.name, a.thumbnail_path,', 'SELECT COUNT(*) as count,', $sql); + $countSql = preg_replace('/ORDER BY.*$/', '', $countSql); + $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 + '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 + ] ]); } } diff --git a/app/Controllers/AdultController.php b/app/Controllers/AdultController.php index 719b577..411ffed 100644 --- a/app/Controllers/AdultController.php +++ b/app/Controllers/AdultController.php @@ -41,12 +41,18 @@ class AdultController extends Controller } $directors = array_filter($directors); + $sources = $queryParams['sources'] ?? []; + if (!is_array($sources)) { + $sources = [$sources]; + } + $sources = array_filter($sources); + // Get view mode and sort $viewMode = $queryParams['view'] ?? 'grid'; // grid, list, covers $sort = $queryParams['sort'] ?? 'recent'; // Get adult videos with pagination, filters, and sorting - $adultVideos = AdultVideo::getAllWithPagination($this->pdo, $page, $perPage, $search, $genres, $directors, $sort); + $adultVideos = AdultVideo::getAllWithPagination($this->pdo, $page, $perPage, $search, $genres, $directors, $sources, $sort); // Process metadata to extract local image paths for template compatibility foreach ($adultVideos as &$video) { @@ -68,11 +74,12 @@ class AdultController extends Controller } // Get total count for pagination - $totalCount = AdultVideo::getTotalCount($this->pdo, $search, $genres, $directors); + $totalCount = AdultVideo::getTotalCount($this->pdo, $search, $genres, $directors, $sources); // Get available filter options $availableGenres = AdultVideo::getAvailableGenres($this->pdo); $availableDirectors = AdultVideo::getAvailableDirectors($this->pdo); + $availableSources = AdultVideo::getAvailableSources($this->pdo); // Calculate pagination info $totalPages = ceil($totalCount / $perPage); @@ -112,11 +119,13 @@ class AdultController extends Controller ], 'filters' => [ 'genres' => $genres, - 'directors' => $directors + 'directors' => $directors, + 'sources' => $sources ], 'available_filters' => [ 'genres' => $availableGenres, - 'directors' => $availableDirectors + 'directors' => $availableDirectors, + 'sources' => $availableSources ] ]); } @@ -144,13 +153,13 @@ class AdultController extends Controller // Add local image paths and other metadata to the video data for template compatibility if (!empty($metadata['local_cover_path'])) { - $adultVideo['poster_url'] = '/images/'.$metadata['local_cover_path']; + $adultVideo['poster_url'] = $metadata['local_cover_path']; } elseif (!empty($metadata['cover_url'])) { $adultVideo['poster_url'] = $metadata['cover_url']; } if (!empty($metadata['local_screenshot_path'])) { - $adultVideo['screenshot_url'] = '/images/'.$metadata['local_screenshot_path']; + $adultVideo['screenshot_url'] = $metadata['local_screenshot_path']; } // Add actors data if available diff --git a/app/Controllers/MovieController.php b/app/Controllers/MovieController.php index 55b3e93..a962979 100644 --- a/app/Controllers/MovieController.php +++ b/app/Controllers/MovieController.php @@ -122,6 +122,34 @@ class MovieController extends Controller // Decode metadata for display $metadata = json_decode($movie['metadata'], true); + // Extract additional fields from metadata if available + if ($metadata) { + // Production companies + if (isset($metadata['production_companies']) && is_array($metadata['production_companies'])) { + $companies = array_column($metadata['production_companies'], 'name'); + $movie['production_companies'] = implode(', ', $companies); + } + + // Production countries + if (isset($metadata['production_countries']) && is_array($metadata['production_countries'])) { + $countries = array_column($metadata['production_countries'], 'name'); + $movie['production_countries'] = implode(', ', $countries); + } + + // Collection info + if (isset($metadata['belongs_to_collection']) && is_array($metadata['belongs_to_collection'])) { + $movie['belongs_to_collection'] = $metadata['belongs_to_collection']['name'] ?? null; + } + + // Additional metadata fields + //$movie['budget'] = $metadata['budget'] ?? $movie['budget']; + //$movie['revenue'] = $metadata['revenue'] ?? $movie['revenue']; + //$movie['original_language'] = $metadata['original_language'] ?? $movie['original_language']; + //$movie['tagline'] = $metadata['tagline'] ?? $movie['tagline']; + //$movie['status'] = $metadata['status'] ?? $movie['status']; + //$movie['vote_count'] = $metadata['vote_count'] ?? $movie['vote_count']; + } + // Get actors for this movie $stmt = $this->pdo->prepare(" SELECT a.* diff --git a/app/Controllers/TvShowController.php b/app/Controllers/TvShowController.php index edb0130..de4ea26 100644 --- a/app/Controllers/TvShowController.php +++ b/app/Controllers/TvShowController.php @@ -109,7 +109,7 @@ class TvShowController extends Controller $cast = json_decode($tvShow['cast'] ?? '[]', true); $genre = json_decode($tvShow['genre'] ?? '[]', true); - // Get actors for this TV show + // Get actors for this TV show (from main cast) $stmt = $this->pdo->prepare(" SELECT a.* FROM actors a @@ -118,10 +118,59 @@ class TvShowController extends Controller ORDER BY a.name ASC "); $stmt->execute(['tv_show_id' => $tvShowId]); - $actors = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $mainActors = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + // Get all actors from episodes + $stmt = $this->pdo->prepare(" + SELECT DISTINCT a.* + FROM actors a + JOIN actor_tv_episode ate ON a.id = ate.actor_id + JOIN tv_episodes e ON ate.tv_episode_id = e.id + WHERE e.tv_show_id = :tv_show_id + ORDER BY a.name ASC + "); + $stmt->execute(['tv_show_id' => $tvShowId]); + $episodeActors = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + // Merge and deduplicate actors + $allActors = array_merge($mainActors, $episodeActors); + $actorsById = []; + foreach ($allActors as $actor) { + $actorsById[$actor['id']] = $actor; + } + $actors = array_values($actorsById); + // Sort by name + usort($actors, function($a, $b) { + return strcmp($a['name'], $b['name']); + }); // Get seasons and episodes for this TV show $tvShowModel = new TvShow($this->pdo, $tvShow); $seasons = $tvShowModel->getSeasonsWithEpisodes(); + + // Get recent episodes (last 5 aired episodes) + $stmt = $this->pdo->prepare(" + SELECT e.* + FROM tv_episodes e + WHERE e.tv_show_id = :tv_show_id AND e.air_date IS NOT NULL + ORDER BY e.air_date DESC + LIMIT 5 + "); + $stmt->execute(['tv_show_id' => $tvShowId]); + $recent_episodes = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + // Add actors for recent episodes + foreach ($recent_episodes as &$episode) { + $episodeStmt = $this->pdo->prepare(" + SELECT a.* + FROM actors a + JOIN actor_tv_episode ate ON a.id = ate.actor_id + WHERE ate.tv_episode_id = :tv_episode_id + ORDER BY a.name ASC + "); + $episodeStmt->execute(['tv_episode_id' => $episode['id']]); + $episode['actors'] = $episodeStmt->fetchAll(\PDO::FETCH_ASSOC); + } + return $this->view->render($response, 'tvshows/show.twig', [ 'title' => $tvShow['title'], 'tvshow' => $tvShow, @@ -129,7 +178,8 @@ class TvShowController extends Controller 'cast' => $cast, 'genre' => $genre, 'actors' => $actors, - 'seasons' => $seasons + 'seasons' => $seasons, + 'recent_episodes' => $recent_episodes ]); } diff --git a/app/Models/AdultVideo.php b/app/Models/AdultVideo.php index f427ecd..9ef028b 100644 --- a/app/Models/AdultVideo.php +++ b/app/Models/AdultVideo.php @@ -25,7 +25,7 @@ class AdultVideo extends Model 'external_id' ]; - public static function getAllWithPagination(\PDO $pdo, int $page, int $perPage, string $search = '', array $genres = [], array $directors = [], string $sort = 'recent'): array + public static function getAllWithPagination(\PDO $pdo, int $page, int $perPage, string $search = '', array $genres = [], array $directors = [], array $sources = [], string $sort = 'recent'): array { $offset = ($page - 1) * $perPage; @@ -61,6 +61,16 @@ class AdultVideo extends Model $sql .= $whereClause . " av.director IN (" . implode(',', $placeholders) . ")"; } + if (!empty($sources)) { + $placeholders = []; + foreach ($sources as $index => $source) { + $placeholders[] = ":source_{$index}"; + $params["source_{$index}"] = $source; + } + $whereClause = (!empty($search) || !empty($genres) || !empty($directors)) ? " AND" : " WHERE"; + $sql .= $whereClause . " s.display_name IN (" . implode(',', $placeholders) . ")"; + } + // Add sorting $sortOptions = [ 'recent' => 'av.created_at DESC', @@ -92,7 +102,7 @@ class AdultVideo extends Model return $stmt->fetchAll(\PDO::FETCH_ASSOC); } - public static function getTotalCount(\PDO $pdo, string $search = '', array $genres = [], array $directors = []): int + public static function getTotalCount(\PDO $pdo, string $search = '', array $genres = [], array $directors = [], array $sources = []): int { $sql = "SELECT COUNT(*) as count FROM adult_videos av JOIN sources s ON av.source_id = s.id"; $params = []; @@ -122,6 +132,16 @@ class AdultVideo extends Model $sql .= $whereClause . " av.director IN (" . implode(',', $placeholders) . ")"; } + if (!empty($sources)) { + $placeholders = []; + foreach ($sources as $index => $source) { + $placeholders[] = ":source_{$index}"; + $params["source_{$index}"] = $source; + } + $whereClause = (!empty($search) || !empty($genres) || !empty($directors)) ? " AND" : " WHERE"; + $sql .= $whereClause . " s.display_name IN (" . implode(',', $placeholders) . ")"; + } + $stmt = $pdo->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue(":{$key}", $value); @@ -245,6 +265,21 @@ class AdultVideo extends Model return $stmt->fetchAll(\PDO::FETCH_COLUMN); } + /** + * Get available sources for filtering + */ + public static function getAvailableSources(\PDO $pdo): array + { + $stmt = $pdo->query(" + SELECT DISTINCT s.display_name + FROM sources s + JOIN adult_videos av ON s.id = av.source_id + WHERE s.display_name IS NOT NULL AND s.display_name != '' + ORDER BY s.display_name + "); + return $stmt->fetchAll(\PDO::FETCH_COLUMN); + } + /** * Get TV show statistics */ diff --git a/app/Models/TvShow.php b/app/Models/TvShow.php index 53ae980..467c431 100644 --- a/app/Models/TvShow.php +++ b/app/Models/TvShow.php @@ -375,6 +375,19 @@ class TvShow extends Model ]); $episodes = $stmt->fetchAll(\PDO::FETCH_ASSOC); + // Add actors for each episode + foreach ($episodes as &$episode) { + $episodeStmt = $this->pdo->prepare(" + SELECT a.* + FROM actors a + JOIN actor_tv_episode ate ON a.id = ate.actor_id + WHERE ate.tv_episode_id = :tv_episode_id + ORDER BY a.name ASC + "); + $episodeStmt->execute(['tv_episode_id' => $episode['id']]); + $episode['actors'] = $episodeStmt->fetchAll(\PDO::FETCH_ASSOC); + } + // Create a season object (simulating the old seasons table structure) $seasons[] = [ 'id' => null, // No seasons table, so no ID diff --git a/app/Services/StashSyncService.php b/app/Services/StashSyncService.php index eb7d58e..007d075 100644 --- a/app/Services/StashSyncService.php +++ b/app/Services/StashSyncService.php @@ -465,15 +465,9 @@ class StashSyncService extends BaseSyncService $sceneData['local_cover_path'] = $existingMetadata['local_cover_path'] ?? null; $sceneData['local_screenshot_path'] = $existingMetadata['local_screenshot_path'] ?? null; } - // Handle performers/actors + // Handle performers/actors with full metadata $performers = $sceneData['performers'] ?? []; - $actorNames = []; - $performerImages = []; - foreach ($performers as $performer) { - $actorNames[] = $performer['name']; - $performerImages[$performer['name']] = $performer['image_path'] ?? null; - } - $actors = $this->syncActors($actorNames, $performerImages); + $actors = $this->syncActors($performers); $sceneData = [ 'title' => $sceneData['title'] ?: 'Untitled Scene', @@ -618,15 +612,14 @@ class StashSyncService extends BaseSyncService } } - private function syncActors(array $actorNames, array $performerImages = []): array + private function syncActors(array $performers): array { $actors = []; - foreach ($actorNames as $actorName) { - if (empty($actorName)) continue; + foreach ($performers as $performer) { + if (empty($performer['name'])) continue; - $imagePath = $performerImages[$actorName] ?? null; - $actor = $this->getOrCreateActor($actorName, $imagePath); + $actor = $this->getOrCreateActor($performer); if ($actor) { $actors[] = $actor; } @@ -635,25 +628,63 @@ class StashSyncService extends BaseSyncService return $actors; } - private function getOrCreateActor(string $name, ?string $imagePath = null): ?array + private function getOrCreateActor(array $performer): ?array { + $name = $performer['name'] ?? ''; + if (empty($name)) return null; + // Check if actor already exists $stmt = $this->pdo->prepare(" - SELECT id, name, thumbnail_path FROM actors WHERE name = :name + SELECT id, name, thumbnail_path, metadata FROM actors WHERE name = :name "); $stmt->execute(['name' => $name]); $existingActor = $stmt->fetch(PDO::FETCH_ASSOC); - if ($existingActor) { - return [ - 'id' => $existingActor['id'], - 'name' => $existingActor['name'], - 'thumbnail_path' => $existingActor['thumbnail_path'] - ]; - } + // Prepare rich metadata from Stash performer data + $actorMetadata = [ + 'stash_id' => $performer['id'] ?? null, + 'stash_url' => $performer['url'] ?? null, + 'disambiguation' => $performer['disambiguation'] ?? '', + 'gender' => $performer['gender'] ?? null, + 'birth_date' => $performer['birthdate'] ?? null, + 'death_date' => $performer['death_date'] ?? null, + 'ethnicity' => $performer['ethnicity'] ?? null, + 'country' => $performer['country'] ?? null, + 'nationality' => $performer['country'] ?? null, // Map country to nationality + 'eye_color' => $performer['eye_color'] ?? null, + 'hair_color' => $performer['hair_color'] ?? null, + 'height' => $performer['height_cm'] ? $performer['height_cm'] . 'cm' : null, + 'measurements' => $performer['measurements'] ?? null, + 'cup_size' => $this->extractCupSize($performer['measurements'] ?? ''), + 'weight' => $performer['weight'] ? $performer['weight'] . 'kg' : null, + 'piercings' => $performer['piercings'] ?? null, + 'tattoos' => $performer['tattoos'] ?? null, + 'fake_tits' => $performer['fake_tits'] ?? null, + 'penis_length' => $performer['penis_length'] ?? null, + 'circumcised' => $performer['circumcised'] ?? null, + 'career_length' => $performer['career_length'] ?? null, + 'aliases' => $performer['alias_list'] ?? [], + 'favorite' => $performer['favorite'] ?? false, + 'ignore_auto_tag' => $performer['ignore_auto_tag'] ?? false, + 'scene_count' => $performer['scene_count'] ?? 0, + 'details' => $performer['details'] ?? null, + 'stash_created_at' => $performer['created_at'] ?? null, + 'stash_updated_at' => $performer['updated_at'] ?? null, + 'social_media' => [ + 'website' => $performer['url'] ?? null + ], + 'adult_specific' => [ + 'debut_year' => $this->extractDebutYear($performer['career_length'] ?? ''), + 'retirement_year' => $this->extractRetirementYear($performer['career_length'] ?? ''), + 'active' => $this->isActivePerformer($performer['career_length'] ?? ''), + 'genres' => [], + 'specialties' => [] + ] + ]; // Try to download performer image if available $thumbnailPath = null; + $imagePath = $performer['image_path'] ?? null; if ($imagePath) { // Validate image path before constructing URL if (!empty(trim($imagePath))) { @@ -667,7 +698,7 @@ class StashSyncService extends BaseSyncService $imageUrl = "{$this->baseUrl}" . $imagePath; } else { // Relative path - assume it's in performer images directory - $imageUrl = "{$this->baseUrl}/performer/" . $imagePath; + $imageUrl = "{$this->baseUrl}/performer/" . $performer['id'] . "/" . $imagePath; } // Validate the constructed URL @@ -690,17 +721,56 @@ class StashSyncService extends BaseSyncService } } + if ($existingActor) { + // Update existing actor with new metadata if it's more complete + $existingMetadata = json_decode($existingActor['metadata'] ?? '{}', true); + + // Check if we should update - prefer more complete data + $shouldUpdate = false; + if (empty($existingMetadata['stash_id']) && !empty($actorMetadata['stash_id'])) { + $shouldUpdate = true; + } elseif (!empty($thumbnailPath) && empty($existingActor['thumbnail_path'])) { + $shouldUpdate = true; + } + + if ($shouldUpdate) { + $stmt = $this->pdo->prepare(" + UPDATE actors + SET thumbnail_path = COALESCE(:thumbnail_path, thumbnail_path), + metadata = :metadata, + updated_at = NOW() + WHERE id = :id + "); + $stmt->execute([ + 'id' => $existingActor['id'], + 'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path'], + 'metadata' => json_encode(array_merge($existingMetadata, $actorMetadata)) + ]); + $this->logProgress("Updated existing actor {$name} with Stash metadata"); + } + + return [ + 'id' => $existingActor['id'], + 'name' => $existingActor['name'], + 'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path'] + ]; + } + + // Create new actor with full metadata try { $stmt = $this->pdo->prepare(" - INSERT INTO actors (name, thumbnail_path, created_at, updated_at) - VALUES (:name, :thumbnail_path, NOW(), NOW()) + INSERT INTO actors (name, thumbnail_path, metadata, created_at, updated_at) + VALUES (:name, :thumbnail_path, :metadata, NOW(), NOW()) "); $stmt->execute([ 'name' => $name, - 'thumbnail_path' => $thumbnailPath + 'thumbnail_path' => $thumbnailPath, + 'metadata' => json_encode($actorMetadata) ]); $actorId = $this->pdo->lastInsertId(); + $this->logProgress("Created new actor {$name} with full Stash metadata"); + return [ 'id' => $actorId, 'name' => $name, @@ -712,6 +782,55 @@ class StashSyncService extends BaseSyncService } } + private function extractCupSize(string $measurements): ?string + { + if (empty($measurements)) return null; + + // Try to extract cup size from measurements like "34C-24-35" + $parts = explode('-', $measurements); + if (count($parts) >= 1) { + $firstPart = trim($parts[0]); + // Look for cup size pattern (number followed by letter) + if (preg_match('/(\d+)([A-Z])/', $firstPart, $matches)) { + return $matches[2]; + } + } + + return null; + } + + private function extractDebutYear(string $careerLength): ?string + { + if (empty($careerLength)) return null; + + // Extract debut year from patterns like "2015 -" or "2015 - 2020" + if (preg_match('/(\d{4})\s*-\s*(\d{4})?/', $careerLength, $matches)) { + return $matches[1]; + } + + return null; + } + + private function extractRetirementYear(string $careerLength): ?string + { + if (empty($careerLength)) return null; + + // Extract retirement year from patterns like "2015 - 2020" + if (preg_match('/\d{4}\s*-\s*(\d{4})/', $careerLength, $matches)) { + return $matches[1]; + } + + return null; + } + + private function isActivePerformer(string $careerLength): bool + { + if (empty($careerLength)) return false; + + // Check if career is still active (ends with " -") + return str_ends_with(trim($careerLength), '-'); + } + protected function getProcessedCount(): int { return $this->processedCount; diff --git a/app/Services/XbvrSyncService.php b/app/Services/XbvrSyncService.php index e6dbd91..39fbe52 100644 --- a/app/Services/XbvrSyncService.php +++ b/app/Services/XbvrSyncService.php @@ -386,10 +386,16 @@ class XbvrSyncService extends BaseSyncService { $actors = []; - foreach ($cast as $actorName) { + foreach ($cast as $actorData) { + // Handle both string names and actor objects + $actorName = is_array($actorData) ? ($actorData['name'] ?? '') : $actorData; + if (empty($actorName)) continue; - $actor = $this->getOrCreateActor($actorName); + // Try to get detailed actor information from XBVR + $detailedActorData = $this->getActorDetails($actorName, $actorData); + + $actor = $this->getOrCreateActor($detailedActorData); if ($actor) { $actors[] = $actor; } @@ -398,37 +404,326 @@ class XbvrSyncService extends BaseSyncService return $actors; } - private function getOrCreateActor(string $name): ?array + private function getActorDetails(string $actorName, $actorData): array { + // If we already have detailed actor data from the scene, use it + if (is_array($actorData) && !empty($actorData)) { + return $actorData; + } + + // Try to fetch detailed actor information from XBVR/DeoVR API + // XBVR might have actor detail endpoints, let's try a few possibilities + + $actorDetails = ['name' => $actorName]; + + // Try different XBVR actor API endpoints + $actorApiUrls = [ + "{$this->baseUrl}/api/actor/search/" . urlencode($actorName), + "{$this->baseUrl}/actor/" . urlencode($actorName), + "{$this->baseUrl}/api/actors?name=" . urlencode($actorName), + ]; + + foreach ($actorApiUrls as $apiUrl) { + try { + $this->logProgress("Trying to fetch actor details from: {$apiUrl}"); + + $response = $this->httpClient->get($apiUrl, [ + 'timeout' => 10, + 'connect_timeout' => 5 + ]); + + if ($response->getStatusCode() === 200) { + $actorApiData = json_decode($response->getBody(), true); + + if (!empty($actorApiData)) { + $this->logProgress("Successfully fetched actor details for: {$actorName}"); + + // Merge API data with basic info + $actorDetails = array_merge($actorDetails, $this->mapActorApiData($actorApiData)); + break; + } + } + } catch (Exception $e) { + // Continue to next API endpoint + $this->logProgress("Actor API endpoint failed: {$apiUrl} - " . $e->getMessage()); + } + } + + // If no detailed data found, try to scrape from web search or use basic info + if (count($actorDetails) <= 1) { + $this->logProgress("No detailed actor data found for {$actorName}, using basic info"); + $actorDetails = $this->scrapeActorInfo($actorName); + } + + return $actorDetails; + } + + private function mapActorApiData(array $apiData): array + { + $mapped = []; + + // Handle different possible API response formats + if (isset($apiData['actor'])) { + $apiData = $apiData['actor']; + } + + // Map common fields + $fieldMappings = [ + 'id' => 'xbvr_id', + 'name' => 'name', + 'image' => 'image_path', + 'thumbnail' => 'thumbnail_path', + 'bio' => 'biography', + 'biography' => 'biography', + 'birthdate' => 'birth_date', + 'age' => 'age', + 'height' => 'height', + 'weight' => 'weight', + 'measurements' => 'measurements', + 'nationality' => 'nationality', + 'ethnicity' => 'ethnicity', + 'eye_color' => 'eye_color', + 'hair_color' => 'hair_color', + 'tattoos' => 'tattoos', + 'piercings' => 'piercings', + 'aliases' => 'aliases', + 'debut_year' => 'debut_year', + 'retirement_year' => 'retirement_year', + 'active' => 'active', + 'website' => 'website', + 'twitter' => 'twitter', + 'instagram' => 'instagram', + 'scene_count' => 'scene_count' + ]; + + foreach ($fieldMappings as $apiField => $localField) { + if (isset($apiData[$apiField])) { + $mapped[$localField] = $apiData[$apiField]; + } + } + + return $mapped; + } + + private function scrapeActorInfo(string $actorName): array + { + $actorInfo = ['name' => $actorName]; + + // Try to get basic information from web scraping + // This is a fallback when API doesn't provide details + + try { + // Try to search for actor on common adult industry sites + $searchUrls = [ + "https://www.adultempire.com/search.php?query=" . urlencode($actorName), + "https://www.brazzers.com/search/" . urlencode($actorName) . "/", + "https://www.naughtyamerica.com/search/" . urlencode($actorName), + ]; + + foreach ($searchUrls as $searchUrl) { + try { + $response = $this->httpClient->get($searchUrl, [ + 'timeout' => 5, + 'headers' => [ + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + ] + ]); + + if ($response->getStatusCode() === 200) { + $html = $response->getBody()->getContents(); + + // Basic HTML parsing to extract information + $actorInfo = array_merge($actorInfo, $this->parseActorHtml($html, $actorName)); + break; + } + } catch (Exception $e) { + continue; + } + } + } catch (Exception $e) { + $this->logProgress("Web scraping failed for {$actorName}: " . $e->getMessage()); + } + + return $actorInfo; + } + + private function parseActorHtml(string $html, string $actorName): array + { + $info = []; + + // Very basic HTML parsing - look for common patterns + // This is quite fragile and would need improvement for production use + + // Look for image URLs + if (preg_match('/]+src=["\']([^"\']*?(?:actor|performer|model)[^"\']*?)["\'][^>]*>/i', $html, $matches)) { + $info['image_path'] = $matches[1]; + } + + // Look for birthdate patterns + if (preg_match('/(?:born|birthdate|birth).*?(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{4})/i', $html, $matches)) { + $info['birth_date'] = date('Y-m-d', strtotime($matches[1])); + } + + // Look for age + if (preg_match('/age.*?(\d+)/i', $html, $matches)) { + $info['age'] = (int)$matches[1]; + } + + // Look for measurements + if (preg_match('/measurements?.*?(\d+-\d+-\d+)/i', $html, $matches)) { + $info['measurements'] = $matches[1]; + } + + // Look for height + if (preg_match('/height.*?(\d+\'?\d*)/i', $html, $matches)) { + $info['height'] = $matches[1]; + } + + return $info; + } + + private function getOrCreateActor(array $actorData): ?array + { + $name = $actorData['name'] ?? ''; + if (empty($name)) return null; + // Check if actor already exists $stmt = $this->pdo->prepare(" - SELECT id, name, thumbnail_path FROM actors WHERE name = :name + SELECT id, name, thumbnail_path, metadata FROM actors WHERE name = :name "); $stmt->execute(['name' => $name]); $existingActor = $stmt->fetch(\PDO::FETCH_ASSOC); + // Prepare metadata from XBVR actor data + $actorMetadata = [ + 'xbvr_id' => $actorData['xbvr_id'] ?? $actorData['id'] ?? null, + 'biography' => $actorData['biography'] ?? null, + 'birth_date' => $actorData['birth_date'] ?? null, + 'age' => $actorData['age'] ?? null, + 'height' => $actorData['height'] ?? null, + 'weight' => $actorData['weight'] ?? null, + 'measurements' => $actorData['measurements'] ?? null, + 'nationality' => $actorData['nationality'] ?? null, + 'ethnicity' => $actorData['ethnicity'] ?? null, + 'eye_color' => $actorData['eye_color'] ?? null, + 'hair_color' => $actorData['hair_color'] ?? null, + 'tattoos' => $actorData['tattoos'] ?? null, + 'piercings' => $actorData['piercings'] ?? null, + 'aliases' => is_array($actorData['aliases'] ?? null) ? $actorData['aliases'] : [], + 'debut_year' => $actorData['debut_year'] ?? null, + 'retirement_year' => $actorData['retirement_year'] ?? null, + 'active' => $actorData['active'] ?? null, + 'scene_count' => $actorData['scene_count'] ?? null, + 'social_media' => [ + 'website' => $actorData['website'] ?? null, + 'twitter' => $actorData['twitter'] ?? null, + 'instagram' => $actorData['instagram'] ?? null + ], + 'adult_specific' => [ + 'debut_year' => $actorData['debut_year'] ?? null, + 'retirement_year' => $actorData['retirement_year'] ?? null, + 'active' => $actorData['active'] ?? null, + 'genres' => [], + 'specialties' => [] + ] + ]; + + // Try to download actor image if available + $thumbnailPath = null; + $imagePath = $actorData['image_path'] ?? $actorData['thumbnail_path'] ?? null; + + if ($imagePath) { + // Validate image path before constructing URL + if (!empty(trim($imagePath))) { + try { + // Handle different image path formats + if (strpos($imagePath, 'http') === 0) { + // Already a full URL + $imageUrl = $imagePath; + } elseif (strpos($imagePath, '/') === 0) { + // Absolute path from XBVR + $imageUrl = rtrim($this->baseUrl, '/') . $imagePath; + } else { + // Relative path + $imageUrl = rtrim($this->baseUrl, '/') . '/' . $imagePath; + } + + // Validate the constructed URL + if (filter_var($imageUrl, FILTER_VALIDATE_URL)) { + $this->logProgress("Actor image URL for {$name}: " . $imageUrl); + $thumbnailFilename = $this->imageDownloader->generateFilename($imageUrl, 'actor'); + $localImagePath = $this->imageDownloader->downloadImage($imageUrl, $thumbnailFilename, 'actors'); + if ($localImagePath) { + $thumbnailPath = $this->imageDownloader->getPublicUrl($localImagePath); + $this->logProgress("Downloaded actor image: " . $localImagePath); + } else { + $this->logProgress("Failed to download actor image from: " . $imageUrl); + } + } else { + $this->logProgress("Invalid actor image URL constructed: " . $imageUrl); + } + } catch (Exception $e) { + $this->logProgress("Exception downloading actor image for {$name} from {$imagePath}: " . $e->getMessage()); + } + } + } + if ($existingActor) { + // Update existing actor with new metadata if it's more complete + $existingMetadata = json_decode($existingActor['metadata'] ?? '{}', true); + + // Check if we should update - prefer more complete data + $shouldUpdate = false; + if (empty($existingMetadata['xbvr_id']) && !empty($actorMetadata['xbvr_id'])) { + $shouldUpdate = true; + } elseif (!empty($thumbnailPath) && empty($existingActor['thumbnail_path'])) { + $shouldUpdate = true; + } elseif (count($existingMetadata) < count(array_filter($actorMetadata))) { + $shouldUpdate = true; + } + + if ($shouldUpdate) { + $stmt = $this->pdo->prepare(" + UPDATE actors + SET thumbnail_path = COALESCE(:thumbnail_path, thumbnail_path), + metadata = :metadata, + updated_at = NOW() + WHERE id = :id + "); + $stmt->execute([ + 'id' => $existingActor['id'], + 'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path'], + 'metadata' => json_encode(array_merge($existingMetadata, $actorMetadata)) + ]); + $this->logProgress("Updated existing actor {$name} with XBVR metadata"); + } + return [ 'id' => $existingActor['id'], 'name' => $existingActor['name'], - 'thumbnail_path' => $existingActor['thumbnail_path'] + 'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path'] ]; } - // For now, we'll create actor without thumbnail - // In a full implementation, you'd fetch actor details from XBVR API + // Create new actor with XBVR metadata try { $stmt = $this->pdo->prepare(" - INSERT INTO actors (name, created_at, updated_at) - VALUES (:name, NOW(), NOW()) + INSERT INTO actors (name, thumbnail_path, metadata, created_at, updated_at) + VALUES (:name, :thumbnail_path, :metadata, NOW(), NOW()) "); - $stmt->execute(['name' => $name]); + $stmt->execute([ + 'name' => $name, + 'thumbnail_path' => $thumbnailPath, + 'metadata' => json_encode($actorMetadata) + ]); $actorId = $this->pdo->lastInsertId(); + $this->logProgress("Created new actor {$name} with XBVR metadata"); + return [ 'id' => $actorId, 'name' => $name, - 'thumbnail_path' => null + 'thumbnail_path' => $thumbnailPath ]; } catch (Exception $e) { $this->logProgress("Failed to create actor {$name}: " . $e->getMessage()); @@ -498,7 +793,7 @@ class XbvrSyncService extends BaseSyncService $this->logProgress("Starting cleanup - detecting deleted VR scenes in XBVR..."); // Clean up VR scenes - $this->cleanupScenes(); + //$this->cleanupScenes(); $this->logProgress("Cleanup completed. Deleted {$this->deletedCount} VR scenes."); } @@ -545,20 +840,41 @@ class XbvrSyncService extends BaseSyncService private function getXbvrScenesForCleanup(): array { try { - $response = $this->httpClient->get("{$this->baseUrl}/api/scene/list", [ - 'timeout' => 30, - 'connect_timeout' => 10 - ]); + // Use the same DeoVR API as the main sync process to ensure consistency + $scenes = $this->getXbvrScenes(); - if ($response->getStatusCode() === 200) { - $data = json_decode($response->getBody(), true); - return $data['scenes'] ?? []; + // Extract scene IDs from the detailed scene data + $sceneIds = array_map(function($scene) { + return $scene['id'] ?? null; + }, $scenes); + + // Filter out null IDs + return array_filter($sceneIds, function($id) { + return $id !== null; + }); + + } catch (Exception $e) { + $this->logProgress("Error fetching XBVR scenes for cleanup: " . $e->getMessage()); + $this->logProgress("Skipping cleanup to prevent accidental data loss"); + + // Return all current scene IDs to prevent deletion + // This is safer than returning empty array which would delete everything + $stmt = $this->pdo->prepare(" + SELECT metadata FROM adult_videos WHERE source_id = :source_id + "); + $stmt->execute(['source_id' => $this->source['id']]); + $localScenes = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + $existingIds = []; + foreach ($localScenes as $scene) { + $metadata = json_decode($scene['metadata'], true); + if (isset($metadata['xbvr_id'])) { + $existingIds[] = $metadata['xbvr_id']; + } } - return []; - } catch (Exception $e) { - $this->logProgress("Error fetching XBVR scenes: " . $e->getMessage()); - return []; + $this->logProgress("Returning " . count($existingIds) . " existing scene IDs to prevent cleanup"); + return $existingIds; } } diff --git a/package.json b/package.json index f37980b..5cb80ea 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ }, "dependencies": { "alpinejs": "^3.12.0", - "axios": "^1.4.0", - "bootstrap": "^5.3.0" + "axios": "^1.4.0" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/css/app.css b/public/css/app.css index b723179..c856919 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -1,118 +1,71 @@ - .items, - .item { - flex-flow: row wrap; - } - .items .item, - .item .item { - margin: 20px; - width: 120px; - height: 180px; - overflow: hidden; - box-shadow: 0 5px 10px rgba(0,0,0,0.8); - transform-origin: center top; - transform-style: preserve-3d; - transform: translateZ(0); - transition: 0.3s; - } - .items .item img, - .item .item img { - width: 100%; - min-height: 100%; - } - .items .item figcaption, - .item .item figcaption { - bottom: 0; - left: 0; - right: 0; - padding: 20px; - padding-bottom: 10px; - font-size: 20px; - background: none; - color: #fff; - transform: translateY(100%); - transition: 0.3s; - } - .items .item:after, - .item .item:after { - content: ''; - z-index: 10; - width: 200%; - height: 100%; - top: -90%; - left: -20px; - opacity: 0.1; - transform: rotate(45deg); - background: linear-gradient(to top, transparent, #fff 15%, rgba(255,255,255,0.5)); - transition: 0.3s; - } - .items .item:hover, - .item .item:hover, - .items .item:focus, - .item .item:focus, - .items .item:active, - .item .item:active { - box-shadow: 0 8px 16px 3px rgba(0,0,0,0.6); - transform: translateY(-3px) scale(1.05) rotateX(15deg); - } - .items .item:hover figcaption, - .item .item:hover figcaption, - .items .item:focus figcaption, - .item .item:focus figcaption, - .items .item:active figcaption, - .item .item:active figcaption { - transform: none; - } - .items .item:hover:after, - .item .item:hover:after, - .items .item:focus:after, - .item .item:focus:after, - .items .item:active:after, - .item .item:active:after { - transform: rotate(25deg); - top: -40%; - opacity: 0.15; - } - .item .article { - overflow: hidden; - width: 350px; - height: 235px; - margin: 20px; - } - .item .article img { - width: 100%; - min-height: 100%; - transition: 0.2s; - } - .item .article figcaption { - font-size: 14px; - text-shadow: 0 1px 0 rgba(51,51,51,0.3); - color: #fff; - left: 0; - right: 0; - top: 0; - bottom: 0; - padding: 40px; - box-shadow: 0 0 2px rgba(0,0,0,0.2); - background: rgba(6,18,53,0.6); - opacity: 0; - transform: scale(1.15); - transition: 0.2s; - } - .item .article figcaption h3 { - color: #3792e3; - font-size: 16px; - margin-bottom: 0; - font-weight: bold; - } - .item .article:hover img, - .item .article:focus img, - .item .article:active img { - filter: blur(3px); - transform: scale(0.97); - } - .item .article:hover figcaption, - .item .article:focus figcaption, - .item .article:active figcaption { - opacity: 1; - transform: none; - } \ No newline at end of file +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Custom styles for media cards */ +.items { + @apply flex flex-wrap; +} + +.item { + @apply m-5 w-32 h-48 overflow-hidden shadow-lg transform-gpu transition-transform duration-300 origin-top; +} + +.item img { + @apply w-full min-h-full; +} + +.item figcaption { + @apply absolute bottom-0 left-0 right-0 p-5 pb-2.5 text-xl bg-transparent text-white transform translate-y-full transition-transform duration-300; +} + +.item::after { + content: ''; + @apply absolute z-10 w-full h-full -top-9/10 -left-5 opacity-10 rotate-45 bg-gradient-to-t from-transparent via-white/15 to-white/50 transition-all duration-300; +} + +.item:hover, +.item:focus, +.item:active { + @apply shadow-2xl -translate-y-1 scale-105 rotate-x-15; +} + +.item:hover figcaption, +.item:focus figcaption, +.item:active figcaption { + @apply translate-y-0; +} + +.item:hover::after, +.item:focus::after, +.item:active::after { + @apply rotate-25 -top-2/5 opacity-15; +} + +.article { + @apply overflow-hidden w-80 h-56 m-5; +} + +.article img { + @apply w-full min-h-full transition-all duration-200; +} + +.article figcaption { + @apply absolute inset-0 p-10 text-sm text-white bg-blue-900/60 opacity-0 scale-115 transition-all duration-200 shadow-sm; +} + +.article figcaption h3 { + @apply text-blue-300 text-base mb-0 font-bold; +} + +.article:hover img, +.article:focus img, +.article:active img { + @apply blur-sm scale-95; +} + +.article:hover figcaption, +.article:focus figcaption, +.article:active figcaption { + @apply opacity-100 scale-100; +} diff --git a/public/index.php b/public/index.php index dc764c6..74e61c2 100644 --- a/public/index.php +++ b/public/index.php @@ -31,6 +31,7 @@ use Slim\Views\TwigMiddleware; use DI\Container; use Twig\TwigFunction; use Twig\TwigFilter; +use \Twig\Extension\DebugExtension; // Create DI Container $container = new Container(); @@ -46,6 +47,7 @@ $container->set('view', function () use ($container) { 'cache' => $_ENV['APP_ENV'] === 'production' ? __DIR__ . '/../storage/views' : false, 'debug' => $_ENV['APP_DEBUG'] === 'true', ]); + $twig->addExtension(new \Twig\Extension\DebugExtension()); // Add custom functions $twig->getEnvironment()->addFunction(new TwigFunction('base_url', function () { @@ -69,6 +71,9 @@ $container->set('view', function () use ($container) { // Handle common route patterns switch ($name) { case 'home': + $basePath = '/'; + break; + case 'dashboard.index': $basePath = '/'; break; case 'games.index': @@ -125,6 +130,9 @@ $container->set('view', function () use ($container) { case 'actors.show': $basePath = '/media/actors/' . ($data['id'] ?? ''); break; + case 'actors.edit': + $basePath = '/media/actors/' . ($data['id'] ?? '') .'/edit'; + break; case 'search.index': $basePath = '/search'; break; diff --git a/resources/views/actor/edit.twig b/resources/views/actor/edit.twig new file mode 100644 index 0000000..1458645 --- /dev/null +++ b/resources/views/actor/edit.twig @@ -0,0 +1,280 @@ +{% extends "layouts/app.twig" %} + +{% block content %} +
+ +
+
+
+

Edit Actor

+

Update actor information and metadata

+
+ + Cancel + +
+
+ + {% if error %} +
+
+
+ + + +
+
+

Error

+
+ {{ error }} +
+
+
+
+ {% endif %} + + +
+
+

Basic Information

+
+ +
+ +
+ + +
+ + + {% if actor.thumbnail_path %} +
+ +
+ {{ actor.name }} +
+ Current actor image. Upload a new image below to replace it. +
+
+
+ {% endif %} + + +
+ + +

Supported formats: JPEG, PNG, GIF, WebP. Maximum file size: 5MB.

+
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+

Physical Attributes

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

Body Modifications

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +

Separate multiple aliases with commas

+
+
+ + +
+

Social Media

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

Adult Industry Information

+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +

Genres this performer specializes in

+
+
+ + +

Specific skills or specialties

+
+
+
+
+ + +
+ + Cancel + + +
+
+
+{% endblock %} diff --git a/resources/views/actor/index.twig b/resources/views/actor/index.twig index ad22f7b..686cabe 100644 --- a/resources/views/actor/index.twig +++ b/resources/views/actor/index.twig @@ -1,68 +1,346 @@ {% extends "layouts/app.twig" %} {% block content %} -
-
-
-

Performers

-

{{ actors|length }} performer{{ actors|length != 1 ? 's' : '' }}

+ +
+
+
+ + +
+
+

Actors & Performers

+

{{ pagination.total_items }} performer{{ pagination.total_items != 1 ? 's' : '' }}

+ + + {% if pagination.total_pages > 0 %} +
+ + {% if pagination.has_prev %} + + + + + Prev + + {% else %} + + + + + Prev + + {% endif %} + + + + Page {{ pagination.current_page }} of {{ pagination.total_pages }} + + + + {% if pagination.has_next %} + + Next + + + + + {% else %} + + Next + + + + + {% endif %} +
+ {% endif %} +
-
- {% if actors %} -
- {% for actor in actors %} +
-
- - -
{{ actor.name }}
-
-
- - - - +
+ +
+
+ +
+
+ +
+
+ +
- {% endif %} + +
+
-
{{ actor.name }}
-

- {{ actor.total_media_count }} scene{{ actor.total_media_count != 1 ? 's' : '' }} -

- - {% if actor.latest_scene_date %} - - Latest: {{ actor.latest_scene_date|date('M j, Y') }} - - {% endif %} - + +
+ +
-
- {% endfor %} -
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+ + + + + + Clear Filters + +
+
+ + {% if actors %} + +
+
+
+ + + +
+
{{ actors|length }}
+
Total Performers
+
+ + {% set totalMovies = 0 %} + {% set totalShows = 0 %} + {% set totalAdult = 0 %} + {% for actor in actors %} + {% set totalMovies = totalMovies + actor.movie_count %} + {% set totalShows = totalShows + actor.tv_show_count %} + {% set totalAdult = totalAdult + actor.adult_video_count %} + {% endfor %} + +
+
+ + + +
+
{{ totalMovies }}
+
Movies
+
+ +
+
+ + + +
+
{{ totalShows }}
+
TV Shows
+
+ +
+
+ + + +
+
{{ totalAdult }}
+
Adult Videos
+
+
+ + + + + + {% if pagination.total_pages > 1 %} +
+
+ Showing {{ (pagination.current_page - 1) * pagination.per_page + 1 }} to {{ min(pagination.current_page * pagination.per_page, pagination.total_items) }} of {{ pagination.total_items }} performers +
+ +
+ + {% if pagination.has_prev %} + + + + + Previous + + {% else %} + + + + + Previous + + {% endif %} + + + {% set start_page = max(1, pagination.current_page - 2) %} + {% set end_page = min(pagination.total_pages, pagination.current_page + 2) %} + + {% if start_page > 1 %} + 1 + {% if start_page > 2 %} + ... + {% endif %} + {% endif %} + + {% for page_num in start_page..end_page %} + {% if page_num == pagination.current_page %} + {{ page_num }} + {% else %} + {{ page_num }} + {% endif %} + {% endfor %} + + {% if end_page < pagination.total_pages %} + {% if end_page < pagination.total_pages - 1 %} + ... + {% endif %} + {{ pagination.total_pages }} + {% endif %} + + + {% if pagination.has_next %} + + Next + + + + + {% else %} + + Next + + + + + {% endif %} +
+
+ {% endif %} {% else %} -
- - - -
No performers found
-

Performers will appear here once you sync content from your adult video sources.

-
+ +
+
+ + + +
+

No Performers Found

+

Performers will appear here once you sync content from your media sources. Import movies, TV shows, or adult videos to start building your actor database.

+
{% endif %}
{% endblock %} diff --git a/resources/views/actor/show.twig b/resources/views/actor/show.twig index 75af231..733f6be 100644 --- a/resources/views/actor/show.twig +++ b/resources/views/actor/show.twig @@ -1,113 +1,280 @@ {% extends "layouts/app.twig" %} -{% block content %} -
- -
- - - +{% block sidebar %} +{% if actor.metadata %} +{% set metadata = actor.metadata|json_decode %} +
+ + {% if metadata.biography or metadata.birth_date or metadata.birth_place or metadata.nationality %} + - -
-
- -
-
-
- {% if actor.thumbnail_path %} - {{ actor.name }} - {% else %} -
- - - -
- {% endif %} -

{{ actor.name }}

-

{{ actor.scene_count }} scene{{ actor.scene_count != 1 ? 's' : '' }}

-
-
+ Personal Information + +
+ {% if metadata.birth_date %} +
+ Birth Date + {{ metadata.birth_date }}
+ {% endif %} + {% if metadata.birth_place %} +
+ Birth Place + {{ metadata.birth_place }} +
+ {% endif %} + {% if metadata.nationality %} +
+ Nationality + {{ metadata.nationality }} +
+ {% endif %} + {% if metadata.death_date %} +
+ Death Date + {{ metadata.death_date }} +
+ {% endif %} +
+
+ {% endif %} - -
-
- -
-
-
- - - -
-
{{ actor.scene_count }}
- Total Scenes -
-
-
+ + {% if metadata.gender or metadata.height or metadata.weight or metadata.hair_color or metadata.eye_color %} +
+

+ + + + Physical Attributes +

+
+ {% if metadata.gender %} +
+ Gender + {{ metadata.gender }} +
+ {% endif %} + {% if metadata.height %} +
+ Height + {{ metadata.height }} +
+ {% endif %} + {% if metadata.weight %} +
+ Weight + {{ metadata.weight }} +
+ {% endif %} + {% if metadata.hair_color %} +
+ Hair Color + {{ metadata.hair_color }} +
+ {% endif %} + {% if metadata.eye_color %} +
+ Eye Color + {{ metadata.eye_color }} +
+ {% endif %} + {% if metadata.ethnicity %} +
+ Ethnicity + {{ metadata.ethnicity }} +
+ {% endif %} +
+
+ {% endif %} - {% if actor.scene_count > 0 %} -
-
- - - -
-
{{ actor.latest_scene_date|date('M j, Y') }}
- Latest Scene -
-
-
- {% endif %} -
+ + {% if metadata.measurements or metadata.cup_size or metadata.fake_tits or metadata.penis_length or metadata.circumcised %} +
+

+ + + + Adult Attributes +

+
+ {% if metadata.measurements %} +
+ Measurements + {{ metadata.measurements }} +
+ {% endif %} + {% if metadata.cup_size %} +
+ Cup Size + {{ metadata.cup_size }} +
+ {% endif %} + {% if metadata.fake_tits %} +
+ Fake Tits + {{ metadata.fake_tits }} +
+ {% endif %} + {% if metadata.penis_length %} +
+ Penis Length + {{ metadata.penis_length }} +
+ {% endif %} + {% if metadata.circumcised %} +
+ Circumcised + {{ metadata.circumcised }} +
+ {% endif %} +
+
+ {% endif %} - - {% if scenes %} -
-

Scenes featuring {{ actor.name }}

-
- {% for scene in scenes %} -
-
-
- {% if scene.poster_url %} - {{ scene.title }} - {% else %} -
- - - -
- {% endif %} -
-
-
- {{ scene.title }} -
-

- {{ scene.release_date|date('M j, Y') }} - {% if scene.runtime_minutes %} - • {{ (scene.runtime_minutes / 60)|round(1) }}h {{ scene.runtime_minutes % 60 }}m - {% endif %} -

- {{ scene.source_name }} -
-
-
- {% endfor %} -
-
+ + {% if metadata.career_length or metadata.scene_count %} +
+

+ + + + Career Information +

+
+ {% if metadata.career_length %} +
+ Career Length + {{ metadata.career_length }} +
+ {% endif %} + {% if metadata.scene_count %} +
+ Scene Count + {{ metadata.scene_count }} +
+ {% endif %} + {% if metadata.adult_specific.debut_year %} +
+ Debut Year + {{ metadata.adult_specific.debut_year }} +
+ {% endif %} + {% if metadata.adult_specific.retirement_year %} +
+ Retirement Year + {{ metadata.adult_specific.retirement_year }} +
+ {% endif %} + {% if metadata.adult_specific.active is defined %} +
+ Active + {{ metadata.adult_specific.active ? 'Yes' : 'No' }} +
+ {% endif %} +
+
+ {% endif %} + + + {% if metadata.tattoos or metadata.piercings %} +
+

+ + + + Body Modifications +

+
+ {% if metadata.tattoos %} +
+

Tattoos

+

{{ metadata.tattoos }}

+
+ {% endif %} + {% if metadata.piercings %} +
+

Piercings

+

{{ metadata.piercings }}

+
+ {% endif %} +
+
+ {% endif %} +
+ {% endif %} +{% endblock %} + +{% block content %} + +
+
+
+ + + + + +
+
+ +
+ {% if actor.thumbnail_path %} + {{ actor.name }} {% else %} -
- +
+ {{ actor.name|first|upper }} +
+ {% endif %} +
+ + +

{{ actor.name }}

+ + +
+ {% if actor.movie_count > 0 %} +
+ + + + {{ actor.movie_count }} Movie{{ actor.movie_count != 1 ? 's' : '' }} +
+ {% endif %} + + {% if actor.tv_show_count > 0 %} +
+ -
No scenes found
-

This performer hasn't appeared in any scenes yet.

+ {{ actor.tv_show_count }} TV Show{{ actor.tv_show_count != 1 ? 's' : '' }} +
+ {% endif %} + + {% if actor.adult_video_count > 0 %} +
+ + + + {{ actor.adult_video_count }} Adult Video{{ actor.adult_video_count != 1 ? 's' : '' }}
{% endif %}
@@ -115,4 +282,171 @@
-{% endblock %} + + +
+ +
+ {% if actor.movie_count > 0 %} +
+
+ + + +
+
{{ actor.movie_count }}
+
Movies
+
+ {% endif %} + + {% if actor.tv_show_count > 0 %} +
+
+ + + +
+
{{ actor.tv_show_count }}
+
TV Shows
+
+ {% endif %} + + {% if actor.adult_video_count > 0 %} +
+
+ + + +
+
{{ actor.adult_video_count }}
+
Adult Videos
+
+ {% endif %} +
+ + + {% if movies %} + + {% endif %} + + + {% if tv_shows %} + + {% endif %} + + + {% if scenes %} + + {% endif %} + + + {% if not movies and not tv_shows and not scenes %} +
+
+ + + +
+

No Media Found

+

This performer hasn't appeared in any movies, TV shows, or adult videos yet.

+
+ {% endif %} +
+ +{% endblock %} \ No newline at end of file diff --git a/resources/views/adult/index.twig b/resources/views/adult/index.twig index 04e9388..910d050 100644 --- a/resources/views/adult/index.twig +++ b/resources/views/adult/index.twig @@ -1,175 +1,264 @@ {% extends "layouts/app.twig" %} -{% block content %} -
-
- -
-
-
-
Filters
-
-
- -
- - - +{% block nav_controls %} + + + + + {% for genre in filters.genres %} + + {% endfor %} + {% for director in filters.directors %} + + {% endfor %} + {% for source in filters.sources %} + + {% endfor %} +
+ + + + +
+ +
- - {% if available_filters.genres %} -
-
Genres
- -
- {% endif %} + + - - {% if available_filters.directors %} -
-
Directors
- -
- {% endif %} - - -
- - - Clear All - -
- -
-
+ +
+ +
+
+ {% for key, label in sort_options %} + + {{ label }} + {% if sort == key %} + + + + {% endif %} + + {% endfor %}
+
+
+{% endblock %} - -
-
- -
-
-

Adult Videos

- {% if pagination.total_items > 0 %} -
- {{ pagination.total_items }} videos - {% if search %} - matching "{{ search }}" - {% endif %} - {% if filters.genres or filters.directors %} - {% if filters.genres %} - {{ filters.genres|join(', ') }} - {% endif %} - {% if filters.directors %} - {{ filters.directors|join(', ') }} - {% endif %} - {% endif %} -
- {% endif %} -
+{% block sidebar %} +
+ +
+

Filters

-
- -
- - - - {% for genre in filters.genres %} - - {% endfor %} - {% for director in filters.directors %} - - {% endfor %} -
- - - - -
- -
+ +
+ + + + - - + + {% if available_filters.genres %} +
+ + +
+ {% endif %} - - -
+ + {% if available_filters.directors %} +
+ + +
+ {% endif %} + + + {% if available_filters.sources %} +
+ + +
+ {% endif %} + + +
+ + + Clear All + +
+ +
+ + + {% if filters.genres or filters.directors or filters.sources or search %} +
+

Active Filters

+
+ {% if search %} +
+ Search: "{{ search }}" + + + + + +
+ {% endif %} + {% for genre in filters.genres %} +
+ Genre: {{ genre }} + + + + + +
+ {% endfor %} + {% for director in filters.directors %} +
+ Director: {{ director }} + + + + + +
+ {% endfor %} + {% for source in filters.sources %} +
+ Source: {{ source }} + + + + + +
+ {% endfor %} +
+
+ {% endif %} + + +
+

Quick Stats

+
+
+ Total Videos + {{ pagination.total_items }} +
+
+ This Page + {{ movies|length }} +
+ {% if pagination.total_pages > 1 %} +
+ Page + {{ pagination.current_page }} of {{ pagination.total_pages }} +
+ {% endif %} +
+
+
+{% endblock %} + +{% block content %} + +
+ +
+

Adult Videos

+ {% if pagination.total_items > 0 %} +
+ {{ pagination.total_items }} videos + {% if search %} + matching "{{ search }}" + {% endif %} + {% if filters.genres or filters.directors or filters.sources %} + {% if filters.genres %} + {{ filters.genres|join(', ') }} + {% endif %} + {% if filters.directors %} + {{ filters.directors|join(', ') }} + {% endif %} + {% if filters.sources %} + {{ filters.sources|join(', ') }} + {% endif %} + {% endif %} +
+ {% endif %} +
+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + +
+ Sorted by: {{ sort_options[sort] }}
{% if error %} @@ -188,20 +277,20 @@

- {% if search or filters.genres or filters.directors %} + {% if search or filters.genres or filters.directors or filters.sources %} No adult videos found matching your criteria {% else %} No adult videos found {% endif %}

- {% if search or filters.genres or filters.directors %} + {% if search or filters.genres or filters.directors or filters.sources %} Try adjusting your search terms or filters. {% else %} Adult videos will appear here after syncing with XBVR or Stash sources. {% endif %}

- {% if search or filters.genres or filters.directors %} + {% if search or filters.genres or filters.directors or filters.sources %} Clear filters @@ -211,28 +300,28 @@ {% if view_mode == 'list' %} -
-
    +
    +
      {% for movie in movies %} -
    • -
      -
      +
    • +
      +
      {% if movie.poster_url %} - {{ movie.title }} + {{ movie.title }} {% else %} -
      - +
      +
      {% endif %} -
      -

      - +
      +

      + {{ movie.title }}

      -
      +
      {% if movie.release_date %} {{ movie.release_date|date('Y') }} {% endif %} @@ -246,14 +335,14 @@
      -
      +
      {% if movie.watched %} - + Watched {% endif %} {% if movie.is_favorite %} - + Favorite {% endif %} @@ -265,32 +354,58 @@
      {% elseif view_mode == 'covers' %} - -
      + +
      {% for movie in movies %} -
      -
      - {% if movie.poster_url %} -
      - {{ movie.title }} +
      + {% if movie.poster_url %} +
      + {{ movie.title }} + +
      +
      + {% if movie.rating %} +
      + + + + {{ movie.rating }} +
      + {% endif %} + {% if movie.watched %} +
      + + + + Watched +
      + {% endif %} +
      - {% else %} -
      - - - +
      + {% else %} +
      + + + +
      + {% endif %} +
      +
      + + {{ movie.title }} + +
      + {% if movie.release_date %} +

      {{ movie.release_date|date('Y') }}

      + {% endif %} + {% if movie.genre %} +
      + {% for genre in movie.genre|split(',')|slice(0, 2) %} + {{ genre|trim }} + {% endfor %}
      {% endif %} -
      -
      - - {{ movie.title }} - -
      - {% if movie.release_date %} -

      {{ movie.release_date|date('Y') }}

      - {% endif %} -
      {% endfor %} @@ -298,67 +413,65 @@ {% else %} -
      +
      {% for movie in movies %} -
      -
      -
      -
      -
      - {% if movie.poster_url %} - {{ movie.title }} - {% else %} -
      - - - -
      - {% endif %} +
      +
      +
      +
      + {% if movie.poster_url %} + {{ movie.title }} + {% else %} +
      + + +
      -
      -
      - - {{ movie.title }} - -
      -
      - {% if movie.release_date %} - {{ movie.release_date|date('Y') }} - {% endif %} - {% if movie.rating %} - ⭐ {{ movie.rating }}/10 - {% endif %} -
      - {% if movie.source_name %} -

      - {{ movie.source_name }} -

      - {% endif %} -
      -
      - {% if movie.overview %} -
      -

      - {{ movie.overview|slice(0, 150) }}{% if movie.overview|length > 150 %}...{% endif %} -

      -
      - {% endif %} -
      - {% if movie.runtime_minutes %} - {{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m {% endif %} -
      - {% if movie.watched %} - - Watched - +
      +
      +
      + + {{ movie.title }} + +
      +
      + {% if movie.release_date %} + {{ movie.release_date|date('Y') }} {% endif %} - {% if movie.is_favorite %} - - Favorite - + {% if movie.rating %} + ⭐ {{ movie.rating }}/10 {% endif %}
      + {% if movie.source_name %} +

      + {{ movie.source_name }} +

      + {% endif %} +
      +
      + {% if movie.overview %} +
      +

      + {{ movie.overview|slice(0, 150) }}{% if movie.overview|length > 150 %}...{% endif %} +

      +
      + {% endif %} +
      + {% if movie.runtime_minutes %} + {{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m + {% endif %} +
      + {% if movie.watched %} + + Watched + + {% endif %} + {% if movie.is_favorite %} + + Favorite + + {% endif %}
      @@ -367,42 +480,89 @@
      {% endif %} - + {% if pagination.total_pages > 1 %} -
      -
      - - - per page +
      +
      +
      + Showing {{ (pagination.current_page - 1) * pagination.per_page + 1 }} to {{ min(pagination.current_page * pagination.per_page, pagination.total_items) }} of {{ pagination.total_items }} videos +
      +
      + + + per page +
      -
      +
      + {% if pagination.has_prev %} - + + + + Previous + {% else %} + + + + + Previous + {% endif %} -
      - {% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %} - - {{ page_num }} - - {% endfor %} -
      + + {% set start_page = max(1, pagination.current_page - 2) %} + {% set end_page = min(pagination.total_pages, pagination.current_page + 2) %} + {% if start_page > 1 %} + 1 + {% if start_page > 2 %} + ... + {% endif %} + {% endif %} + + {% for page_num in start_page..end_page %} + {% if page_num == pagination.current_page %} + {{ page_num }} + {% else %} + {{ page_num }} + {% endif %} + {% endfor %} + + {% if end_page < pagination.total_pages %} + {% if end_page < pagination.total_pages - 1 %} + ... + {% endif %} + {{ pagination.total_pages }} + {% endif %} + + {% if pagination.has_next %} - + Next + + + + {% else %} + + Next + + + + {% endif %}
      @@ -462,6 +622,13 @@ document.getElementById('per_page')?.addEventListener('change', function() { url.searchParams.set('page', '1'); // Reset to first page window.location = url.toString(); }); + +document.getElementById('per_page_top')?.addEventListener('change', function() { + const url = new URL(window.location); + url.searchParams.set('per_page', this.value); + url.searchParams.set('page', '1'); // Reset to first page + window.location = url.toString(); +}); {% endblock %} diff --git a/resources/views/adult/show.twig b/resources/views/adult/show.twig index e8bea85..a313621 100644 --- a/resources/views/adult/show.twig +++ b/resources/views/adult/show.twig @@ -1,372 +1,978 @@ {% extends "layouts/app.twig" %} {% block content %} -
      - - + +
      + {% if movie.backdrop_url %} +
      + {{ movie.title }} backdrop +
      +
      -
      -
      - -
      -
      -
      - {% if movie.poster_url %} - {{ movie.title }} - {% else %} -
      - - - -
      - {% endif %} -
      + + - -
      - - -
      + +
      +
      +

      {{ movie.title }}

      + {% if movie.tagline %} +

      {{ movie.tagline }}

      + {% endif %} + + +
      + {% if movie.release_date %} + + + + + {{ movie.release_date|date('Y') }} + + {% endif %} + + {% if movie.rating %} + + + + + {{ movie.rating }}/10 + + {% endif %} + + {% if movie.runtime_minutes %} + + + + + {{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m + + {% endif %} + + {% if movie.file_size %} + + + + + {{ movie.file_size|filesizeformat }} + + {% endif %}
      -
      - -
      -
      -
      -

      {{ movie.title }}

      - {% if movie.tagline %} -

      {{ movie.tagline }}

      - {% endif %} - - -
      - {% if movie.release_date %} - - - - - {{ movie.release_date|date('Y') }} - - {% endif %} - - {% if movie.rating %} - - - - - {{ movie.rating }}/10 - - {% endif %} - - {% if movie.runtime_minutes %} - - - - - {{ (movie.runtime_minutes / 60)|round(1) }}h {{ movie.runtime_minutes % 60 }}m - - {% endif %} - - {% if movie.file_size %} - - - - - {{ movie.file_size|filesizeformat }} - - {% endif %} - - - - - - {{ movie.source_name }} - -
      - - -
      - {% if movie.watched %} - - - - - Watched - - {% endif %} - - {% if movie.watch_count > 0 %} - {{ movie.watch_count }} watch{{ movie.watch_count > 1 ? 'es' : '' }} - {% endif %} - - {% if movie.is_favorite %} - - - - - Favorite - - {% endif %} - - {% if movie.collection %} - - - - - {{ movie.collection }} - - {% endif %} -
      -
      - - - {% if movie.overview %} -
      -

      Overview

      -

      {{ movie.overview }}

      -
      + +
      + {% if movie.watched %} + + + + + Watched + {% endif %} - -
      -

      Cast & Crew

      -
      - {% if movie.director %} -
      -
      - - - -
      - Director - {{ movie.director }} -
      -
      -
      - {% endif %} - - {% if movie.cast %} -
      -
      - - - -
      - Cast -
      - {% for actor in movie.cast|split(',') %} - {{ actor|trim }} - {% endfor %} -
      -
      -
      -
      - {% endif %} -
      -
      - - - {% if movie.actors %} -
      -

      Performers

      - -
      + {% if movie.is_favorite %} + + + + + Favorite + {% endif %} - -
      - - {% if movie.genre or movie.categories %} -
      -

      Categories

      -
      - {% if movie.genre %} - {% for genre in movie.genre|split(',') %} - {{ genre|trim }} - {% endfor %} - {% endif %} - {% if movie.categories %} - {% for category in movie.categories|split(',') %} - {{ category|trim }} - {% endfor %} - {% endif %} -
      -
      - {% endif %} - - - {% if movie.video_codec or movie.audio_codec or movie.resolution or movie.bitrate %} -
      -

      Technical Details

      -
      - {% if movie.video_codec %} -
      - Video Codec - {{ movie.video_codec }} -
      - {% endif %} - {% if movie.audio_codec %} -
      - Audio Codec - {{ movie.audio_codec }} -
      - {% endif %} - {% if movie.resolution %} -
      - Resolution - {{ movie.resolution }} -
      - {% endif %} - {% if movie.bitrate %} -
      - Bitrate - {{ movie.bitrate }} kbps -
      - {% endif %} - {% if movie.frame_rate %} -
      - Frame Rate - {{ movie.frame_rate }} fps -
      - {% endif %} - {% if movie.aspect_ratio %} -
      - Aspect Ratio - {{ movie.aspect_ratio }} -
      - {% endif %} -
      -
      - {% endif %} -
      - - - {% if movie.studio or movie.production_companies %} -
      -

      Production

      -
      - {% if movie.studio %} -
      - Studio - {{ movie.studio }} -
      - {% endif %} - {% if movie.production_companies %} -
      - Production Company - {{ movie.production_companies }} -
      - {% endif %} -
      -
      + {% if movie.collection %} + + + + + {{ movie.collection }} + {% endif %} +
      - - {% if movie.file_path or movie.duration or movie.created_at %} -
      -

      File Information

      -
      - {% if movie.file_path %} -
      - File Path - {{ movie.file_path }} -
      - {% endif %} - {% if movie.duration %} -
      - Duration - {{ movie.duration }} -
      - {% endif %} - {% if movie.created_at %} -
      - Added - {{ movie.created_at|date('M j, Y') }} -
      - {% endif %} -
      -
      - {% endif %} - - - {% if movie.streaming_providers or movie.availability %} -
      -

      Availability

      -
      - {% if movie.streaming_providers %} -
      - Streaming Platforms -
      - {% for provider in movie.streaming_providers %} - {{ provider }} - {% endfor %} -
      -
      - {% endif %} - {% if movie.availability %} -
      - Availability Status - {{ movie.availability }} -
      - {% endif %} -
      -
      - {% endif %} - - - {% if metadata %} -
      -
      - - - - - Technical Details & Metadata - -
      -
      {{ metadata|json_encode(constant('JSON_PRETTY_PRINT')) }}
      -
      -
      -
      - {% endif %} + +
      + +
      + {% else %} + +
      + +
      +

      {{ movie.title }}

      + {% if movie.tagline %} +

      {{ movie.tagline }}

      + {% endif %} +
      +
      + {% endif %} +
      + + +
      +
      + +
      +
      + +
      +
      + {% if movie.poster_url %} + {{ movie.title }} + {% else %} +
      + + + +
      + {% endif %} +
      +
      + + +
      +

      Quick Stats

      +
      + {% if movie.watch_count %} +
      + Watch Count + {{ movie.watch_count }} +
      + {% endif %} +
      + Source + {{ movie.source_name }} +
      + {% if movie.file_size %} +
      + File Size + {{ movie.file_size|filesizeformat }} +
      + {% endif %} +
      +
      +
      +
      + + +
      + + {% if movie.overview %} +
      +

      Overview

      +

      {{ movie.overview }}

      +
      + {% endif %} + + +
      +

      Cast & Crew

      + + +
      + {% if movie.director %} +
      +
      + + + +
      +
      +

      Director

      +

      {{ movie.director }}

      +
      +
      + {% endif %} + + {% if movie.writer %} +
      +
      + + + +
      +
      +

      Writer

      +

      {{ movie.writer }}

      +
      +
      + {% endif %} +
      + + + {% if movie.cast %} +
      +

      Cast

      +
      + {% for actor in movie.cast|split(',') %} + {{ actor|trim }} + {% endfor %} +
      +
      + {% endif %} + + + {% if movie.actors %} + + {% endif %} +
      + + +
      + + {% if movie.genre %} +
      +

      Genres

      +
      + {% for genre in movie.genre|split(',') %} + {{ genre|trim }} + {% endfor %} +
      +
      + {% endif %} + + + {% if movie.categories %} +
      +

      Categories

      +
      + {% for category in movie.categories|split(',') %} + {{ category|trim }} + {% endfor %} +
      +
      + {% endif %} + + + {% if movie.video_codec or movie.audio_codec or movie.resolution %} +
      +

      Technical

      + {% if movie.video_codec %} +
      +

      Video Codec

      +

      {{ movie.video_codec }}

      +
      + {% endif %} + {% if movie.audio_codec %} +
      +

      Audio Codec

      +

      {{ movie.audio_codec }}

      +
      + {% endif %} + {% if movie.resolution %} +
      +

      Resolution

      +

      {{ movie.resolution }}

      +
      + {% endif %} +
      + {% endif %} +
      + + + {% if movie.studio or movie.production_companies %} +
      +

      Production

      +
      + {% if movie.studio %} +
      +
      + + + +
      +
      +

      Studio

      +

      {{ movie.studio }}

      +
      +
      + {% endif %} + {% if movie.production_companies %} +
      +
      + + + +
      +
      +

      Production Company

      +

      {{ movie.production_companies }}

      +
      +
      + {% endif %} +
      +
      + {% endif %} + + + {% if metadata %} +
      +

      Technical Details & Metadata

      + + + {% if metadata.source %} +
      +

      + + + + Source Information +

      +
      + {{ metadata.source }} +
      +
      + {% endif %} + + + {% if metadata.file_path or metadata.file_size %} +
      +

      + + + + File Information +

      +
      + {% if metadata.file_path %} +
      +

      File Path

      +
      + {{ metadata.file_path }} +
      +
      + {% endif %} + {% if metadata.file_size %} +
      +

      File Size

      +

      {{ metadata.file_size|filesizeformat }}

      +
      + {% endif %} +
      +
      + {% endif %} + + + {% if metadata.video_codec or metadata.audio_codec or metadata.resolution %} +
      +

      + + + + Video Details +

      +
      + {% if metadata.video_codec %} +
      +

      Video Codec

      +

      {{ metadata.video_codec }}

      +
      + {% endif %} + {% if metadata.audio_codec %} +
      +

      Audio Codec

      +

      {{ metadata.audio_codec }}

      +
      + {% endif %} + {% if metadata.resolution %} +
      +

      Resolution

      +

      {{ metadata.resolution }}

      +
      + {% endif %} + {% if metadata.bitrate %} +
      +

      Bitrate

      +

      {{ metadata.bitrate }} kbps

      +
      + {% endif %} +
      +
      + {% endif %} + + {{ dump(movie.metadata.source)}} + + + {% if movie.metadata.source == 'stash' %} + + {% if movie.metadata.stash_id %} +
      +

      + + + + Stash Information +

      +
      +
      +

      Stash ID

      +

      {{ metadata.stash_id }}

      +
      + {% if metadata.stash_url %} +
      +

      Stash URL

      + {{ metadata.stash_url }} +
      + {% endif %} + {% if metadata.organized is defined %} +
      +

      Organized

      +

      {{ metadata.organized ? 'Yes' : 'No' }}

      +
      + {% endif %} + {% if metadata.o_counter is defined %} +
      +

      Organization Counter

      +

      {{ metadata.o_counter }}

      +
      + {% endif %} +
      +
      + {% endif %} + + + {% if metadata.performers %} +
      +

      + + + + Performers +

      +
      + {% for performer in metadata.performers %} +
      +
      + {% if performer.image_path %} + {{ performer.name }} + {% else %} +
      + {{ performer.name|first|upper }} +
      + {% endif %} +
      +

      {{ performer.name }}

      + {% if performer.gender %} +

      {{ performer.gender }}

      + {% endif %} +
      +
      +
      + {% if performer.birthdate %} +
      + Birthdate: + {{ performer.birthdate }} +
      + {% endif %} + {% if performer.ethnicity %} +
      + Ethnicity: + {{ performer.ethnicity }} +
      + {% endif %} + {% if performer.country %} +
      + Country: + {{ performer.country }} +
      + {% endif %} + {% if performer.height_cm %} +
      + Height: + {{ performer.height_cm }}cm +
      + {% endif %} + {% if performer.measurements %} +
      + Measurements: + {{ performer.measurements }} +
      + {% endif %} + {% if performer.career_length %} +
      + Career: + {{ performer.career_length }} +
      + {% endif %} +
      +
      + {% endfor %} +
      +
      + {% endif %} + + + {% if metadata.file_info %} +
      +

      + + + + File Information +

      +
      + {% if metadata.file_info.size %} +
      +

      File Size

      +

      {{ metadata.file_info.size|filesizeformat }}

      +
      + {% endif %} + {% if metadata.file_info.duration %} +
      +

      Duration

      +

      {{ (metadata.file_info.duration / 3600)|round(1) }}h {{ ((metadata.file_info.duration % 3600) / 60)|round(0) }}m

      +
      + {% endif %} + {% if metadata.file_info.video_codec %} +
      +

      Video Codec

      +

      {{ metadata.file_info.video_codec }}

      +
      + {% endif %} + {% if metadata.file_info.audio_codec %} +
      +

      Audio Codec

      +

      {{ metadata.file_info.audio_codec }}

      +
      + {% endif %} + {% if metadata.file_info.width and metadata.file_info.height %} +
      +

      Resolution

      +

      {{ metadata.file_info.width }}x{{ metadata.file_info.height }}

      +
      + {% endif %} +
      +
      + {% endif %} + + + {% if metadata.paths %} +
      +

      + + + + Media Paths +

      +
      + {% if metadata.paths.stream %} +
      +

      Stream URL

      +
      + {{ metadata.paths.stream }} +
      +
      + {% endif %} + {% if metadata.paths.preview %} +
      +

      Preview URL

      +
      + {{ metadata.paths.preview }} +
      +
      + {% endif %} + {% if metadata.paths.webp %} +
      +

      WebP URL

      +
      + {{ metadata.paths.webp }} +
      +
      + {% endif %} + {% if metadata.paths.vtt %} +
      +

      VTT URL

      +
      + {{ metadata.paths.vtt }} +
      +
      + {% endif %} + {% if metadata.paths.caption %} +
      +

      Caption URL

      +
      + {{ metadata.paths.caption }} +
      +
      + {% endif %} + {% if metadata.paths.funscript %} +
      +

      Funscript URL

      +
      + {{ metadata.paths.funscript }} +
      +
      + {% endif %} + {% if metadata.paths.sprite %} +
      +

      Sprite URL

      +
      + {{ metadata.paths.sprite }} +
      +
      + {% endif %} +
      +
      + {% endif %} + + + {% if metadata.cover_url or metadata.screenshot_url %} +
      +

      + + + + Image URLs +

      +
      + {% if metadata.cover_url %} +
      +

      Cover URL

      +
      + {{ metadata.cover_url }} +
      +
      + {% endif %} + {% if metadata.screenshot_url %} +
      +

      Screenshot URL

      +
      + {{ metadata.screenshot_url }} +
      +
      + {% endif %} + {% if metadata.local_cover_path %} +
      +

      Local Cover Path

      +
      + {{ metadata.local_cover_path }} +
      +
      + {% endif %} + {% if metadata.local_screenshot_path %} +
      +

      Local Screenshot Path

      +
      + {{ metadata.local_screenshot_path }} +
      +
      + {% endif %} +
      +
      + {% endif %} + + + {% elseif metadata.source == 'xbvr' %} + + {% if metadata.xbvr_id %} +
      +

      + + + + XBVR Information +

      +
      +
      +

      XBVR ID

      +

      {{ metadata.xbvr_id }}

      +
      + {% if metadata.xbvr_url %} +
      +

      XBVR URL

      + {{ metadata.xbvr_url }} +
      + {% endif %} +
      +
      + {% endif %} + + + {% if metadata.cast %} +
      +

      + + + + Cast +

      +
      + {% for actor in metadata.cast %} + {{ actor }} + {% endfor %} +
      +
      + {% endif %} + + + {% if metadata.tags %} +
      +

      + + + + Tags +

      +
      + {% for tag in metadata.tags %} + {{ tag }} + {% endfor %} +
      +
      + {% endif %} + + +
      +

      + + + + Video Details +

      +
      + {% if metadata.video_length %} +
      +

      Duration

      +

      {{ (metadata.video_length / 3600)|round(1) }}h {{ ((metadata.video_length % 3600) / 60)|round(0) }}m

      +
      + {% endif %} + {% if metadata.video_width and metadata.video_height %} +
      +

      Resolution

      +

      {{ metadata.video_width }}x{{ metadata.video_height }}

      +
      + {% endif %} + {% if metadata.video_codec %} +
      +

      Video Codec

      +

      {{ metadata.video_codec }}

      +
      + {% endif %} + {% if metadata.paysite %} +
      +

      Paysite

      +

      {{ metadata.paysite }}

      +
      + {% endif %} + {% if metadata.is3d is defined %} +
      +

      3D

      +

      {{ metadata.is3d ? 'Yes' : 'No' }}

      +
      + {% endif %} + {% if metadata.deoVR_format is defined %} +
      +

      DeoVR Format

      +

      {{ metadata.deoVR_format ? 'Yes' : 'No' }}

      +
      + {% endif %} + {% if metadata.screenType %} +
      +

      Screen Type

      +

      {{ metadata.screenType }}

      +
      + {% endif %} + {% if metadata.stereoMode %} +
      +

      Stereo Mode

      +

      {{ metadata.stereoMode }}

      +
      + {% endif %} +
      +
      + + + {% if metadata.file_path %} +
      +

      + + + + File Information +

      +
      + {% if metadata.file_path %} +
      +

      File Path

      +
      + {{ metadata.file_path }} +
      +
      + {% endif %} +
      +
      + {% endif %} + + + {% if metadata.cover_url or metadata.screenshot_url %} +
      +

      + + + + Image URLs +

      +
      + {% if metadata.cover_url %} +
      +

      Cover URL

      +
      + {{ metadata.cover_url }} +
      +
      + {% endif %} + {% if metadata.screenshot_url %} +
      +

      Screenshot URL

      +
      + {{ metadata.screenshot_url }} +
      +
      + {% endif %} + {% if metadata.local_cover_path %} +
      +

      Local Cover Path

      +
      + {{ metadata.local_cover_path }} +
      +
      + {% endif %} + {% if metadata.local_screenshot_path %} +
      +

      Local Screenshot Path

      +
      + {{ metadata.local_screenshot_path }} +
      +
      + {% endif %} +
      +
      + {% endif %} + + +
      +

      + + + + Availability +

      +
      +
      +

      Available

      +

      {{ metadata.is_available ? 'Yes' : 'No' }}

      +
      +
      +

      Watched

      +

      {{ metadata.is_watched ? 'Yes' : 'No' }}

      +
      +
      +

      Watch Count

      +

      {{ metadata.watch_count }}

      +
      +
      +

      Full Access

      +

      {{ metadata.fullAccess ? 'Yes' : 'No' }}

      +
      +
      +

      Full Video Ready

      +

      {{ metadata.fullVideoReady ? 'Yes' : 'No' }}

      +
      +
      +
      + + {% else %} + + {% if metadata.poster_url or metadata.backdrop_url %} +
      +

      + + + + Image URLs +

      +
      + {% if metadata.poster_url %} +
      +

      Poster URL

      +
      + {{ metadata.poster_url }} +
      +
      + {% endif %} + {% if metadata.backdrop_url %} +
      +

      Backdrop URL

      +
      + {{ metadata.backdrop_url }} +
      +
      + {% endif %} +
      +
      + {% endif %} + {% endif %} + + +
      +
      + + + + + Full Raw Metadata + +
      +
      {{ metadata|json_encode(constant('JSON_PRETTY_PRINT')) }}
      +
      +
      +
      +
      + {% endif %} +
      +
      {% endblock %} diff --git a/resources/views/dashboard/index.twig b/resources/views/dashboard/index.twig index 7a6efa9..6e32ee3 100644 --- a/resources/views/dashboard/index.twig +++ b/resources/views/dashboard/index.twig @@ -1,330 +1,275 @@ {% extends 'layouts/app.twig' %} {% block content %} -
      -

      Dashboard

      -

      Overview of your media collection

      -
      - - {% if error %} -
      - {{ error }} +
      +
      +

      Dashboard

      +

      Overview of your media collection

      - {% endif %} - -
      - -
      -
      -
      -
      -
      - - - -
      -
      -
      -
      Total Media
      -
      -
      {{ stats.total_media|number_format }}
      -
      -
      -
      -
      -
      + {% if error %} +
      + {{ error }}
      -
      - - -
      -
      -
      -
      -
      - - - -
      -
      -
      -
      Games
      -
      -
      {{ stats.total_games|number_format }}
      -
      {{ stats.favorite_games }} favorites
      -
      -
      -
      -
      -
      -
      -
      - - -
      -
      -
      -
      -
      - - - -
      -
      -
      -
      Movies & TV
      -
      -
      - {{ (stats.total_movies + stats.total_tv_shows)|number_format }} -
      -
      {{ stats.watched_movies }} watched
      -
      -
      -
      -
      -
      -
      -
      - - -
      -
      -
      -
      -
      - - - -
      -
      -
      -
      Music
      -
      -
      {{ stats.total_music|number_format }}
      -
      {{ stats.favorite_music }} favorites
      -
      -
      -
      -
      -
      -
      -
      -
      - - -
      - -
      -
      -
      -
      -
      - - - -
      -
      -
      -
      Total Playtime
      -
      -
      - {% if stats.total_playtime %} - {{ (stats.total_playtime / 60)|round }}h - {% else %} - 0h - {% endif %} -
      -
      -
      -
      -
      -
      -
      -
      - - -
      -
      -
      -
      -
      - - - -
      -
      -
      -
      TV Episodes
      -
      -
      {{ stats.total_episodes|number_format }}
      -
      -
      -
      -
      -
      -
      -
      - - -
      -
      -
      -
      -
      - - - -
      -
      -
      -
      Sync Status
      -
      -
      - {% if sync_stats.successful_syncs > 0 %} - {{ sync_stats.successful_syncs }}/{{ sync_stats.total_syncs }} Success - {% else %} - No syncs yet - {% endif %} -
      -
      -
      -
      -
      -
      -
      -
      -
      - - -
      -

      Recent Activity

      - - - {% if recent_games %} -
      -

      Recently Played Games

      -
      -
        - {% for game in recent_games %} -
      • -
        -
        - {% if game.image_url %} - - {% else %} -
        - - - -
        - {% endif %} -
        -

        {{ game.title }}

        -

        {{ game.source_name }}

        -
        -
        -
        - {% if game.playtime_minutes %} - {{ (game.playtime_minutes / 60)|round }}h played - {% endif %} -
        -
        -
      • - {% endfor %} -
      -
      -
      {% endif %} - - {% if recent_movies %} -
      -

      Recently Watched Movies

      -
      -
        - {% for movie in recent_movies %} -
      • -
        -
        - {% if movie.poster_url %} - - {% else %} -
        - - - -
        - {% endif %} -
        -

        {{ movie.title }}

        -

        {{ movie.source_name }}

        -
        -
        -
        - {% if movie.watch_count %} - Watched {{ movie.watch_count }} times - {% endif %} -
        -
        -
      • - {% endfor %} -
      + +
      + +
      +
      +
      + + + +
      +
      +
      Total Media
      +
      {{ stats.total_media|number_format }}
      +
      +
      +
      + + +
      +
      +
      + + + +
      +
      +
      Games
      +
      {{ stats.total_games|number_format }}
      +
      {{ stats.favorite_games }} favorites
      +
      +
      +
      + + +
      +
      +
      + + + +
      +
      +
      Movies & TV
      +
      {{ (stats.total_movies + stats.total_tv_shows)|number_format }}
      +
      {{ stats.watched_movies }} watched
      +
      +
      +
      + + +
      +
      +
      + + + +
      +
      +
      Music
      +
      {{ stats.total_music|number_format }}
      +
      {{ stats.favorite_music }} favorites
      +
      +
      - {% endif %} - - {% if recent_syncs %} -
      -

      Recent Sync Activities

      -
      -
        - {% for sync in recent_syncs %} -
      • -
        -
        -
        - {% if sync.status == 'completed' %} - - - - {% elseif sync.status == 'failed' %} - - - + +
        + +
        +
        +
        + + + +
        +
        +
        Total Playtime
        +
        + {% if stats.total_playtime %} + {{ (stats.total_playtime / 60)|round }}h + {% else %} + 0h + {% endif %} +
        +
        +
        +
        + + +
        +
        +
        + + + +
        +
        +
        TV Episodes
        +
        {{ stats.total_episodes|number_format }}
        +
        +
        +
        + + +
        +
        +
        + + + +
        +
        +
        Sync Status
        +
        + {% if sync_stats.successful_syncs > 0 %} + {{ sync_stats.successful_syncs }}/{{ sync_stats.total_syncs }} Success + {% else %} + No syncs yet + {% endif %} +
        +
        +
        +
        +
        + + +
        +
        +

        Recent Activity

        +
        + + + {% if recent_games %} +
        +

        Recently Played Games

        +
        +
          + {% for game in recent_games %} +
        • +
          +
          + {% if game.image_url %} + {% else %} - - - +
          + + + +
          + {% endif %} +
          +

          {{ game.title }}

          +

          {{ game.source_name }}

          +
          +
          +
          + {% if game.playtime_minutes %} + {{ (game.playtime_minutes / 60)|round }}h played {% endif %}
          -
          -

          {{ sync.source_name }}

          -

          {{ sync.sync_type|title }} sync

          +
          +
        • + {% endfor %} +
        +
        +
        + {% endif %} + + + {% if recent_movies %} +
        +

        Recently Watched Movies

        +
        +
          + {% for movie in recent_movies %} +
        • +
          +
          + {% if movie.poster_url %} + + {% else %} +
          + + + +
          + {% endif %} +
          +

          {{ movie.title }}

          +

          {{ movie.source_name }}

          +
          +
          +
          + {% if movie.watch_count %} + Watched {{ movie.watch_count }} times + {% endif %}
          -
          - {{ sync.processed_items }} items • {{ sync.created_at|date('M j, Y') }} -
          -
        -
      • - {% endfor %} -
      +

    • + {% endfor %} +
    +
-
- {% endif %} + {% endif %} - {% if not recent_games and not recent_movies and not recent_syncs %} -
-
- - - -

No recent activity

-

Start adding media to see your activity here.

+ + {% if recent_syncs %} +
+

Recent Sync Activities

+
+
    + {% for sync in recent_syncs %} +
  • +
    +
    +
    + {% if sync.status == 'completed' %} + + + + {% elseif sync.status == 'failed' %} + + + + {% else %} + + + + {% endif %} +
    +
    +

    {{ sync.source_name }}

    +

    {{ sync.sync_type|title }} sync

    +
    +
    +
    + {{ sync.processed_items }} items • {{ sync.created_at|date('M j, Y') }} +
    +
    +
  • + {% endfor %} +
+
+ {% endif %} + + {% if not recent_games and not recent_movies and not recent_syncs %} +
+
+ + + +

No recent activity

+

Start adding media to see your activity here.

+
+
+ {% endif %}
- {% endif %} -
{% endblock %} diff --git a/resources/views/games/index.twig b/resources/views/games/index.twig index b9d0a49..3fbea29 100644 --- a/resources/views/games/index.twig +++ b/resources/views/games/index.twig @@ -1,172 +1,224 @@ {% extends "layouts/app.twig" %} -{% block content %} -
-
- -
-
-
-
Filters
-
-
- -
- - - +{% block nav_controls %} + + + + + {% for genre in filters.genres %} + + {% endfor %} + {% for platform in filters.platforms %} + + {% endfor %} +
+ + + + +
+ +
- - {% if available_filters.genres %} -
-
Genres
- -
- {% endif %} + + - - {% if available_filters.platforms %} -
-
Platforms
- -
- {% endif %} - - -
- - - Clear All - -
- -
-
+ +
+ +
+
+ {% for key, label in sort_options %} + + {{ label }} + {% if sort == key %} + + + + {% endif %} + + {% endfor %}
+
+
+{% endblock %} - -
-
- -
-
-

Games

- {% if pagination.total_items > 0 %} -
- {{ pagination.total_items }} games from {{ games|reduce((carry, game) => carry + game.platform_count, 0) }} platforms - {% if search %} - matching "{{ search }}" - {% endif %} - {% if filters.genres or filters.platforms %} - {% if filters.genres %} - {{ filters.genres|join(', ') }} - {% endif %} - {% if filters.platforms %} - {{ filters.platforms|join(', ') }} - {% endif %} - {% endif %} -
- {% endif %} -
+{% block sidebar %} +
+ +
+

Filters

-
- -
- - - {% for genre in filters.genres %} - - {% endfor %} - {% for platform in filters.platforms %} - - {% endfor %} -
- - - - -
- -
+ +
+ + + -
- - + + {% if available_filters.genres %} +
+ + +
+ {% endif %} - - -
-
-
+ + {% if available_filters.platforms %} +
+ + +
+ {% endif %} + + +
+ + + Clear All + +
+ +
+ + + {% if filters.genres or filters.platforms or search %} +
+

Active Filters

+
+ {% if search %} +
+ Search: "{{ search }}" + + + + + +
+ {% endif %} + {% for genre in filters.genres %} +
+ Genre: {{ genre }} + + + + + +
+ {% endfor %} + {% for platform in filters.platforms %} +
+ Platform: {{ platform }} + + + + + +
+ {% endfor %} +
+
+ {% endif %} + + +
+

Quick Stats

+
+
+ Total Games + {{ pagination.total_items }} +
+
+ This Page + {{ games|length }} +
+ {% if pagination.total_pages > 1 %} +
+ Page + {{ pagination.current_page }} of {{ pagination.total_pages }} +
+ {% endif %} +
+
+
+{% endblock %} + +{% block content %} + +
+ +
+

Games

+ {% if pagination.total_items > 0 %} +
+ {{ pagination.total_items }} games from {{ games|reduce((carry, game) => carry + game.platform_count, 0) }} platforms + {% if search %} + matching "{{ search }}" + {% endif %} + {% if filters.genres or filters.platforms %} + {% if filters.genres %} + {{ filters.genres|join(', ') }} + {% endif %} + {% if filters.platforms %} + {{ filters.platforms|join(', ') }} + {% endif %} + {% endif %} +
+ {% endif %} +
{% if games is empty %}
@@ -197,31 +249,31 @@ {% if view_mode == 'list' %} -
-
    +
    +
      {% for game in games %} -
    • -
      -
      +
    • +
      +
      {% if game.image_url %} - {{ game.title }} + {{ game.title }} {% else %} -
      - +
      +
      {% endif %} -
      -

      - +
      +

      + {{ game.title }}

      -
      +
      {{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }} {% if game.platforms %} - + {{ game.platforms|join(', ') }} {% endif %} @@ -233,9 +285,9 @@
      {% if game.genres %} -
      +
      {% for genre in game.genres|slice(0, 3) %} - + {{ genre }} {% endfor %} @@ -249,27 +301,25 @@ {% elseif view_mode == 'covers' %} -
      +
      {% for game in games %} -
      -
      - {% if game.image_url %} -
      - {{ game.title }} -
      - {% else %} -
      - - - -
      - {% endif %} -
      -
      - {{ game.title }} -
      -

      {{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}

      -
      +
      + {% if game.image_url %} +
      + {{ game.title }} +
      + {% else %} +
      + + + +
      + {% endif %} +
      +
      + {{ game.title }} +
      +

      {{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }}

      {% endfor %} @@ -277,56 +327,54 @@ {% else %} -
      +
      {% for game in games %} -
      -
      -
      -
      -
      - {% if game.image_url %} - {{ game.title }} - {% else %} -
      - - - -
      - {% endif %} -
      -
      -
      - - {{ game.title }} - -
      -

      - {{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }} - {% if game.platforms %} - - {{ game.platforms|join(', ') }} - - {% endif %} -

      -
      -
      -
      -
      - {{ game.total_playtime|format_duration }} played - {% if game.max_completion > 0 %} - {{ game.max_completion }}% complete - {% endif %} -
      - {% if game.genres %} -
      - {% for genre in game.genres|slice(0, 3) %} - - {{ genre }} - - {% endfor %} +
      +
      +
      +
      + {% if game.image_url %} + {{ game.title }} + {% else %} +
      + + +
      {% endif %}
      +
      +
      + + {{ game.title }} + +
      +

      + {{ game.platform_count }} platform{{ game.platform_count > 1 ? 's' : '' }} + {% if game.platforms %} + + {{ game.platforms|join(', ') }} + + {% endif %} +

      +
      +
      +
      +
      + {{ game.total_playtime|format_duration }} played + {% if game.max_completion > 0 %} + {{ game.max_completion }}% complete + {% endif %} +
      + {% if game.genres %} +
      + {% for genre in game.genres|slice(0, 3) %} + + {{ genre }} + + {% endfor %} +
      + {% endif %}
      @@ -334,42 +382,89 @@
      {% endif %} - + {% if pagination.total_pages > 1 %} -
      -
      - - - per page +
      +
      +
      + Showing {{ (pagination.current_page - 1) * pagination.per_page + 1 }} to {{ min(pagination.current_page * pagination.per_page, pagination.total_items) }} of {{ pagination.total_items }} games +
      +
      + + + per page +
      -
      +
      + {% if pagination.has_prev %} + class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center"> + + + Previous + {% else %} + + + + + Previous + {% endif %} -
      - {% for page_num in range(max(1, pagination.current_page - 2), min(pagination.total_pages, pagination.current_page + 2)) %} - - {{ page_num }} - - {% endfor %} -
      + + {% set start_page = max(1, pagination.current_page - 2) %} + {% set end_page = min(pagination.total_pages, pagination.current_page + 2) %} + {% if start_page > 1 %} + 1 + {% if start_page > 2 %} + ... + {% endif %} + {% endif %} + + {% for page_num in start_page..end_page %} + {% if page_num == pagination.current_page %} + {{ page_num }} + {% else %} + {{ page_num }} + {% endif %} + {% endfor %} + + {% if end_page < pagination.total_pages %} + {% if end_page < pagination.total_pages - 1 %} + ... + {% endif %} + {{ pagination.total_pages }} + {% endif %} + + {% if pagination.has_next %} + class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center"> Next + + + + {% else %} + + Next + + + + {% endif %}
      @@ -429,6 +524,13 @@ document.getElementById('per_page')?.addEventListener('change', function() { url.searchParams.set('page', '1'); // Reset to first page window.location = url.toString(); }); + +document.getElementById('per_page_top')?.addEventListener('change', function() { + const url = new URL(window.location); + url.searchParams.set('per_page', this.value); + url.searchParams.set('page', '1'); // Reset to first page + window.location = url.toString(); +}); {% endblock %} diff --git a/resources/views/games/show.twig b/resources/views/games/show.twig index e2c7ddc..551ef80 100644 --- a/resources/views/games/show.twig +++ b/resources/views/games/show.twig @@ -1,107 +1,156 @@ {% extends "layouts/app.twig" %} {% block content %} - -
      -
      -
      + +
      + +
      +
      + + +
      +
      + -
      -
      +
      +
      {% if platform_versions[0].cover_image_url %} - {{ main_game.title }} + {{ main_game.title }} +
      {% elseif main_game.image_url %} - {{ main_game.title }} + {{ main_game.title }} +
      {% else %} -
      - +
      + + +
      {% endif %}
      - + -
      -