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 %}
+
Update actor information and metadata
+{{ actors|length }} performer{{ actors|length != 1 ? 's' : '' }}
+ +{{ pagination.total_items }} performer{{ pagination.total_items != 1 ? 's' : '' }}
+ + + {% if pagination.total_pages > 0 %} +Performers will appear here once you sync content from your adult video sources.
-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.
+