mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
actor sync
This commit is contained in:
@@ -489,6 +489,90 @@ class ActorController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMissingStashReports(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$logDir = __DIR__ . '/../../storage/logs';
|
||||||
|
$reports = [];
|
||||||
|
|
||||||
|
if (is_dir($logDir)) {
|
||||||
|
$files = glob($logDir . '/missing_stash_actors_*.json');
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$filename = basename($file);
|
||||||
|
$fileData = json_decode(file_get_contents($file), true);
|
||||||
|
|
||||||
|
if ($fileData) {
|
||||||
|
$reports[] = [
|
||||||
|
'filename' => $filename,
|
||||||
|
'generated_at' => $fileData['generated_at'] ?? 'Unknown',
|
||||||
|
'total_missing' => $fileData['total_missing'] ?? 0,
|
||||||
|
'description' => $fileData['description'] ?? '',
|
||||||
|
'file_path' => $file
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by generation date (newest first)
|
||||||
|
usort($reports, function($a, $b) {
|
||||||
|
return strtotime($b['generated_at']) <=> strtotime($a['generated_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->jsonResponse($response, [
|
||||||
|
'reports' => $reports,
|
||||||
|
'total_reports' => count($reports)
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->jsonResponse($response->withStatus(500), [
|
||||||
|
'error' => 'Internal server error: ' . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncExistingPerformers(Request $request, Response $response, $args)
|
||||||
|
{
|
||||||
|
// Set PHP timeouts for long-running operations
|
||||||
|
ini_set('memory_limit', '2G');
|
||||||
|
ini_set('max_execution_time', 0); // Allow unlimited execution time
|
||||||
|
ini_set('max_input_time', 3600); // 1 hour for input processing
|
||||||
|
ini_set('default_socket_timeout', 3600); // 1 hour for socket operations
|
||||||
|
|
||||||
|
// Set headers to prevent timeouts
|
||||||
|
$response = $response->withHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get Stash configuration from database
|
||||||
|
$stmt = $this->pdo->prepare('SELECT * FROM sources WHERE name = ?');
|
||||||
|
$stmt->execute(['stash']);
|
||||||
|
$stashSource = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$stashSource) {
|
||||||
|
return $this->jsonResponse($response->withStatus(500), [
|
||||||
|
'error' => 'Stash source not configured in database'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Stash sync service
|
||||||
|
$stashSyncService = new \App\Services\StashSyncService($this->pdo, $stashSource);
|
||||||
|
|
||||||
|
// Run the existing performers sync
|
||||||
|
$results = $stashSyncService->syncExistingPerformers();
|
||||||
|
|
||||||
|
return $this->jsonResponse($response, [
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Existing performers sync completed',
|
||||||
|
'results' => $results
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->jsonResponse($response->withStatus(500), [
|
||||||
|
'error' => 'Internal server error: ' . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function index(Request $request, Response $response, $args)
|
public function index(Request $request, Response $response, $args)
|
||||||
{
|
{
|
||||||
$queryParams = $request->getQueryParams();
|
$queryParams = $request->getQueryParams();
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ class Database
|
|||||||
require_once $file;
|
require_once $file;
|
||||||
|
|
||||||
$className = self::getMigrationClassName($file);
|
$className = self::getMigrationClassName($file);
|
||||||
$migration = new $className();
|
$pdo = self::getInstance();
|
||||||
|
$migration = new $className($pdo);
|
||||||
$migration->up();
|
$migration->up();
|
||||||
|
|
||||||
// Record the migration
|
// Record the migration
|
||||||
@@ -112,14 +113,20 @@ class Database
|
|||||||
{
|
{
|
||||||
$content = file_get_contents($file);
|
$content = file_get_contents($file);
|
||||||
|
|
||||||
|
// Extract namespace and class name from PHP file
|
||||||
|
$namespace = '';
|
||||||
|
if (preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatches)) {
|
||||||
|
$namespace = $namespaceMatches[1] . '\\';
|
||||||
|
}
|
||||||
|
|
||||||
// Extract class name from PHP file
|
// Extract class name from PHP file
|
||||||
if (preg_match('/class\s+(\w+)\s+extends\s+Migration/', $content, $matches)) {
|
if (preg_match('/class\s+(\w+)\s+extends\s+Migration/', $content, $matches)) {
|
||||||
return $matches[1];
|
return $namespace . $matches[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: convert filename to class name
|
// Fallback: convert filename to class name
|
||||||
$filename = basename($file, '.php');
|
$filename = basename($file, '.php');
|
||||||
return str_replace(' ', '', ucwords(str_replace('_', ' ', $filename)));
|
return $namespace . str_replace(' ', '', ucwords(str_replace('_', ' ', $filename)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function seed(): void
|
public static function seed(): void
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ class AdultVideo extends Model
|
|||||||
'title',
|
'title',
|
||||||
'overview',
|
'overview',
|
||||||
'poster_url',
|
'poster_url',
|
||||||
|
'poster_aspect_ratio',
|
||||||
'backdrop_url',
|
'backdrop_url',
|
||||||
|
'backdrop_aspect_ratio',
|
||||||
'rating',
|
'rating',
|
||||||
'runtime_minutes',
|
'runtime_minutes',
|
||||||
'release_date',
|
'release_date',
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class Game extends Model
|
|||||||
'platform_game_id',
|
'platform_game_id',
|
||||||
'steam_app_id',
|
'steam_app_id',
|
||||||
'image_url',
|
'image_url',
|
||||||
|
'image_aspect_ratio',
|
||||||
'banner_url',
|
'banner_url',
|
||||||
'rating',
|
'rating',
|
||||||
'playtime_minutes',
|
'playtime_minutes',
|
||||||
@@ -27,6 +28,7 @@ class Game extends Model
|
|||||||
'platform_achievements',
|
'platform_achievements',
|
||||||
'background_image',
|
'background_image',
|
||||||
'cover_image',
|
'cover_image',
|
||||||
|
'cover_aspect_ratio',
|
||||||
'icon',
|
'icon',
|
||||||
'genres_json',
|
'genres_json',
|
||||||
'developers_json',
|
'developers_json',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Services;
|
|||||||
|
|
||||||
use App\Models\Game;
|
use App\Models\Game;
|
||||||
use App\Models\Source;
|
use App\Models\Source;
|
||||||
|
use App\Utils\ImageAspectRatioDetector;
|
||||||
|
|
||||||
class PlayniteImportService
|
class PlayniteImportService
|
||||||
{
|
{
|
||||||
@@ -100,6 +101,7 @@ class PlayniteImportService
|
|||||||
// Rich media
|
// Rich media
|
||||||
'background_image' => $game['BackgroundImage'] ?? null,
|
'background_image' => $game['BackgroundImage'] ?? null,
|
||||||
'cover_image' => $game['CoverImage'] ?? null,
|
'cover_image' => $game['CoverImage'] ?? null,
|
||||||
|
'cover_aspect_ratio' => $this->detectAspectRatio($game['CoverImage'] ?? null),
|
||||||
'icon' => $game['Icon'] ?? null,
|
'icon' => $game['Icon'] ?? null,
|
||||||
|
|
||||||
// Multiple entities as JSON
|
// Multiple entities as JSON
|
||||||
@@ -410,6 +412,18 @@ class PlayniteImportService
|
|||||||
$gameModel->update($gameId, $gameData);
|
$gameModel->update($gameId, $gameData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect aspect ratio for an image URL
|
||||||
|
*/
|
||||||
|
private function detectAspectRatio(?string $imageUrl): ?float
|
||||||
|
{
|
||||||
|
if (!$imageUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImageAspectRatioDetector::detectAspectRatio($imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a value to boolean, handling empty strings properly
|
* Convert a value to boolean, handling empty strings properly
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Utils\ImageDownloader;
|
use App\Utils\ImageDownloader;
|
||||||
|
use App\Utils\ImageAspectRatioDetector;
|
||||||
use App\Models\AdultVideo;
|
use App\Models\AdultVideo;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use PDO;
|
use PDO;
|
||||||
@@ -469,9 +470,31 @@ class StashSyncService extends BaseSyncService
|
|||||||
$performers = $sceneData['performers'] ?? [];
|
$performers = $sceneData['performers'] ?? [];
|
||||||
$actors = $this->syncActors($performers);
|
$actors = $this->syncActors($performers);
|
||||||
|
|
||||||
|
// Detect aspect ratios for downloaded images
|
||||||
|
$posterAspectRatio = null;
|
||||||
|
$backdropAspectRatio = null;
|
||||||
|
|
||||||
|
if (!empty($sceneData['local_cover_path'])) {
|
||||||
|
$posterAspectRatio = ImageAspectRatioDetector::detectAspectRatio($sceneData['local_cover_path']);
|
||||||
|
if ($posterAspectRatio) {
|
||||||
|
$this->logProgress("Detected poster aspect ratio: {$posterAspectRatio}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($sceneData['local_screenshot_path'])) {
|
||||||
|
$backdropAspectRatio = ImageAspectRatioDetector::detectAspectRatio($sceneData['local_screenshot_path']);
|
||||||
|
if ($backdropAspectRatio) {
|
||||||
|
$this->logProgress("Detected backdrop aspect ratio: {$backdropAspectRatio}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$sceneData = [
|
$sceneData = [
|
||||||
'title' => $sceneData['title'] ?: 'Untitled Scene',
|
'title' => $sceneData['title'] ?: 'Untitled Scene',
|
||||||
'overview' => $sceneData['details'] ?? null,
|
'overview' => $sceneData['details'] ?? null,
|
||||||
|
'poster_url' => $sceneData['local_cover_path'] ?? null,
|
||||||
|
'poster_aspect_ratio' => $posterAspectRatio,
|
||||||
|
'backdrop_url' => $sceneData['local_screenshot_path'] ?? null,
|
||||||
|
'backdrop_aspect_ratio' => $backdropAspectRatio,
|
||||||
'release_date' => $sceneData['date'] ? date('Y-m-d', strtotime($sceneData['date'])) : null,
|
'release_date' => $sceneData['date'] ? date('Y-m-d', strtotime($sceneData['date'])) : null,
|
||||||
'runtime_minutes' => !empty($sceneData['files'][0]['duration']) ? round($sceneData['files'][0]['duration'] / 60) : null,
|
'runtime_minutes' => !empty($sceneData['files'][0]['duration']) ? round($sceneData['files'][0]['duration'] / 60) : null,
|
||||||
'rating' => $sceneData['rating100'] ? $sceneData['rating100'] / 100 : null, // Convert from 0-100 to 0-10
|
'rating' => $sceneData['rating100'] ? $sceneData['rating100'] / 100 : null, // Convert from 0-100 to 0-10
|
||||||
@@ -869,6 +892,313 @@ class StashSyncService extends BaseSyncService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync existing performers with Stash data
|
||||||
|
*/
|
||||||
|
public function syncExistingPerformers(): array
|
||||||
|
{
|
||||||
|
$results = [
|
||||||
|
'processed' => 0,
|
||||||
|
'updated' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'not_found_in_stash' => [],
|
||||||
|
'errors' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->logProgress('Starting existing performers sync with Stash...');
|
||||||
|
|
||||||
|
// Get all existing actors from database
|
||||||
|
$stmt = $this->pdo->prepare("SELECT id, name, metadata FROM actors ORDER BY name ASC");
|
||||||
|
$stmt->execute();
|
||||||
|
$existingActors = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$this->logProgress("Found " . count($existingActors) . " existing actors to check");
|
||||||
|
|
||||||
|
foreach ($existingActors as $actor) {
|
||||||
|
try {
|
||||||
|
$this->logProgress("Processing actor: {$actor['name']} (ID: {$actor['id']})");
|
||||||
|
|
||||||
|
// Search for this actor in Stash
|
||||||
|
$stashPerformers = $this->searchStashPerformer($actor['name']);
|
||||||
|
|
||||||
|
if (empty($stashPerformers)) {
|
||||||
|
$this->logProgress("No matching performer found in Stash for: {$actor['name']}");
|
||||||
|
$results['not_found_in_stash'][] = [
|
||||||
|
'id' => $actor['id'],
|
||||||
|
'name' => $actor['name'],
|
||||||
|
'local_metadata' => json_decode($actor['metadata'] ?? '{}', true)
|
||||||
|
];
|
||||||
|
$results['skipped']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the best match (exact name match preferred)
|
||||||
|
$bestMatch = null;
|
||||||
|
foreach ($stashPerformers as $performer) {
|
||||||
|
if (strtolower(trim($performer['name'])) === strtolower(trim($actor['name']))) {
|
||||||
|
$bestMatch = $performer;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no exact match, use the first result
|
||||||
|
if (!$bestMatch && !empty($stashPerformers)) {
|
||||||
|
$bestMatch = $stashPerformers[0];
|
||||||
|
$this->logProgress("Using closest match for {$actor['name']}: {$bestMatch['name']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bestMatch) {
|
||||||
|
// Update the actor with Stash data
|
||||||
|
$this->updateActorWithStashData($actor['id'], $bestMatch);
|
||||||
|
$results['updated']++;
|
||||||
|
$this->logProgress("Updated actor {$actor['name']} with Stash data");
|
||||||
|
} else {
|
||||||
|
$results['skipped']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$results['processed']++;
|
||||||
|
|
||||||
|
// Add a small delay to avoid overwhelming the Stash server
|
||||||
|
usleep(100000); // 0.1 seconds
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errorMsg = "Failed to sync actor {$actor['name']}: " . $e->getMessage();
|
||||||
|
$results['errors'][] = $errorMsg;
|
||||||
|
$this->logProgress("ERROR: " . $errorMsg);
|
||||||
|
$results['processed']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logProgress("Existing performers sync completed: {$results['updated']} updated, {$results['skipped']} skipped, " . count($results['errors']) . " errors");
|
||||||
|
|
||||||
|
// Save missing actors report
|
||||||
|
$this->saveMissingActorsReport($results['not_found_in_stash']);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->logProgress("Error during existing performers sync: " . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a report of actors not found in Stash
|
||||||
|
*/
|
||||||
|
private function saveMissingActorsReport(array $missingActors): void
|
||||||
|
{
|
||||||
|
if (empty($missingActors)) {
|
||||||
|
$this->logProgress("No missing actors to report");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportPath = __DIR__ . '/../../storage/logs/missing_stash_actors_' . date('Y-m-d_H-i-s') . '.json';
|
||||||
|
|
||||||
|
// Create logs directory if it doesn't exist
|
||||||
|
$logDir = dirname($reportPath);
|
||||||
|
if (!is_dir($logDir)) {
|
||||||
|
mkdir($logDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportData = [
|
||||||
|
'generated_at' => date('Y-m-d H:i:s'),
|
||||||
|
'total_missing' => count($missingActors),
|
||||||
|
'missing_actors' => $missingActors,
|
||||||
|
'description' => 'These actors exist in your local database but were not found in Stash. You can create them in Stash for future syncs.'
|
||||||
|
];
|
||||||
|
|
||||||
|
$jsonReport = json_encode($reportData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
if (file_put_contents($reportPath, $jsonReport)) {
|
||||||
|
$this->logProgress("Missing actors report saved to: {$reportPath}");
|
||||||
|
$this->logProgress("Found " . count($missingActors) . " actors not in Stash");
|
||||||
|
} else {
|
||||||
|
$this->logProgress("Failed to save missing actors report");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for a performer in Stash by name
|
||||||
|
*/
|
||||||
|
private function searchStashPerformer(string $name): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$query = '
|
||||||
|
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' => $name,
|
||||||
|
'per_page' => 5, // Get a few results to find the best match
|
||||||
|
'sort' => 'name',
|
||||||
|
'direction' => 'ASC'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->httpClient->post("{$this->baseUrl}/graphql", [
|
||||||
|
'json' => [
|
||||||
|
'query' => $query,
|
||||||
|
'variables' => $variables
|
||||||
|
],
|
||||||
|
'timeout' => 30
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = json_decode($response->getBody(), true);
|
||||||
|
|
||||||
|
if (!isset($data['data']['findPerformers']['performers'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data['data']['findPerformers']['performers'];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->logProgress('Failed to search Stash for performer: ' . $e->getMessage());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing actor with Stash performer data
|
||||||
|
*/
|
||||||
|
private function updateActorWithStashData(int $actorId, array $performer): void
|
||||||
|
{
|
||||||
|
// Get existing actor data
|
||||||
|
$stmt = $this->pdo->prepare("SELECT metadata FROM actors WHERE id = :id");
|
||||||
|
$stmt->execute(['id' => $actorId]);
|
||||||
|
$existingActor = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$existingActor) {
|
||||||
|
throw new Exception("Actor with ID {$actorId} not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingMetadata = json_decode($existingActor['metadata'] ?? '{}', true);
|
||||||
|
|
||||||
|
// Prepare updated metadata from Stash performer data
|
||||||
|
$updatedMetadata = [
|
||||||
|
'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' => []
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Merge with existing metadata, preferring new Stash data but keeping any custom fields
|
||||||
|
$finalMetadata = array_merge($existingMetadata, $updatedMetadata);
|
||||||
|
|
||||||
|
// Try to download/update performer image if available and not already set
|
||||||
|
$thumbnailPath = null;
|
||||||
|
$imagePath = $performer['image_path'] ?? null;
|
||||||
|
if ($imagePath && empty($existingMetadata['local_image_path'])) {
|
||||||
|
try {
|
||||||
|
// Handle different image path formats from Stash
|
||||||
|
if (strpos($imagePath, 'http') === 0) {
|
||||||
|
// Already a full URL
|
||||||
|
$imageUrl = $imagePath;
|
||||||
|
} elseif (strpos($imagePath, '/') === 0) {
|
||||||
|
// Absolute path from Stash root
|
||||||
|
$imageUrl = "{$this->baseUrl}" . $imagePath;
|
||||||
|
} else {
|
||||||
|
// Relative path - assume it's in performer images directory
|
||||||
|
$imageUrl = "{$this->baseUrl}/performer/" . $performer['id'] . "/" . $imagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the constructed URL
|
||||||
|
if (filter_var($imageUrl, FILTER_VALIDATE_URL)) {
|
||||||
|
$this->logProgress("Downloading image for performer {$performer['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 performer image: " . $localImagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->logProgress("Exception downloading performer image for {$performer['name']}: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the actor record
|
||||||
|
$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' => $actorId,
|
||||||
|
'thumbnail_path' => $thumbnailPath,
|
||||||
|
'metadata' => json_encode($finalMetadata)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
protected function executeCleanup(): void
|
protected function executeCleanup(): void
|
||||||
{
|
{
|
||||||
$this->logProgress("Starting cleanup - detecting deleted media in Stash...");
|
$this->logProgress("Starting cleanup - detecting deleted media in Stash...");
|
||||||
|
|||||||
164
app/Utils/ImageAspectRatioDetector.php
Normal file
164
app/Utils/ImageAspectRatioDetector.php
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Utils;
|
||||||
|
|
||||||
|
class ImageAspectRatioDetector
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Detect the aspect ratio of an image from its URL
|
||||||
|
*
|
||||||
|
* @param string $imageUrl The URL of the image to analyze
|
||||||
|
* @return float|null The aspect ratio (width/height) or null if detection fails
|
||||||
|
*/
|
||||||
|
public static function detectAspectRatio(string $imageUrl): ?float
|
||||||
|
{
|
||||||
|
if (empty($imageUrl)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle local images (relative URLs)
|
||||||
|
if (!filter_var($imageUrl, FILTER_VALIDATE_URL)) {
|
||||||
|
$imageUrl = self::resolveLocalImagePath($imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get image dimensions
|
||||||
|
$imageInfo = @getimagesize($imageUrl);
|
||||||
|
|
||||||
|
if (!$imageInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$width = $imageInfo[0];
|
||||||
|
$height = $imageInfo[1];
|
||||||
|
|
||||||
|
if ($width <= 0 || $height <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate aspect ratio (width/height)
|
||||||
|
$aspectRatio = round($width / $height, 3);
|
||||||
|
|
||||||
|
// Validate aspect ratio is reasonable (between 0.1 and 10)
|
||||||
|
if ($aspectRatio < 0.1 || $aspectRatio > 10) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $aspectRatio;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log error but don't throw - aspect ratio detection should be non-blocking
|
||||||
|
error_log("Failed to detect aspect ratio for image {$imageUrl}: " . $e->getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve local image path for getimagesize
|
||||||
|
*
|
||||||
|
* @param string $relativeUrl The relative URL of the image
|
||||||
|
* @return string The absolute file path
|
||||||
|
*/
|
||||||
|
private static function resolveLocalImagePath(string $relativeUrl): string
|
||||||
|
{
|
||||||
|
// Remove leading slash if present
|
||||||
|
$relativeUrl = ltrim($relativeUrl, '/');
|
||||||
|
|
||||||
|
// Define common image directories
|
||||||
|
$possiblePaths = [
|
||||||
|
__DIR__ . '/../../public/' . $relativeUrl,
|
||||||
|
__DIR__ . '/../../public/images/' . $relativeUrl,
|
||||||
|
__DIR__ . '/../../public/media/' . $relativeUrl,
|
||||||
|
__DIR__ . '/../../storage/' . $relativeUrl,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($possiblePaths as $path) {
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no local file found, return the original URL (might be external)
|
||||||
|
return $relativeUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a CSS aspect ratio class based on the aspect ratio value
|
||||||
|
*
|
||||||
|
* @param float|null $aspectRatio The aspect ratio value
|
||||||
|
* @param string $defaultClass The default class to use if aspect ratio is null
|
||||||
|
* @return string The CSS class for the aspect ratio
|
||||||
|
*/
|
||||||
|
public static function getAspectRatioClass(?float $aspectRatio, string $defaultClass = 'aspect-[1/1]'): string
|
||||||
|
{
|
||||||
|
if ($aspectRatio === null) {
|
||||||
|
return $defaultClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common aspect ratios with nice round numbers
|
||||||
|
if (abs($aspectRatio - 1.333) < 0.05) { // 4:3
|
||||||
|
return 'aspect-[4/3]';
|
||||||
|
} elseif (abs($aspectRatio - 1.5) < 0.05) { // 3:2
|
||||||
|
return 'aspect-[3/2]';
|
||||||
|
} elseif (abs($aspectRatio - 1.667) < 0.05) { // 5:3
|
||||||
|
return 'aspect-[5/3]';
|
||||||
|
} elseif (abs($aspectRatio - 1.778) < 0.05) { // 16:9
|
||||||
|
return 'aspect-[16/9]';
|
||||||
|
} elseif (abs($aspectRatio - 0.667) < 0.05) { // 2:3
|
||||||
|
return 'aspect-[2/3]';
|
||||||
|
} elseif (abs($aspectRatio - 0.75) < 0.05) { // 3:4
|
||||||
|
return 'aspect-[3/4]';
|
||||||
|
} elseif (abs($aspectRatio - 1.0) < 0.05) { // 1:1
|
||||||
|
return 'aspect-[1/1]';
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other ratios, use a custom aspect ratio class
|
||||||
|
// Tailwind doesn't support dynamic aspect ratios, so we'll use a data attribute approach
|
||||||
|
return 'aspect-custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get inline styles for custom aspect ratios
|
||||||
|
*
|
||||||
|
* @param float|null $aspectRatio The aspect ratio value
|
||||||
|
* @return string CSS style string for custom aspect ratios
|
||||||
|
*/
|
||||||
|
public static function getCustomAspectRatioStyle(?float $aspectRatio): string
|
||||||
|
{
|
||||||
|
if ($aspectRatio === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert aspect ratio to padding-bottom percentage for aspect ratio simulation
|
||||||
|
$paddingBottom = (1 / $aspectRatio) * 100;
|
||||||
|
return "padding-bottom: {$paddingBottom}%;";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch detect aspect ratios for multiple images
|
||||||
|
*
|
||||||
|
* @param array $imageUrls Array of image URLs
|
||||||
|
* @return array Array of aspect ratios corresponding to the input URLs
|
||||||
|
*/
|
||||||
|
public static function batchDetectAspectRatios(array $imageUrls): array
|
||||||
|
{
|
||||||
|
$aspectRatios = [];
|
||||||
|
|
||||||
|
foreach ($imageUrls as $url) {
|
||||||
|
$aspectRatios[] = self::detectAspectRatio($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $aspectRatios;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if an aspect ratio value is reasonable
|
||||||
|
*
|
||||||
|
* @param float $aspectRatio The aspect ratio to validate
|
||||||
|
* @return bool True if the aspect ratio is valid
|
||||||
|
*/
|
||||||
|
public static function isValidAspectRatio(float $aspectRatio): bool
|
||||||
|
{
|
||||||
|
return $aspectRatio > 0.1 && $aspectRatio < 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use App\Database\Database;
|
||||||
|
|
||||||
|
class AddAspectRatioFields extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
$capsule = Database::getCapsule();
|
||||||
|
|
||||||
|
// Add aspect_ratio field to games table
|
||||||
|
$capsule->schema()->table('games', function (Blueprint $table) {
|
||||||
|
$table->decimal('cover_aspect_ratio', 4, 3)->nullable()->after('cover_image');
|
||||||
|
$table->decimal('image_aspect_ratio', 4, 3)->nullable()->after('image_url');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add aspect_ratio field to adult_videos table
|
||||||
|
$capsule->schema()->table('adult_videos', function (Blueprint $table) {
|
||||||
|
$table->decimal('poster_aspect_ratio', 4, 3)->nullable()->after('poster_url');
|
||||||
|
$table->decimal('backdrop_aspect_ratio', 4, 3)->nullable()->after('backdrop_url');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
$capsule = Database::getCapsule();
|
||||||
|
|
||||||
|
// Remove aspect_ratio fields from games table
|
||||||
|
$capsule->schema()->table('games', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['cover_aspect_ratio', 'image_aspect_ratio']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove aspect_ratio fields from adult_videos table
|
||||||
|
$capsule->schema()->table('adult_videos', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['poster_aspect_ratio', 'backdrop_aspect_ratio']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,17 @@ server {
|
|||||||
fastcgi_param HTTP_X_FORWARDED_PROTO $scheme;
|
fastcgi_param HTTP_X_FORWARDED_PROTO $scheme;
|
||||||
fastcgi_param HTTP_X_REAL_IP $remote_addr;
|
fastcgi_param HTTP_X_REAL_IP $remote_addr;
|
||||||
fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
|
fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
# Timeout settings for long-running operations (like sync processes)
|
||||||
|
fastcgi_read_timeout 1800; # 30 minutes
|
||||||
|
fastcgi_send_timeout 1800; # 30 minutes
|
||||||
|
fastcgi_connect_timeout 300; # 5 minutes
|
||||||
|
|
||||||
|
# Buffer settings for large responses
|
||||||
|
fastcgi_buffer_size 128k;
|
||||||
|
fastcgi_buffers 256 16k;
|
||||||
|
fastcgi_busy_buffers_size 256k;
|
||||||
|
fastcgi_temp_file_write_size 256k;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Security headers
|
# Security headers
|
||||||
|
|||||||
@@ -85,6 +85,28 @@
|
|||||||
Cleanup
|
Cleanup
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{% elseif source.name == 'stash' %}
|
||||||
|
<!-- Stash-specific sync options -->
|
||||||
|
<div class="d-flex gap-2 mb-2">
|
||||||
|
<button onclick="startSync({{ source.id }}, 'full')"
|
||||||
|
class="btn btn-primary btn-sm flex-fill"
|
||||||
|
data-source-id="{{ source.id }}">
|
||||||
|
Full Sync
|
||||||
|
</button>
|
||||||
|
<button onclick="startSync({{ source.id }}, 'incremental')"
|
||||||
|
class="btn btn-secondary btn-sm flex-fill"
|
||||||
|
data-source-id="{{ source.id }}">
|
||||||
|
Incremental
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Stash Performer Sync Button -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<button onclick="syncStashPerformers()"
|
||||||
|
class="btn btn-info btn-sm w-100"
|
||||||
|
id="stash-performer-sync-btn">
|
||||||
|
<i class="bi bi-person-lines-fill me-1"></i>Sync Performers
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Standard sync options for other sources -->
|
<!-- Standard sync options for other sources -->
|
||||||
<div class="d-flex gap-2 mb-2">
|
<div class="d-flex gap-2 mb-2">
|
||||||
@@ -487,6 +509,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncStashPerformers() {
|
||||||
|
const button = document.getElementById('stash-performer-sync-btn');
|
||||||
|
const originalText = button.innerHTML;
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Syncing...';
|
||||||
|
|
||||||
|
// Make AJAX request to sync performers
|
||||||
|
fetch('/api/actors/sync-existing-stash', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert(`Stash performer sync completed!\n\nProcessed: ${data.results.processed}\nUpdated: ${data.results.updated}\nSkipped: ${data.results.skipped}\nNot found in Stash: ${data.results.not_found_in_stash.length}\n\nCheck the logs for detailed information.`);
|
||||||
|
} else {
|
||||||
|
alert('Error syncing performers: ' + (data.error || 'Unknown error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable button
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = originalText;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Failed to sync performers: ' + error.message);
|
||||||
|
|
||||||
|
// Re-enable button
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = originalText;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup intervals on page unload
|
// Cleanup intervals on page unload
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
Object.values(syncIntervals).forEach(interval => clearInterval(interval));
|
Object.values(syncIntervals).forEach(interval => clearInterval(interval));
|
||||||
|
|||||||
@@ -63,9 +63,9 @@
|
|||||||
<h5 class="mt-3" id="syncStatus">Idle</h5>
|
<h5 class="mt-3" id="syncStatus">Idle</h5>
|
||||||
<p class="text-muted mb-0">Last sync: <span id="lastSyncTime">Never</span></p>
|
<p class="text-muted mb-0">Last sync: <span id="lastSyncTime">Never</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="d-flex justify-content-between mb-1">
|
<div class="d-flex justify-content-between mb-1">
|
||||||
<span>Movies</span>
|
<span>Movies</span>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<div id="movieProgress" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
|
<div id="movieProgress" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="d-flex justify-content-between mb-1">
|
<div class="d-flex justify-content-between mb-1">
|
||||||
<span>TV Shows</span>
|
<span>TV Shows</span>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<div id="tvShowProgress" class="progress-bar bg-info" role="progressbar" style="width: 0%"></div>
|
<div id="tvShowProgress" class="progress-bar bg-info" role="progressbar" style="width: 0%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="d-flex justify-content-between mb-1">
|
<div class="d-flex justify-content-between mb-1">
|
||||||
<span>Music</span>
|
<span>Music</span>
|
||||||
@@ -95,6 +95,16 @@
|
|||||||
<div id="musicProgress" class="progress-bar bg-warning" role="progressbar" style="width: 0%"></div>
|
<div id="musicProgress" class="progress-bar bg-warning" role="progressbar" style="width: 0%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-content-between mb-1">
|
||||||
|
<span>Adult Videos</span>
|
||||||
|
<span id="adultVideoCount">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 5px;">
|
||||||
|
<div id="adultVideoProgress" class="progress-bar bg-danger" role="progressbar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -107,6 +117,9 @@
|
|||||||
<span><i class="bi bi-search me-2"></i> Scan Libraries</span>
|
<span><i class="bi bi-search me-2"></i> Scan Libraries</span>
|
||||||
<span class="badge bg-primary rounded-pill" id="pendingScans">0</span>
|
<span class="badge bg-primary rounded-pill" id="pendingScans">0</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="list-group-item list-group-item-action" id="syncStashPerformers">
|
||||||
|
<i class="bi bi-person-lines-fill me-2"></i> Sync Stash Performers
|
||||||
|
</button>
|
||||||
<button class="list-group-item list-group-item-action">
|
<button class="list-group-item list-group-item-action">
|
||||||
<i class="bi bi-arrow-clockwise me-2"></i> Update Metadata
|
<i class="bi bi-arrow-clockwise me-2"></i> Update Metadata
|
||||||
</button>
|
</button>
|
||||||
@@ -248,6 +261,13 @@
|
|||||||
$('#scanLibraries').click(function() {
|
$('#scanLibraries').click(function() {
|
||||||
startSync('scan');
|
startSync('scan');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle sync Stash performers
|
||||||
|
$('#syncStashPerformers').click(function() {
|
||||||
|
if (confirm('Are you sure you want to sync existing performers with Stash? This will update your local performer data with information from Stash.')) {
|
||||||
|
syncStashPerformers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Handle cancel sync
|
// Handle cancel sync
|
||||||
$('#cancelSync').click(function() {
|
$('#cancelSync').click(function() {
|
||||||
@@ -294,6 +314,35 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to sync Stash performers
|
||||||
|
function syncStashPerformers() {
|
||||||
|
// Show loading state
|
||||||
|
$('#syncStashPerformers').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Syncing...');
|
||||||
|
|
||||||
|
// Make AJAX request to sync performers
|
||||||
|
$.post('/api/actors/sync-existing-stash', function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
addLog('Stash performer sync completed successfully', 'success');
|
||||||
|
addLog('Processed: ' + response.results.processed + ', Updated: ' + response.results.updated + ', Skipped: ' + response.results.skipped, 'info');
|
||||||
|
|
||||||
|
if (response.results.not_found_in_stash && response.results.not_found_in_stash.length > 0) {
|
||||||
|
addLog('Found ' + response.results.not_found_in_stash.length + ' performers not in Stash - check reports', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.results.errors && response.results.errors.length > 0) {
|
||||||
|
addLog('Encountered ' + response.results.errors.length + ' errors during sync', 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addLog('Error syncing performers: ' + (response.error || 'Unknown error'), 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#syncStashPerformers').prop('disabled', false).html('<i class="bi bi-person-lines-fill me-2"></i> Sync Stash Performers');
|
||||||
|
}).fail(function(xhr, status, error) {
|
||||||
|
addLog('Failed to sync performers: ' + error, 'error');
|
||||||
|
$('#syncStashPerformers').prop('disabled', false).html('<i class="bi bi-person-lines-fill me-2"></i> Sync Stash Performers');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Function to cancel sync
|
// Function to cancel sync
|
||||||
function cancelSync() {
|
function cancelSync() {
|
||||||
$.post('{{ path_for("admin.sync.cancel") }}', function(response) {
|
$.post('{{ path_for("admin.sync.cancel") }}', function(response) {
|
||||||
|
|||||||
@@ -359,7 +359,7 @@
|
|||||||
{% for movie in movies %}
|
{% for movie in movies %}
|
||||||
<div class="group relative bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1">
|
<div class="group relative bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1">
|
||||||
{% if movie.poster_url %}
|
{% if movie.poster_url %}
|
||||||
<div class="relative aspect-[2/3] overflow-hidden">
|
<div class="relative {{ movie.poster_aspect_ratio ? 'aspect-custom' : 'aspect-[2/3]' }} overflow-hidden"{% if movie.poster_aspect_ratio %} style="padding-bottom: {{ (1 / movie.poster_aspect_ratio) * 100 }}%;"{% endif %}>
|
||||||
<img src="{{ movie.poster_url }}" alt="{{ movie.title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300">
|
<img src="{{ movie.poster_url }}" alt="{{ movie.title }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300">
|
||||||
<!-- Overlay with movie info -->
|
<!-- Overlay with movie info -->
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||||
|
|||||||
@@ -305,7 +305,7 @@
|
|||||||
{% for game in games %}
|
{% for game in games %}
|
||||||
<div class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden h-full">
|
<div class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden h-full">
|
||||||
{% if game.image_url %}
|
{% if game.image_url %}
|
||||||
<div class="relative aspect-[3/4] overflow-hidden">
|
<div class="relative {{ game.cover_aspect_ratio ? 'aspect-custom' : 'aspect-[3/4]' }} overflow-hidden"{% if game.cover_aspect_ratio %} style="padding-bottom: {{ (1 / game.cover_aspect_ratio) * 100 }}%;"{% endif %}>
|
||||||
<img src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}" class="w-full h-full object-cover">
|
<img src="/images/playnite/{{ game.image_url }}" alt="{{ game.title }}" class="w-full h-full object-cover">
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ $app->group('/api', function (RouteCollectorProxy $apiGroup) {
|
|||||||
// Actor API endpoints
|
// Actor API endpoints
|
||||||
$apiGroup->group('/actors', function (RouteCollectorProxy $actorGroup) {
|
$apiGroup->group('/actors', function (RouteCollectorProxy $actorGroup) {
|
||||||
$actorGroup->post('/fetch-stash', 'App\Controllers\ActorController:fetchStashData')->setName('api.actors.fetch-stash');
|
$actorGroup->post('/fetch-stash', 'App\Controllers\ActorController:fetchStashData')->setName('api.actors.fetch-stash');
|
||||||
|
$actorGroup->post('/sync-existing-stash', 'App\Controllers\ActorController:syncExistingPerformers')->setName('api.actors.sync-existing-stash');
|
||||||
|
$actorGroup->get('/missing-stash-reports', 'App\Controllers\ActorController:getMissingStashReports')->setName('api.actors.missing-reports');
|
||||||
});
|
});
|
||||||
|
|
||||||
// User authentication check (requires authentication)
|
// User authentication check (requires authentication)
|
||||||
|
|||||||
Reference in New Issue
Block a user