mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
actor fetching :)
Stash / ADultVideoAPI
This commit is contained in:
@@ -149,8 +149,12 @@ class ActorController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle image upload
|
// Handle image upload/download
|
||||||
$thumbnailPath = $actor['thumbnail_path']; // Keep existing by default
|
$thumbnailPath = $actor['thumbnail_path']; // Keep existing by default
|
||||||
|
$imageSource = $data['image_source'] ?? 'upload';
|
||||||
|
|
||||||
|
if ($imageSource === 'upload') {
|
||||||
|
// Handle file upload
|
||||||
if (!empty($uploadedFiles['thumbnail']) && $uploadedFiles['thumbnail']->getError() === UPLOAD_ERR_OK) {
|
if (!empty($uploadedFiles['thumbnail']) && $uploadedFiles['thumbnail']->getError() === UPLOAD_ERR_OK) {
|
||||||
$uploadedFile = $uploadedFiles['thumbnail'];
|
$uploadedFile = $uploadedFiles['thumbnail'];
|
||||||
|
|
||||||
@@ -179,6 +183,102 @@ class ActorController extends Controller
|
|||||||
$uploadedFile->moveTo($uploadPath);
|
$uploadedFile->moveTo($uploadPath);
|
||||||
$thumbnailPath = '/images/actors/' . $filename;
|
$thumbnailPath = '/images/actors/' . $filename;
|
||||||
}
|
}
|
||||||
|
} elseif ($imageSource === 'url') {
|
||||||
|
// Handle URL download
|
||||||
|
$imageUrl = trim($data['thumbnail_url'] ?? '');
|
||||||
|
if (!empty($imageUrl)) {
|
||||||
|
// Validate URL
|
||||||
|
if (!filter_var($imageUrl, FILTER_VALIDATE_URL)) {
|
||||||
|
return $this->view->render($response, 'actor/edit.twig', [
|
||||||
|
'title' => 'Edit Actor',
|
||||||
|
'actor' => $actor,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
'error' => 'Invalid image URL provided.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Download image from URL
|
||||||
|
$imageData = file_get_contents($imageUrl);
|
||||||
|
if ($imageData === false) {
|
||||||
|
return $this->view->render($response, 'actor/edit.twig', [
|
||||||
|
'title' => 'Edit Actor',
|
||||||
|
'actor' => $actor,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
'error' => 'Failed to download image from the provided URL.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get image info to validate type and determine extension
|
||||||
|
$imageInfo = getimagesizefromstring($imageData);
|
||||||
|
if (!$imageInfo) {
|
||||||
|
return $this->view->render($response, 'actor/edit.twig', [
|
||||||
|
'title' => 'Edit Actor',
|
||||||
|
'actor' => $actor,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
'error' => 'The URL does not point to a valid image.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate MIME type
|
||||||
|
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||||
|
if (!in_array($imageInfo['mime'], $allowedTypes)) {
|
||||||
|
return $this->view->render($response, 'actor/edit.twig', [
|
||||||
|
'title' => 'Edit Actor',
|
||||||
|
'actor' => $actor,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
'error' => 'Invalid image type. Only JPEG, PNG, GIF, and WebP are allowed.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine extension from MIME type
|
||||||
|
$extension = '';
|
||||||
|
switch ($imageInfo['mime']) {
|
||||||
|
case 'image/jpeg':
|
||||||
|
$extension = 'jpg';
|
||||||
|
break;
|
||||||
|
case 'image/png':
|
||||||
|
$extension = 'png';
|
||||||
|
break;
|
||||||
|
case 'image/gif':
|
||||||
|
$extension = 'gif';
|
||||||
|
break;
|
||||||
|
case 'image/webp':
|
||||||
|
$extension = 'webp';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate filename and save file
|
||||||
|
$filename = 'actor_' . $actorId . '_' . time() . '.' . $extension;
|
||||||
|
$uploadPath = __DIR__ . '/../../public/images/actors/' . $filename;
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
$uploadDir = dirname($uploadPath);
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the downloaded image
|
||||||
|
if (file_put_contents($uploadPath, $imageData) === false) {
|
||||||
|
return $this->view->render($response, 'actor/edit.twig', [
|
||||||
|
'title' => 'Edit Actor',
|
||||||
|
'actor' => $actor,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
'error' => 'Failed to save the downloaded image.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$thumbnailPath = '/images/actors/' . $filename;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return $this->view->render($response, 'actor/edit.twig', [
|
||||||
|
'title' => 'Edit Actor',
|
||||||
|
'actor' => $actor,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
'error' => 'Error downloading image: ' . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare metadata
|
// Prepare metadata
|
||||||
$actorMetadata = [
|
$actorMetadata = [
|
||||||
@@ -246,13 +346,156 @@ class ActorController extends Controller
|
|||||||
'metadata' => $metadata
|
'metadata' => $metadata
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
public function fetchStashData(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
ini_set('memory_limit', '2G');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse JSON body for API requests
|
||||||
|
$rawBody = $request->getBody()->getContents();
|
||||||
|
$data = json_decode($rawBody, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
return $this->jsonResponse($response->withStatus(400), [
|
||||||
|
'error' => 'Invalid JSON in request body'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = trim($data['query'] ?? '');
|
||||||
|
|
||||||
|
if (empty($query)) {
|
||||||
|
return $this->jsonResponse($response->withStatus(400), [
|
||||||
|
'error' => 'Missing required parameter: query'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Stash configuration from database
|
||||||
|
$stmt = $this->pdo->prepare('SELECT * FROM sources WHERE name = ?');
|
||||||
|
$stmt->execute(['stash']);
|
||||||
|
$stashSource = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$stashSource) {
|
||||||
|
return $this->jsonResponse($response->withStatus(500), [
|
||||||
|
'error' => 'Stash source not configured in database'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stashUrl = trim($stashSource['api_url'] ?? '');
|
||||||
|
$stashApiKey = trim($stashSource['api_key'] ?? '');
|
||||||
|
|
||||||
|
if (empty($stashUrl)) {
|
||||||
|
return $this->jsonResponse($response->withStatus(500), [
|
||||||
|
'error' => 'Stash API URL not configured'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTTP client for Stash API
|
||||||
|
$client = new \GuzzleHttp\Client([
|
||||||
|
'timeout' => 30,
|
||||||
|
'verify' => false,
|
||||||
|
'headers' => [
|
||||||
|
'User-Agent' => 'MediaCollector/1.0',
|
||||||
|
'ApiKey' => $stashApiKey,
|
||||||
|
'Content-Type' => 'application/json'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Build GraphQL query to search for performers
|
||||||
|
$graphqlQuery = '
|
||||||
|
query FindPerformers($filter: FindFilterType) {
|
||||||
|
findPerformers(filter: $filter) {
|
||||||
|
performers {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
disambiguation
|
||||||
|
url
|
||||||
|
gender
|
||||||
|
birthdate
|
||||||
|
ethnicity
|
||||||
|
country
|
||||||
|
eye_color
|
||||||
|
height_cm
|
||||||
|
measurements
|
||||||
|
fake_tits
|
||||||
|
penis_length
|
||||||
|
circumcised
|
||||||
|
career_length
|
||||||
|
tattoos
|
||||||
|
piercings
|
||||||
|
alias_list
|
||||||
|
favorite
|
||||||
|
ignore_auto_tag
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
details
|
||||||
|
death_date
|
||||||
|
hair_color
|
||||||
|
weight
|
||||||
|
image_path
|
||||||
|
scene_count
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
';
|
||||||
|
|
||||||
|
$variables = [
|
||||||
|
'filter' => [
|
||||||
|
'q' => $query,
|
||||||
|
'per_page' => 10, // Limit results
|
||||||
|
'sort' => 'name',
|
||||||
|
'direction' => 'ASC'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Make the API call
|
||||||
|
$apiResponse = $client->post(rtrim($stashUrl, '/') . '/graphql', [
|
||||||
|
'json' => [
|
||||||
|
'query' => $graphqlQuery,
|
||||||
|
'variables' => $variables
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = json_decode($apiResponse->getBody(), true);
|
||||||
|
|
||||||
|
if (!isset($result['data']['findPerformers']['performers'])) {
|
||||||
|
return $this->jsonResponse($response, [
|
||||||
|
'performers' => [],
|
||||||
|
'message' => 'No performers found'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$performers = $result['data']['findPerformers']['performers'];
|
||||||
|
|
||||||
|
return $this->jsonResponse($response, [
|
||||||
|
'performers' => $performers,
|
||||||
|
'count' => count($performers),
|
||||||
|
'stash_url' => $stashUrl
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||||||
|
$errorMessage = 'Failed to connect to Stash server';
|
||||||
|
if ($e->hasResponse()) {
|
||||||
|
$statusCode = $e->getResponse()->getStatusCode();
|
||||||
|
$errorMessage .= ": HTTP {$statusCode}";
|
||||||
|
}
|
||||||
|
return $this->jsonResponse($response->withStatus(500), [
|
||||||
|
'error' => $errorMessage
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->jsonResponse($response->withStatus(500), [
|
||||||
|
'error' => 'Internal server error: ' . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function index(Request $request, Response $response, $args)
|
public function index(Request $request, Response $response, $args)
|
||||||
{
|
{
|
||||||
$queryParams = $request->getQueryParams();
|
$queryParams = $request->getQueryParams();
|
||||||
|
|
||||||
// Get pagination parameters
|
// Get pagination parameters
|
||||||
$page = max(1, (int)($queryParams['page'] ?? 1));
|
$page = max(1, (int)($queryParams['page'] ?? 1));
|
||||||
$perPage = max(12, min(100, (int)($queryParams['per_page'] ?? 24)));
|
$perPage = max(12, min(100, (int)($queryParams['per_page'] ?? 48)));
|
||||||
|
|
||||||
// Get search parameters
|
// Get search parameters
|
||||||
$search = trim($queryParams['search'] ?? '');
|
$search = trim($queryParams['search'] ?? '');
|
||||||
@@ -367,9 +610,51 @@ class ActorController extends Controller
|
|||||||
$orderBy = $sortMap[$sort] ?? 'total_media_count DESC, a.name ASC';
|
$orderBy = $sortMap[$sort] ?? 'total_media_count DESC, a.name ASC';
|
||||||
$sql .= " ORDER BY {$orderBy}";
|
$sql .= " ORDER BY {$orderBy}";
|
||||||
|
|
||||||
// Get total count for pagination
|
// Get total count for pagination - use a subquery to count the results
|
||||||
$countSql = str_replace('SELECT a.id, a.name, a.thumbnail_path,', 'SELECT COUNT(*) as count,', $sql);
|
$countSql = "
|
||||||
$countSql = preg_replace('/ORDER BY.*$/', '', $countSql);
|
SELECT COUNT(*) as count FROM (
|
||||||
|
SELECT a.id,
|
||||||
|
COALESCE(adult_counts.adult_video_count, 0) as adult_video_count,
|
||||||
|
COALESCE(movie_counts.movie_count, 0) as movie_count,
|
||||||
|
COALESCE(tv_counts.tv_show_count, 0) as tv_show_count
|
||||||
|
FROM actors a
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT actor_id, COUNT(DISTINCT adult_video_id) as adult_video_count
|
||||||
|
FROM actor_adult_video
|
||||||
|
GROUP BY actor_id
|
||||||
|
) adult_counts ON a.id = adult_counts.actor_id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT actor_id, COUNT(DISTINCT movie_id) as movie_count
|
||||||
|
FROM actor_movie
|
||||||
|
GROUP BY actor_id
|
||||||
|
) movie_counts ON a.id = movie_counts.actor_id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT actor_id, COUNT(DISTINCT tv_show_id) as tv_show_count
|
||||||
|
FROM (
|
||||||
|
SELECT actor_id, tv_show_id FROM actor_tv_show
|
||||||
|
UNION
|
||||||
|
SELECT ate.actor_id, te.tv_show_id
|
||||||
|
FROM actor_tv_episode ate
|
||||||
|
JOIN tv_episodes te ON ate.tv_episode_id = te.id
|
||||||
|
) combined_tv
|
||||||
|
GROUP BY actor_id
|
||||||
|
) tv_counts ON a.id = tv_counts.actor_id
|
||||||
|
";
|
||||||
|
|
||||||
|
// Add search filter
|
||||||
|
if (!empty($whereClauses)) {
|
||||||
|
$countSql .= ' WHERE ' . implode(' AND ', $whereClauses);
|
||||||
|
}
|
||||||
|
|
||||||
|
$countSql .= " GROUP BY a.id";
|
||||||
|
|
||||||
|
// Add HAVING clause for filters
|
||||||
|
if (!empty($havingClauses)) {
|
||||||
|
$countSql .= ' HAVING ' . implode(' AND ', $havingClauses);
|
||||||
|
}
|
||||||
|
|
||||||
|
$countSql .= ") as filtered_actors";
|
||||||
|
|
||||||
$countStmt = $this->pdo->prepare($countSql);
|
$countStmt = $this->pdo->prepare($countSql);
|
||||||
foreach ($params as $key => $value) {
|
foreach ($params as $key => $value) {
|
||||||
$countStmt->bindValue($key, $value);
|
$countStmt->bindValue($key, $value);
|
||||||
|
|||||||
187
public/index.php
187
public/index.php
@@ -73,9 +73,6 @@ $container->set('view', function () use ($container) {
|
|||||||
case 'home':
|
case 'home':
|
||||||
$basePath = '/';
|
$basePath = '/';
|
||||||
break;
|
break;
|
||||||
case 'dashboard.index':
|
|
||||||
$basePath = '/';
|
|
||||||
break;
|
|
||||||
case 'games.index':
|
case 'games.index':
|
||||||
$basePath = '/media/games';
|
$basePath = '/media/games';
|
||||||
break;
|
break;
|
||||||
@@ -164,7 +161,191 @@ $container->set('view', function () use ($container) {
|
|||||||
return $authService->generateCSRFToken();
|
return $authService->generateCSRFToken();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
$twig->getEnvironment()->addFunction(new TwigFunction('country_flag', function ($countryName) {
|
||||||
|
if (!$countryName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Country name to ISO 3166-1 alpha-2 code mapping (most common countries)
|
||||||
|
$countryMap = [
|
||||||
|
'United States' => 'us',
|
||||||
|
'USA' => 'us',
|
||||||
|
'America' => 'us',
|
||||||
|
'United Kingdom' => 'gb',
|
||||||
|
'UK' => 'gb',
|
||||||
|
'Britain' => 'gb',
|
||||||
|
'England' => 'gb',
|
||||||
|
'Germany' => 'de',
|
||||||
|
'France' => 'fr',
|
||||||
|
'Italy' => 'it',
|
||||||
|
'Spain' => 'es',
|
||||||
|
'Canada' => 'ca',
|
||||||
|
'Australia' => 'au',
|
||||||
|
'Japan' => 'jp',
|
||||||
|
'China' => 'cn',
|
||||||
|
'India' => 'in',
|
||||||
|
'Brazil' => 'br',
|
||||||
|
'Mexico' => 'mx',
|
||||||
|
'Russia' => 'ru',
|
||||||
|
'South Korea' => 'kr',
|
||||||
|
'Netherlands' => 'nl',
|
||||||
|
'Sweden' => 'se',
|
||||||
|
'Norway' => 'no',
|
||||||
|
'Denmark' => 'dk',
|
||||||
|
'Finland' => 'fi',
|
||||||
|
'Poland' => 'pl',
|
||||||
|
'Czech Republic' => 'cz',
|
||||||
|
'Hungary' => 'hu',
|
||||||
|
'Romania' => 'ro',
|
||||||
|
'Bulgaria' => 'bg',
|
||||||
|
'Greece' => 'gr',
|
||||||
|
'Portugal' => 'pt',
|
||||||
|
'Belgium' => 'be',
|
||||||
|
'Austria' => 'at',
|
||||||
|
'Switzerland' => 'ch',
|
||||||
|
'Ireland' => 'ie',
|
||||||
|
'New Zealand' => 'nz',
|
||||||
|
'South Africa' => 'za',
|
||||||
|
'Argentina' => 'ar',
|
||||||
|
'Chile' => 'cl',
|
||||||
|
'Colombia' => 'co',
|
||||||
|
'Peru' => 'pe',
|
||||||
|
'Venezuela' => 've',
|
||||||
|
'Ecuador' => 'ec',
|
||||||
|
'Uruguay' => 'uy',
|
||||||
|
'Paraguay' => 'py',
|
||||||
|
'Bolivia' => 'bo',
|
||||||
|
'Thailand' => 'th',
|
||||||
|
'Vietnam' => 'vn',
|
||||||
|
'Philippines' => 'ph',
|
||||||
|
'Indonesia' => 'id',
|
||||||
|
'Malaysia' => 'my',
|
||||||
|
'Singapore' => 'sg',
|
||||||
|
'Turkey' => 'tr',
|
||||||
|
'Israel' => 'il',
|
||||||
|
'Egypt' => 'eg',
|
||||||
|
'Morocco' => 'ma',
|
||||||
|
'Tunisia' => 'tn',
|
||||||
|
'Algeria' => 'dz',
|
||||||
|
'Saudi Arabia' => 'sa',
|
||||||
|
'UAE' => 'ae',
|
||||||
|
'United Arab Emirates' => 'ae',
|
||||||
|
'Qatar' => 'qa',
|
||||||
|
'Kuwait' => 'kw',
|
||||||
|
'Bahrain' => 'bh',
|
||||||
|
'Oman' => 'om',
|
||||||
|
'Jordan' => 'jo',
|
||||||
|
'Lebanon' => 'lb',
|
||||||
|
'Syria' => 'sy',
|
||||||
|
'Iraq' => 'iq',
|
||||||
|
'Iran' => 'ir',
|
||||||
|
'Pakistan' => 'pk',
|
||||||
|
'Bangladesh' => 'bd',
|
||||||
|
'Sri Lanka' => 'lk',
|
||||||
|
'Nepal' => 'np',
|
||||||
|
'Bhutan' => 'bt',
|
||||||
|
'Maldives' => 'mv',
|
||||||
|
'Afghanistan' => 'af',
|
||||||
|
'Kazakhstan' => 'kz',
|
||||||
|
'Uzbekistan' => 'uz',
|
||||||
|
'Turkmenistan' => 'tm',
|
||||||
|
'Kyrgyzstan' => 'kg',
|
||||||
|
'Tajikistan' => 'tj',
|
||||||
|
'Mongolia' => 'mn',
|
||||||
|
'North Korea' => 'kp',
|
||||||
|
'Taiwan' => 'tw',
|
||||||
|
'Hong Kong' => 'hk',
|
||||||
|
'Macau' => 'mo',
|
||||||
|
'South Korea' => 'kr',
|
||||||
|
'Cambodia' => 'kh',
|
||||||
|
'Laos' => 'la',
|
||||||
|
'Myanmar' => 'mm',
|
||||||
|
'Brunei' => 'bn',
|
||||||
|
'East Timor' => 'tl',
|
||||||
|
'Papua New Guinea' => 'pg',
|
||||||
|
'Fiji' => 'fj',
|
||||||
|
'Solomon Islands' => 'sb',
|
||||||
|
'Vanuatu' => 'vu',
|
||||||
|
'Samoa' => 'ws',
|
||||||
|
'Tonga' => 'to',
|
||||||
|
'Tuvalu' => 'tv',
|
||||||
|
'Kiribati' => 'ki',
|
||||||
|
'Marshall Islands' => 'mh',
|
||||||
|
'Micronesia' => 'fm',
|
||||||
|
'Palau' => 'pw',
|
||||||
|
'Nauru' => 'nr',
|
||||||
|
'Cuba' => 'cu',
|
||||||
|
'Jamaica' => 'jm',
|
||||||
|
'Haiti' => 'ht',
|
||||||
|
'Dominican Republic' => 'do',
|
||||||
|
'Puerto Rico' => 'pr',
|
||||||
|
'Bahamas' => 'bs',
|
||||||
|
'Trinidad and Tobago' => 'tt',
|
||||||
|
'Barbados' => 'bb',
|
||||||
|
'Saint Lucia' => 'lc',
|
||||||
|
'Saint Vincent and the Grenadines' => 'vc',
|
||||||
|
'Grenada' => 'gd',
|
||||||
|
'Antigua and Barbuda' => 'ag',
|
||||||
|
'Saint Kitts and Nevis' => 'kn',
|
||||||
|
'Dominica' => 'dm',
|
||||||
|
'Saint Martin' => 'mf',
|
||||||
|
'Guadeloupe' => 'gp',
|
||||||
|
'Martinique' => 'mq',
|
||||||
|
'French Guiana' => 'gf',
|
||||||
|
'Suriname' => 'sr',
|
||||||
|
'Guyana' => 'gy',
|
||||||
|
'Belize' => 'bz',
|
||||||
|
'Costa Rica' => 'cr',
|
||||||
|
'Panama' => 'pa',
|
||||||
|
'Nicaragua' => 'ni',
|
||||||
|
'Honduras' => 'hn',
|
||||||
|
'El Salvador' => 'sv',
|
||||||
|
'Guatemala' => 'gt',
|
||||||
|
'Greenland' => 'gl',
|
||||||
|
'Iceland' => 'is',
|
||||||
|
'Faroe Islands' => 'fo',
|
||||||
|
'Åland Islands' => 'ax',
|
||||||
|
'Guernsey' => 'gg',
|
||||||
|
'Jersey' => 'je',
|
||||||
|
'Isle of Man' => 'im',
|
||||||
|
'Gibraltar' => 'gi',
|
||||||
|
'Malta' => 'mt',
|
||||||
|
'Cyprus' => 'cy',
|
||||||
|
'Luxembourg' => 'lu',
|
||||||
|
'Monaco' => 'mc',
|
||||||
|
'Andorra' => 'ad',
|
||||||
|
'San Marino' => 'sm',
|
||||||
|
'Vatican City' => 'va',
|
||||||
|
'Liechtenstein' => 'li',
|
||||||
|
'Slovenia' => 'si',
|
||||||
|
'Croatia' => 'hr',
|
||||||
|
'Bosnia and Herzegovina' => 'ba',
|
||||||
|
'Serbia' => 'rs',
|
||||||
|
'Montenegro' => 'me',
|
||||||
|
'Kosovo' => 'xk',
|
||||||
|
'North Macedonia' => 'mk',
|
||||||
|
'Albania' => 'al',
|
||||||
|
'Moldova' => 'md',
|
||||||
|
'Ukraine' => 'ua',
|
||||||
|
'Belarus' => 'by',
|
||||||
|
'Lithuania' => 'lt',
|
||||||
|
'Latvia' => 'lv',
|
||||||
|
'Estonia' => 'ee',
|
||||||
|
'Slovakia' => 'sk',
|
||||||
|
'Armenia' => 'am',
|
||||||
|
'Azerbaijan' => 'az',
|
||||||
|
'Georgia' => 'ge',
|
||||||
|
];
|
||||||
|
|
||||||
|
$countryCode = strtolower($countryMap[$countryName] ?? '');
|
||||||
|
|
||||||
|
if ($countryCode) {
|
||||||
|
// Return Iconify flag icon HTML
|
||||||
|
return '<span class="iconify" data-icon="flag:' . $countryCode . '-4x3" data-inline="false" style="width: 20px; height: 15px; display: inline-block;"></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}));
|
||||||
|
|
||||||
$twig->getEnvironment()->addFilter(new TwigFilter('format_duration', function ($minutes) {
|
$twig->getEnvironment()->addFilter(new TwigFilter('format_duration', function ($minutes) {
|
||||||
if (!$minutes || $minutes == 0) {
|
if (!$minutes || $minutes == 0) {
|
||||||
|
|||||||
83
public/test.php
Normal file
83
public/test.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?php $query = ' query FindScenes($filter: FindFilterType) {
|
||||||
|
findScenes(filter: $filter) {
|
||||||
|
scenes {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
details
|
||||||
|
url
|
||||||
|
date
|
||||||
|
rating100
|
||||||
|
organized
|
||||||
|
o_counter
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
paths {
|
||||||
|
screenshot
|
||||||
|
preview
|
||||||
|
stream
|
||||||
|
webp
|
||||||
|
vtt
|
||||||
|
sprite
|
||||||
|
funscript
|
||||||
|
caption
|
||||||
|
}
|
||||||
|
files {
|
||||||
|
size
|
||||||
|
duration
|
||||||
|
video_codec
|
||||||
|
audio_codec
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
performers {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
disambiguation
|
||||||
|
url
|
||||||
|
gender
|
||||||
|
birthdate
|
||||||
|
ethnicity
|
||||||
|
country
|
||||||
|
eye_color
|
||||||
|
height_cm
|
||||||
|
measurements
|
||||||
|
fake_tits
|
||||||
|
penis_length
|
||||||
|
circumcised
|
||||||
|
career_length
|
||||||
|
tattoos
|
||||||
|
piercings
|
||||||
|
alias_list
|
||||||
|
favorite
|
||||||
|
ignore_auto_tag
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
details
|
||||||
|
death_date
|
||||||
|
hair_color
|
||||||
|
weight
|
||||||
|
image_path
|
||||||
|
scene_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
';
|
||||||
|
|
||||||
|
$variables = [
|
||||||
|
'filter' => [
|
||||||
|
'per_page' => 100,
|
||||||
|
'sort' => 'created_at',
|
||||||
|
'direction' => 'DESC'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
'json' => [
|
||||||
|
'query' => $query,
|
||||||
|
'variables' => $variables
|
||||||
|
]*/
|
||||||
|
|
||||||
|
|
||||||
|
print_r (json_encode(str_replace("\r\n", '', $query) ));
|
||||||
@@ -43,8 +43,28 @@
|
|||||||
<!-- Name -->
|
<!-- Name -->
|
||||||
<div>
|
<div>
|
||||||
<label for="name" class="block text-sm font-medium text-gray-700">Name *</label>
|
<label for="name" class="block text-sm font-medium text-gray-700">Name *</label>
|
||||||
|
<div class="flex space-x-2">
|
||||||
<input type="text" name="name" id="name" value="{{ actor.name }}" required
|
<input type="text" name="name" id="name" value="{{ actor.name }}" required
|
||||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
|
class="mt-1 block flex-1 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
|
||||||
|
<button type="button" id="fetch-api-data" class="mt-1 bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 text-sm">
|
||||||
|
Fetch from API
|
||||||
|
</button>
|
||||||
|
<button type="button" id="fetch-stash-data" class="mt-1 bg-purple-600 text-white px-4 py-2 rounded-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 text-sm">
|
||||||
|
Fetch from Stash
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stash Search Query (Hidden by default) -->
|
||||||
|
<div id="stash-config" class="hidden border border-purple-200 rounded-md p-4 bg-purple-50">
|
||||||
|
<h4 class="text-sm font-medium text-purple-800 mb-3">Stash Performer Search</h4>
|
||||||
|
<div>
|
||||||
|
<label for="stash_query" class="block text-sm font-medium text-purple-700">Search Query</label>
|
||||||
|
<input type="text" name="stash_query" id="stash_query"
|
||||||
|
placeholder="Performer name to search"
|
||||||
|
class="mt-1 block w-full border border-purple-300 rounded-md shadow-sm focus:ring-purple-500 focus:border-purple-500 sm:text-sm px-3 py-2">
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-purple-600">Enter the performer name to search for in your configured Stash instance. Uses the same API configuration as your Stash sync.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Current Image -->
|
<!-- Current Image -->
|
||||||
@@ -60,14 +80,63 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Image Upload -->
|
<!-- Image Source Selection -->
|
||||||
<div>
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-3">Image Source</label>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="radio" name="image_source" id="image_source_upload" value="upload"
|
||||||
|
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
|
||||||
|
checked>
|
||||||
|
<label for="image_source_upload" class="ml-2 block text-sm text-gray-900">
|
||||||
|
Upload from computer
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="radio" name="image_source" id="image_source_url" value="url"
|
||||||
|
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300">
|
||||||
|
<label for="image_source_url" class="ml-2 block text-sm text-gray-900">
|
||||||
|
Download from URL
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Upload -->
|
||||||
|
<div id="upload_section">
|
||||||
<label for="thumbnail" class="block text-sm font-medium text-gray-700">Upload New Image</label>
|
<label for="thumbnail" class="block text-sm font-medium text-gray-700">Upload New Image</label>
|
||||||
<input type="file" name="thumbnail" id="thumbnail" accept="image/*"
|
<input type="file" name="thumbnail" id="thumbnail" accept="image/*"
|
||||||
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
|
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
|
||||||
<p class="mt-1 text-sm text-gray-500">Supported formats: JPEG, PNG, GIF, WebP. Maximum file size: 5MB.</p>
|
<p class="mt-1 text-sm text-gray-500">Supported formats: JPEG, PNG, GIF, WebP. Maximum file size: 5MB.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- External URL -->
|
||||||
|
<div id="url_section" class="hidden">
|
||||||
|
<label for="thumbnail_url" class="block text-sm font-medium text-gray-700">Image URL</label>
|
||||||
|
<input type="url" name="thumbnail_url" id="thumbnail_url"
|
||||||
|
placeholder="https://example.com/image.jpg"
|
||||||
|
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm px-3 py-2">
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Enter a direct link to an image. The image will be downloaded and stored locally.</p>
|
||||||
|
|
||||||
|
<!-- Image Preview -->
|
||||||
|
<div id="image_preview" class="mt-4 hidden">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Preview</label>
|
||||||
|
<div class="border border-gray-300 rounded-md p-4 bg-gray-50">
|
||||||
|
<div id="preview_loading" class="hidden flex items-center justify-center py-8">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
<span class="ml-2 text-gray-600">Loading preview...</span>
|
||||||
|
</div>
|
||||||
|
<div id="preview_error" class="hidden text-center py-8">
|
||||||
|
<svg class="mx-auto h-12 w-12 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
<p class="mt-2 text-sm text-red-600" id="preview_error_text">Unable to load image preview</p>
|
||||||
|
</div>
|
||||||
|
<img id="preview_image" class="max-w-full h-auto max-h-64 mx-auto rounded shadow-sm hidden" alt="Image preview">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Biography -->
|
<!-- Biography -->
|
||||||
<div>
|
<div>
|
||||||
<label for="biography" class="block text-sm font-medium text-gray-700">Biography</label>
|
<label for="biography" class="block text-sm font-medium text-gray-700">Biography</label>
|
||||||
@@ -277,4 +346,368 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Toggle image source sections
|
||||||
|
document.querySelectorAll('input[name="image_source"]').forEach(radio => {
|
||||||
|
radio.addEventListener('change', function() {
|
||||||
|
const uploadSection = document.getElementById('upload_section');
|
||||||
|
const urlSection = document.getElementById('url_section');
|
||||||
|
const imagePreview = document.getElementById('image_preview');
|
||||||
|
|
||||||
|
if (this.value === 'upload') {
|
||||||
|
uploadSection.classList.remove('hidden');
|
||||||
|
urlSection.classList.add('hidden');
|
||||||
|
imagePreview.classList.add('hidden');
|
||||||
|
} else if (this.value === 'url') {
|
||||||
|
uploadSection.classList.add('hidden');
|
||||||
|
urlSection.classList.remove('hidden');
|
||||||
|
// Check if URL field has a value and show preview if it does
|
||||||
|
const urlInput = document.getElementById('thumbnail_url');
|
||||||
|
if (urlInput.value.trim()) {
|
||||||
|
updateImagePreview(urlInput.value.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Image URL preview functionality
|
||||||
|
function updateImagePreview(url) {
|
||||||
|
const previewContainer = document.getElementById('image_preview');
|
||||||
|
const loadingDiv = document.getElementById('preview_loading');
|
||||||
|
const errorDiv = document.getElementById('preview_error');
|
||||||
|
const imageElement = document.getElementById('preview_image');
|
||||||
|
|
||||||
|
// Show preview container and loading state
|
||||||
|
previewContainer.classList.remove('hidden');
|
||||||
|
loadingDiv.classList.remove('hidden');
|
||||||
|
errorDiv.classList.add('hidden');
|
||||||
|
imageElement.classList.add('hidden');
|
||||||
|
|
||||||
|
// Create a new image to test loading
|
||||||
|
const testImage = new Image();
|
||||||
|
|
||||||
|
testImage.onload = function() {
|
||||||
|
// Image loaded successfully
|
||||||
|
loadingDiv.classList.add('hidden');
|
||||||
|
errorDiv.classList.add('hidden');
|
||||||
|
imageElement.src = url;
|
||||||
|
imageElement.classList.remove('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
testImage.onerror = function() {
|
||||||
|
// Image failed to load
|
||||||
|
loadingDiv.classList.add('hidden');
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
imageElement.classList.add('hidden');
|
||||||
|
document.getElementById('preview_error_text').textContent = 'Unable to load image. Please check the URL.';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set a timeout for slow-loading images
|
||||||
|
setTimeout(() => {
|
||||||
|
if (loadingDiv.classList.contains('hidden') === false) {
|
||||||
|
loadingDiv.classList.add('hidden');
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
document.getElementById('preview_error_text').textContent = 'Image loading timed out. Please try again.';
|
||||||
|
}
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
|
||||||
|
testImage.src = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener to URL input
|
||||||
|
document.getElementById('thumbnail_url').addEventListener('input', function() {
|
||||||
|
const url = this.value.trim();
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
// Debounce the preview update
|
||||||
|
clearTimeout(this.previewTimeout);
|
||||||
|
this.previewTimeout = setTimeout(() => {
|
||||||
|
updateImagePreview(url);
|
||||||
|
}, 500); // Wait 500ms after user stops typing
|
||||||
|
} else {
|
||||||
|
// Hide preview if URL is empty
|
||||||
|
document.getElementById('image_preview').classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear preview timeout on form submission
|
||||||
|
document.querySelector('form').addEventListener('submit', function() {
|
||||||
|
const urlInput = document.getElementById('thumbnail_url');
|
||||||
|
if (urlInput.previewTimeout) {
|
||||||
|
clearTimeout(urlInput.previewTimeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('fetch-api-data').addEventListener('click', async function() {
|
||||||
|
const nameInput = document.getElementById('name');
|
||||||
|
const actorName = nameInput.value.trim();
|
||||||
|
|
||||||
|
if (!actorName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const button = this;
|
||||||
|
const originalText = button.textContent;
|
||||||
|
button.textContent = 'Fetching...';
|
||||||
|
button.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Format name for API (firstname%20lastname)
|
||||||
|
const formattedName = actorName.replace(/\s+/g, '%20');
|
||||||
|
const apiUrl = `https://api.adultdatalink.com/pornstar/pornstar-data?name=${formattedName}`;
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Map API data to form fields
|
||||||
|
if (data) {
|
||||||
|
console.log(data);
|
||||||
|
console.log(data.appearance.ethnicity);
|
||||||
|
|
||||||
|
|
||||||
|
console.log(data.date_of_birth)
|
||||||
|
date = new Date(Date.parse(data.date_of_birth));
|
||||||
|
console.log(date);
|
||||||
|
date = date.toLocaleDateString(undefined, {year:'numeric'}) + '-' + date.toLocaleDateString(undefined, {month:'2-digit'}) + '-' + date.toLocaleDateString(undefined, {day:'2-digit'})
|
||||||
|
|
||||||
|
console.log(date);
|
||||||
|
// Basic info
|
||||||
|
if (data.avatar){
|
||||||
|
document.getElementById('image_source_url').value = 'url';
|
||||||
|
document.getElementById('thumbnail_url').value = data.avatar;
|
||||||
|
|
||||||
|
}
|
||||||
|
if (data.biography) document.getElementById('biography').value = data.biography;
|
||||||
|
if (data.birth_date) document.getElementById('birth_date').value = date;
|
||||||
|
if (data.death_date) document.getElementById('death_date').value = data.death_date;
|
||||||
|
if (data.place_of_birth) document.getElementById('birth_place').value = data.place_of_birth;
|
||||||
|
if (data.country) document.getElementById('nationality').value = data.country;
|
||||||
|
if (data.appearance.ethnicity) document.getElementById('ethnicity').value = data.appearance.ethnicity;
|
||||||
|
|
||||||
|
// Gender mapping
|
||||||
|
if (data.gender) {
|
||||||
|
const genderSelect = document.getElementById('gender');
|
||||||
|
const genderMap = {
|
||||||
|
'woman': 'FEMALE',
|
||||||
|
'male': 'MALE',
|
||||||
|
'transgender_female': 'TRANSGENDER_FEMALE',
|
||||||
|
'transgender_male': 'TRANSGENDER_MALE',
|
||||||
|
'intersex': 'INTERSEX',
|
||||||
|
'non_binary': 'NON_BINARY'
|
||||||
|
};
|
||||||
|
const mappedGender = genderMap[data.gender.toLowerCase()] || data.gender.toUpperCase();
|
||||||
|
genderSelect.value = mappedGender;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Physical attributes
|
||||||
|
if (data.appearance.height) document.getElementById('height').value = data.appearance.height;
|
||||||
|
document.getElementById('measurements').value = parseInt(data.appearance.bra)+'-'+ parseInt(data.appearance.waist)+'-'+parseInt(data.appearance.hip);
|
||||||
|
if (data.appearance.cup) document.getElementById('cup_size').value = data.appearance.cup;
|
||||||
|
if (data.appearance.hair_color) document.getElementById('hair_color').value = data.appearance.hair_color;
|
||||||
|
if (data.appearance.eye_color) document.getElementById('eye_color').value = data.appearance.eye_color;
|
||||||
|
|
||||||
|
// Body modifications
|
||||||
|
if (data.appearance.piercing_locations) document.getElementById('piercings').value = data.appearance.piercing_locations;
|
||||||
|
if (data.appearance.tattoo_locations) document.getElementById('tattoos').value = data.appearance.tattoo_locations;
|
||||||
|
|
||||||
|
// Aliases
|
||||||
|
if (data.aliases) document.getElementById('aliases').value = data.aliases;
|
||||||
|
if (data.aliases && Array.isArray(data.aliases)) {
|
||||||
|
document.getElementById('aliases').value = data.aliases.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Social media
|
||||||
|
if (data.social_media) {
|
||||||
|
if (data.social_media.twitter) document.getElementById('twitter').value = data.social_media.twitter;
|
||||||
|
if (data.social_media.instagram) document.getElementById('instagram').value = data.social_media.instagram;
|
||||||
|
if (data.social_media.onlyfans) document.getElementById('onlyfans').value = data.social_media.onlyfans;
|
||||||
|
if (data.social_media.website) document.getElementById('website').value = data.social_media.website;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.career_start) document.getElementById('debut_year').value = data.career_start;
|
||||||
|
if (data.career_end) document.getElementById('retirement_year').value = data.career_end;
|
||||||
|
if (data.career_status !== undefined) document.getElementById('active').checked = data.active;
|
||||||
|
if (data.genres && Array.isArray(data.adult_specific.genres)) {
|
||||||
|
document.getElementById('adult_genres').value = data.adult_specific.genres.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.tags && Array.isArray(data.tags)) {
|
||||||
|
document.getElementById('specialties').value = data.tags.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
alert('No data found for this performer');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
alert('Error fetching data from API: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
// Reset button state
|
||||||
|
button.textContent = originalText;
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stash API fetch functionality
|
||||||
|
document.getElementById('fetch-stash-data').addEventListener('click', async function() {
|
||||||
|
const nameInput = document.getElementById('name');
|
||||||
|
const actorName = nameInput.value.trim();
|
||||||
|
|
||||||
|
if (!actorName) {
|
||||||
|
alert('Please enter a name first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show Stash config section
|
||||||
|
const stashConfig = document.getElementById('stash-config');
|
||||||
|
stashConfig.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Pre-fill query with actor name
|
||||||
|
const queryInput = document.getElementById('stash_query');
|
||||||
|
if (!queryInput.value.trim()) {
|
||||||
|
queryInput.value = actorName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const button = this;
|
||||||
|
const originalText = button.textContent;
|
||||||
|
button.textContent = 'Fetching from Stash...';
|
||||||
|
button.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stashQuery = document.getElementById('stash_query').value.trim();
|
||||||
|
|
||||||
|
if (!stashQuery) {
|
||||||
|
alert('Please enter a search query');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make API call to backend endpoint
|
||||||
|
const response = await fetch('/api/actors/fetch-stash', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: stashQuery
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || `API request failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data && data.performers && data.performers.length > 0) {
|
||||||
|
const performer = data.performers[0]; // Use first result
|
||||||
|
const stashUrl = data.stash_url; // Get Stash URL from response
|
||||||
|
|
||||||
|
// Map Stash performer data to form fields
|
||||||
|
if (performer.image_path) {
|
||||||
|
// Set image source to URL and populate the URL field
|
||||||
|
document.getElementById('image_source_url').checked = true;
|
||||||
|
document.getElementById('upload_section').classList.add('hidden');
|
||||||
|
document.getElementById('url_section').classList.remove('hidden');
|
||||||
|
|
||||||
|
// Construct full image URL
|
||||||
|
let imageUrl = performer.image_path;
|
||||||
|
if (imageUrl.startsWith('/')) {
|
||||||
|
imageUrl = stashUrl + imageUrl;
|
||||||
|
} else if (!imageUrl.startsWith('http')) {
|
||||||
|
imageUrl = `${stashUrl}/performer/${performer.id}/${imageUrl}`;
|
||||||
|
}
|
||||||
|
document.getElementById('thumbnail_url').value = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (performer.details) document.getElementById('biography').value = performer.details;
|
||||||
|
if (performer.birthdate) document.getElementById('birth_date').value = performer.birthdate;
|
||||||
|
if (performer.death_date) document.getElementById('death_date').value = performer.death_date;
|
||||||
|
if (performer.country) document.getElementById('nationality').value = performer.country;
|
||||||
|
if (performer.ethnicity) document.getElementById('ethnicity').value = performer.ethnicity;
|
||||||
|
|
||||||
|
// Gender mapping
|
||||||
|
if (performer.gender) {
|
||||||
|
const genderSelect = document.getElementById('gender');
|
||||||
|
genderSelect.value = performer.gender.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Physical attributes
|
||||||
|
if (performer.height_cm) document.getElementById('height').value = performer.height_cm + 'cm';
|
||||||
|
if (performer.measurements) document.getElementById('measurements').value = performer.measurements;
|
||||||
|
if (performer.hair_color) document.getElementById('hair_color').value = performer.hair_color;
|
||||||
|
if (performer.eye_color) document.getElementById('eye_color').value = performer.eye_color;
|
||||||
|
if (performer.weight) document.getElementById('weight').value = performer.weight + 'kg';
|
||||||
|
|
||||||
|
// Body modifications
|
||||||
|
if (performer.piercings) document.getElementById('piercings').value = performer.piercings;
|
||||||
|
if (performer.tattoos) document.getElementById('tattoos').value = performer.tattoos;
|
||||||
|
|
||||||
|
// Aliases
|
||||||
|
if (performer.alias_list && Array.isArray(performer.alias_list)) {
|
||||||
|
document.getElementById('aliases').value = performer.alias_list.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Social media
|
||||||
|
if (performer.url) document.getElementById('website').value = performer.url;
|
||||||
|
|
||||||
|
// Adult-specific information
|
||||||
|
if (performer.career_length) {
|
||||||
|
const debutYear = extractDebutYear(performer.career_length);
|
||||||
|
const retirementYear = extractRetirementYear(performer.career_length);
|
||||||
|
if (debutYear) document.getElementById('debut_year').value = debutYear;
|
||||||
|
if (retirementYear) document.getElementById('retirement_year').value = retirementYear;
|
||||||
|
document.getElementById('active').checked = isActivePerformer(performer.career_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
//if (performer.scene_count) document.getElementById('scene_count').value = performer.scene_count;
|
||||||
|
|
||||||
|
// Additional Stash-specific fields
|
||||||
|
//if (performer.disambiguation) document.getElementById('birth_place').value = performer.disambiguation;
|
||||||
|
//if (performer.fake_tits !== undefined) document.getElementById('fake_tits').value = performer.fake_tits ? 'Yes' : 'No';
|
||||||
|
//if (performer.penis_length) document.getElementById('penis_length').value = performer.penis_length;
|
||||||
|
//if (performer.circumcised !== undefined) document.getElementById('circumcised').value = performer.circumcised ? 'Yes' : 'No';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
alert('No performers found matching the query');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data from Stash:', error);
|
||||||
|
alert('Error fetching data from Stash: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
// Reset button state
|
||||||
|
button.textContent = originalText;
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions for parsing Stash data
|
||||||
|
function extractDebutYear(careerLength) {
|
||||||
|
if (!careerLength) return null;
|
||||||
|
const match = careerLength.match(/(\d{4})\s*-\s*\d{4}/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRetirementYear(careerLength) {
|
||||||
|
if (!careerLength) return null;
|
||||||
|
const match = careerLength.match(/\d{4}\s*-\s*(\d{4})/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActivePerformer(careerLength) {
|
||||||
|
if (!careerLength) return false;
|
||||||
|
return careerLength.trim().endsWith('-');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -11,13 +11,13 @@
|
|||||||
<div class="text-center text-white">
|
<div class="text-center text-white">
|
||||||
<h1 class="text-3xl md:text-5xl font-bold mb-2">Actors & Performers</h1>
|
<h1 class="text-3xl md:text-5xl font-bold mb-2">Actors & Performers</h1>
|
||||||
<p class="text-lg md:text-xl opacity-90 mb-4">{{ pagination.total_items }} performer{{ pagination.total_items != 1 ? 's' : '' }}</p>
|
<p class="text-lg md:text-xl opacity-90 mb-4">{{ pagination.total_items }} performer{{ pagination.total_items != 1 ? 's' : '' }}</p>
|
||||||
|
{{dump(pagination)}}
|
||||||
<!-- Pagination in Hero -->
|
<!-- Pagination in Hero -->
|
||||||
{% if pagination.total_pages > 0 %}
|
{% if pagination.total_pages > 1 %}
|
||||||
<div class="flex items-center justify-center space-x-2">
|
<div class="flex items-center justify-center space-x-2">
|
||||||
<!-- Previous Button -->
|
<!-- Previous Button -->
|
||||||
{% if pagination.has_prev %}
|
{% if pagination.has_prev %}
|
||||||
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page={{ pagination.prev_page }}"
|
<a href="{{ path_for('actors.index', {}, {'page': pagination.prev_page, 'search': search, 'sort': sort, 'has_movies': filters.has_movies, 'has_tv_shows': filters.has_tv_shows, 'has_adult_videos': filters.has_adult_videos}) }}"
|
||||||
class="px-3 py-2 text-sm font-medium text-white bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg hover:bg-white/30 transition-colors flex items-center">
|
class="px-3 py-2 text-sm font-medium text-white bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg hover:bg-white/30 transition-colors flex items-center">
|
||||||
<svg class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
<!-- Next Button -->
|
<!-- Next Button -->
|
||||||
{% if pagination.has_next %}
|
{% if pagination.has_next %}
|
||||||
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page={{ pagination.next_page }}"
|
<a href="{{ path_for('actors.index', {}, {'page': pagination.next_page, 'search': search, 'sort': sort, 'has_movies': filters.has_movies, 'has_tv_shows': filters.has_tv_shows, 'has_adult_videos': filters.has_adult_videos}) }}"
|
||||||
class="px-3 py-2 text-sm font-medium text-white bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg hover:bg-white/30 transition-colors flex items-center">
|
class="px-3 py-2 text-sm font-medium text-white bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg hover:bg-white/30 transition-colors flex items-center">
|
||||||
Next
|
Next
|
||||||
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@@ -265,7 +265,7 @@
|
|||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<!-- Previous Button -->
|
<!-- Previous Button -->
|
||||||
{% if pagination.has_prev %}
|
{% if pagination.has_prev %}
|
||||||
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page={{ pagination.prev_page }}"
|
<a href="{{ path_for('actors.index', {}, {'page': pagination.prev_page, 'search': search, 'sort': sort, 'has_movies': filters.has_movies, 'has_tv_shows': filters.has_tv_shows, 'has_adult_videos': filters.has_adult_videos}) }}"
|
||||||
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
|
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
|
||||||
<svg class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||||
@@ -286,7 +286,7 @@
|
|||||||
{% set end_page = min(pagination.total_pages, pagination.current_page + 2) %}
|
{% set end_page = min(pagination.total_pages, pagination.current_page + 2) %}
|
||||||
|
|
||||||
{% if start_page > 1 %}
|
{% if start_page > 1 %}
|
||||||
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page=1"
|
<a href="{{ path_for('actors.index', {}, {'page': 1, 'search': search, 'sort': sort, 'has_movies': filters.has_movies, 'has_tv_shows': filters.has_tv_shows, 'has_adult_videos': filters.has_adult_videos}) }}"
|
||||||
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">1</a>
|
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">1</a>
|
||||||
{% if start_page > 2 %}
|
{% if start_page > 2 %}
|
||||||
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
|
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
{% if page_num == pagination.current_page %}
|
{% if page_num == pagination.current_page %}
|
||||||
<span class="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-lg">{{ page_num }}</span>
|
<span class="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-lg">{{ page_num }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page={{ page_num }}"
|
<a href="{{ path_for('actors.index', {}, {'page': page_num, 'search': search, 'sort': sort, 'has_movies': filters.has_movies, 'has_tv_shows': filters.has_tv_shows, 'has_adult_videos': filters.has_adult_videos}) }}"
|
||||||
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ page_num }}</a>
|
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ page_num }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -306,13 +306,13 @@
|
|||||||
{% if end_page < pagination.total_pages - 1 %}
|
{% if end_page < pagination.total_pages - 1 %}
|
||||||
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
|
<span class="px-2 py-2 text-sm font-medium text-gray-500">...</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page={{ pagination.total_pages }}"
|
<a href="{{ path_for('actors.index', {}, {'page': pagination.total_pages, 'search': search, 'sort': sort, 'has_movies': filters.has_movies, 'has_tv_shows': filters.has_tv_shows, 'has_adult_videos': filters.has_adult_videos}) }}"
|
||||||
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ pagination.total_pages }}</a>
|
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors">{{ pagination.total_pages }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Next Button -->
|
<!-- Next Button -->
|
||||||
{% if pagination.has_next %}
|
{% if pagination.has_next %}
|
||||||
<a href="{{ path_for('actors.index') }}?{% for key, value in app.request.query %}{{ key }}={{ value }}{% if not loop.last %}&{% endif %}{% endfor %}{% if app.request.query|length > 0 %}&{% endif %}page={{ pagination.next_page }}"
|
<a href="{{ path_for('actors.index', {}, {'page': pagination.next_page, 'search': search, 'sort': sort, 'has_movies': filters.has_movies, 'has_tv_shows': filters.has_tv_shows, 'has_adult_videos': filters.has_adult_videos}) }}"
|
||||||
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
|
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-colors flex items-center">
|
||||||
Next
|
Next
|
||||||
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="w-4 h-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
{% if metadata.nationality %}
|
{% if metadata.nationality %}
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-sm text-gray-600">Nationality</span>
|
<span class="text-sm text-gray-600">Nationality</span>
|
||||||
<span class="text-sm font-medium">{{ metadata.nationality }}</span>
|
<div class="text-sm font-medium flex items-center gap-2">
|
||||||
|
{{ country_flag(metadata.nationality) | raw }}
|
||||||
|
{{ metadata.nationality }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if metadata.death_date %}
|
{% if metadata.death_date %}
|
||||||
@@ -413,7 +416,7 @@
|
|||||||
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow">
|
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow">
|
||||||
<div class="aspect-[2/3] bg-gray-200">
|
<div class="aspect-[2/3] bg-gray-200">
|
||||||
{% if scene.poster_url %}
|
{% if scene.poster_url %}
|
||||||
<img src="/images/{{ scene.poster_url }}" alt="{{ scene.title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform">
|
<img src="{% if '/images/' in scene.poster_url %}{{ scene.poster_url }}{% else %}/images/{{ scene.poster_url }}{% endif %}" alt="{{ scene.title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-300 to-gray-400">
|
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-300 to-gray-400">
|
||||||
<svg class="text-gray-500 w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="text-gray-500 w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ $app->group('/api', function (RouteCollectorProxy $apiGroup) {
|
|||||||
$playniteGroup->post('/image/base64', 'App\Controllers\Api\PlayniteController:uploadImages')->setName('api.playnite.images');
|
$playniteGroup->post('/image/base64', 'App\Controllers\Api\PlayniteController:uploadImages')->setName('api.playnite.images');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Actor API endpoints
|
||||||
|
$apiGroup->group('/actors', function (RouteCollectorProxy $actorGroup) {
|
||||||
|
$actorGroup->post('/fetch-stash', 'App\Controllers\ActorController:fetchStashData')->setName('api.actors.fetch-stash');
|
||||||
|
});
|
||||||
|
|
||||||
// User authentication check (requires authentication)
|
// User authentication check (requires authentication)
|
||||||
$apiGroup->get('/v1/users/me', 'App\Controllers\Api\AuthController:checkAuth')->setName('api.auth.check')->add('App\Http\Middleware\AuthMiddleware');
|
$apiGroup->get('/v1/users/me', 'App\Controllers\Api\AuthController:checkAuth')->setName('api.auth.check')->add('App\Http\Middleware\AuthMiddleware');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user