mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
dont know ?
This commit is contained in:
@@ -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
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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('/<img[^>]+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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user