mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
1385 lines
56 KiB
PHP
1385 lines
56 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Utils\ImageDownloader;
|
|
use App\Utils\ImageAspectRatioDetector;
|
|
use App\Models\AdultVideo;
|
|
use GuzzleHttp\Client;
|
|
use PDO;
|
|
use Exception;
|
|
|
|
class StashSyncService extends BaseSyncService
|
|
{
|
|
private Client $httpClient;
|
|
private ?string $apiKey;
|
|
private string $baseUrl;
|
|
private ImageDownloader $imageDownloader;
|
|
private int $processedCount = 0;
|
|
private int $newCount = 0;
|
|
private int $updatedCount = 0;
|
|
|
|
public function __construct(PDO $pdo, array $source, ?int $existingSyncLogId = null)
|
|
{
|
|
parent::__construct($pdo, $source, $existingSyncLogId);
|
|
|
|
// Initialize properties first before using them
|
|
$this->apiKey = $source['api_key'];
|
|
$this->baseUrl = rtrim($source['api_url'], '/');
|
|
|
|
$this->httpClient = new Client([
|
|
'timeout' => 60, // Stash can be slow
|
|
'headers' => [
|
|
'User-Agent' => 'MediaCollector/1.0',
|
|
'Content-Type' => 'application/json',
|
|
'ApiKey' => $this->apiKey // Stash API key for authentication
|
|
],
|
|
'verify' => false // Disable SSL verification for problematic servers
|
|
]);
|
|
|
|
$this->imageDownloader = new ImageDownloader(__DIR__ . '/../../storage/images', $this->apiKey);
|
|
}
|
|
|
|
protected function executeSync(string $syncType): void
|
|
{
|
|
if (empty($this->apiKey) || empty($this->baseUrl)) {
|
|
throw new Exception('Stash API key and URL not configured');
|
|
}
|
|
|
|
$this->logProgress('Starting Stash library sync...');
|
|
|
|
// Sync scenes (movies)
|
|
$this->syncScenes();
|
|
|
|
// Sync movies (if Stash has movie support)
|
|
$this->syncMovies();
|
|
|
|
$this->logProgress("Processed {$this->processedCount} Stash items");
|
|
}
|
|
|
|
/**
|
|
* Check if a scene should be ignored based on file paths and ignore patterns in config
|
|
*/
|
|
private function shouldIgnoreScene(array $sceneData): bool
|
|
{
|
|
$config = $this->source['config'] ?? null;
|
|
if (empty($config)) {
|
|
return false;
|
|
}
|
|
|
|
$configData = json_decode($config, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
$this->logProgress('Invalid JSON in source config, skipping ignore check');
|
|
return false;
|
|
}
|
|
|
|
$ignorePaths = $configData['ignore_paths'] ?? [];
|
|
if (empty($ignorePaths) || !is_array($ignorePaths)) {
|
|
return false;
|
|
}
|
|
|
|
$files = $sceneData['files'] ?? [];
|
|
foreach ($files as $file) {
|
|
$filePath = $file['path'] ?? '';
|
|
if (empty($filePath)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($ignorePaths as $ignorePattern) {
|
|
if (stripos($filePath, $ignorePattern) !== false) {
|
|
$this->logProgress("Scene '{$sceneData['title']}' ignored due to file path containing: '{$ignorePattern}'");
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function syncScenes(): void
|
|
{
|
|
try {
|
|
$this->logProgress('Fetching Stash scenes...');
|
|
|
|
// First, get the total count to determine how many pages we need
|
|
$totalCount = $this->getStashScenesCount();
|
|
|
|
if ($totalCount === 0) {
|
|
$this->logProgress('No scenes found in Stash');
|
|
return;
|
|
}
|
|
|
|
$this->logProgress("Found {$totalCount} scenes in Stash");
|
|
|
|
// Use pagination to handle large libraries
|
|
$perPage = 50; // Smaller batch size for reliability
|
|
$totalPages = ceil($totalCount / $perPage);
|
|
|
|
for ($page = 0; $page < $totalPages; $page++) {
|
|
try {
|
|
$offset = $page * $perPage;
|
|
$scenes = $this->getStashScenes($offset, $perPage);
|
|
|
|
if (empty($scenes)) {
|
|
$this->logProgress("No scenes returned for page {$page}");
|
|
continue;
|
|
}
|
|
|
|
$this->logProgress("Processing page {$page} with " . count($scenes) . " scenes...");
|
|
|
|
foreach ($scenes as $sceneData) {
|
|
try {
|
|
$this->logProgress("Processing scene: {$sceneData['title']} (ID: {$sceneData['id']})");
|
|
|
|
// Check if scene should be ignored based on file paths
|
|
if ($this->shouldIgnoreScene($sceneData)) {
|
|
$this->processedCount++; // Still count as processed
|
|
// Update progress for ignored items
|
|
$this->updateSyncLog($this->currentSyncLogId, 'running', [
|
|
'processed_items' => $this->processedCount,
|
|
'new_items' => $this->newCount,
|
|
'updated_items' => $this->updatedCount,
|
|
'message' => "Processed {$this->processedCount} of ~{$totalCount} scenes (ignored)"
|
|
]);
|
|
continue;
|
|
}
|
|
|
|
$this->syncScene($sceneData);
|
|
$this->processedCount++;
|
|
|
|
// Update progress in real-time
|
|
$this->updateSyncLog($this->currentSyncLogId, 'running', [
|
|
'processed_items' => $this->processedCount,
|
|
'new_items' => $this->newCount,
|
|
'updated_items' => $this->updatedCount,
|
|
'message' => "Processed {$this->processedCount} of ~{$totalCount} scenes"
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->logProgress("Error processing scene {$sceneData['id']} ({$sceneData['title']}): " . $e->getMessage());
|
|
$this->processedCount++; // Still count as processed even if failed
|
|
|
|
// Update progress even for failed items
|
|
$this->updateSyncLog($this->currentSyncLogId, 'running', [
|
|
'processed_items' => $this->processedCount,
|
|
'message' => "Error on scene {$sceneData['id']}: " . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->logProgress("Error fetching page {$page}: " . $e->getMessage());
|
|
// Continue with next page even if this page fails
|
|
$this->updateSyncLog($this->currentSyncLogId, 'running', [
|
|
'message' => "Failed to fetch page {$page}, continuing with next page"
|
|
]);
|
|
}
|
|
|
|
// Add a small delay between pages to avoid overwhelming the server
|
|
if ($page < $totalPages - 1) {
|
|
sleep(1);
|
|
}
|
|
}
|
|
|
|
$this->logProgress("Completed syncing Stash scenes");
|
|
} catch (Exception $e) {
|
|
$this->logProgress('Error syncing scenes: ' . $e->getMessage());
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
private function getStashScenesCount(): int
|
|
{
|
|
try {
|
|
$query = '
|
|
query FindScenes($filter: FindFilterType) {
|
|
findScenes(filter: $filter) {
|
|
count
|
|
}
|
|
}
|
|
';
|
|
|
|
$variables = [
|
|
'filter' => [
|
|
'per_page' => 1,
|
|
'page' => 1,
|
|
'sort' => 'created_at',
|
|
'direction' => 'DESC'
|
|
]
|
|
];
|
|
|
|
$response = $this->httpClient->post("{$this->baseUrl}/graphql", [
|
|
'json' => [
|
|
'query' => $query,
|
|
'variables' => $variables
|
|
],
|
|
'timeout' => 30
|
|
]);
|
|
|
|
$data = json_decode($response->getBody(), true);
|
|
|
|
if (!isset($data['data']['findScenes']['count'])) {
|
|
$this->logProgress('No count data in Stash response');
|
|
return 0;
|
|
}
|
|
|
|
return (int) $data['data']['findScenes']['count'];
|
|
} catch (Exception $e) {
|
|
$this->logProgress('Failed to get Stash scenes count: ' . $e->getMessage());
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private function getStashScenes(int $offset = 0, int $limit = 50): array
|
|
{
|
|
try {
|
|
$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
|
|
path
|
|
}
|
|
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' => $limit,
|
|
'page' => $offset / $limit + 1,
|
|
'sort' => 'created_at',
|
|
'direction' => 'DESC'
|
|
]
|
|
];
|
|
|
|
$this->logProgress("Fetching Stash scenes: offset={$offset}, limit={$limit}");
|
|
|
|
$response = $this->httpClient->post("{$this->baseUrl}/graphql", [
|
|
'json' => [
|
|
'query' => $query,
|
|
'variables' => $variables
|
|
],
|
|
'timeout' => 60, // Increased timeout
|
|
'connect_timeout' => 30
|
|
]);
|
|
|
|
$data = json_decode($response->getBody(), true);
|
|
|
|
if (!isset($data['data']['findScenes']['scenes'])) {
|
|
$this->logProgress('No scenes data in Stash response');
|
|
return [];
|
|
}
|
|
|
|
$scenes = $data['data']['findScenes']['scenes'];
|
|
$this->logProgress("Received " . count($scenes) . " scenes from Stash");
|
|
|
|
return $scenes;
|
|
} catch (Exception $e) {
|
|
$this->logProgress('Failed to fetch Stash scenes: ' . $e->getMessage());
|
|
$this->logProgress('Request details: ' . $e->getMessage());
|
|
throw new Exception('Failed to fetch Stash scenes: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function syncMovies(): void
|
|
{
|
|
try {
|
|
$movies = $this->getStashMovies();
|
|
|
|
foreach ($movies as $movieData) {
|
|
$this->syncMovie($movieData);
|
|
$this->processedCount++;
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->logProgress('Error syncing movies: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function getStashMovies(): array
|
|
{
|
|
try {
|
|
$query = '
|
|
query FindMovies($filter: FindFilterType) {
|
|
findMovies(filter: $filter) {
|
|
movies {
|
|
id
|
|
name
|
|
aliases
|
|
duration
|
|
date
|
|
rating100
|
|
director
|
|
synopsis
|
|
url
|
|
created_at
|
|
updated_at
|
|
front_image_path
|
|
back_image_path
|
|
}
|
|
count
|
|
}
|
|
}
|
|
';
|
|
|
|
$variables = [
|
|
'filter' => [
|
|
'per_page' => 100,
|
|
'sort' => 'created_at',
|
|
'direction' => 'DESC'
|
|
]
|
|
];
|
|
|
|
$response = $this->httpClient->post("{$this->baseUrl}/graphql", [
|
|
'json' => [
|
|
'query' => $query,
|
|
'variables' => $variables
|
|
]
|
|
]);
|
|
|
|
$data = json_decode($response->getBody(), true);
|
|
|
|
if (!isset($data['data']['findMovies']['movies'])) {
|
|
return []; // No movies found
|
|
}
|
|
|
|
return $data['data']['findMovies']['movies'];
|
|
} catch (Exception $e) {
|
|
// Return empty array if movies can't be fetched
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private function syncScene(array $sceneData): void
|
|
{
|
|
$adultVideoModel = new AdultVideo($this->pdo);
|
|
|
|
// Check if scene already exists by stash_id in metadata
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT id, metadata FROM adult_videos
|
|
WHERE source_id = :source_id
|
|
");
|
|
$stmt->execute(['source_id' => $this->source['id']]);
|
|
$existingScenes = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
|
|
|
$existingScene = null;
|
|
foreach ($existingScenes as $scene) {
|
|
$metadata = json_decode($scene['metadata'], true);
|
|
if (isset($metadata['stash_id']) && $metadata['stash_id'] === $sceneData['id']) {
|
|
$existingScene = $scene;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Download images locally
|
|
$coverFilename = null;
|
|
$screenshotFilename = null;
|
|
|
|
// Extract image URLs from Stash API response
|
|
$coverUrl = null;
|
|
$screenshotUrl = null;
|
|
|
|
// Stash provides paths.screenshot for screenshot
|
|
if (!empty($sceneData['paths']['screenshot'])) {
|
|
$screenshotPath = $sceneData['paths']['screenshot'];
|
|
|
|
// Handle different path formats from Stash
|
|
if (strpos($screenshotPath, 'http') === 0) {
|
|
// Already a full URL
|
|
$screenshotUrl = $screenshotPath;
|
|
} elseif (strpos($screenshotPath, '/') === 0) {
|
|
// Absolute path from Stash root
|
|
$screenshotUrl = "{$this->baseUrl}" . $screenshotPath;
|
|
} else {
|
|
// Relative path - assume it's in a standard location
|
|
$screenshotUrl = "{$this->baseUrl}/scene/" . $sceneData['id'] . "/" . $screenshotPath;
|
|
}
|
|
|
|
$this->logProgress("Screenshot URL: " . $screenshotUrl);
|
|
}
|
|
|
|
// For cover, we'll use the screenshot as cover if available
|
|
if ($screenshotUrl) {
|
|
$coverUrl = $screenshotUrl;
|
|
}
|
|
|
|
// Check if this is an existing scene and if images already exist
|
|
$shouldDownloadImages = true;
|
|
if ($existingScene) {
|
|
$existingMetadata = json_decode($existingScene['metadata'], true);
|
|
$hasExistingCover = !empty($existingMetadata['local_cover_path']);
|
|
$hasExistingScreenshot = !empty($existingMetadata['local_screenshot_path']);
|
|
|
|
if ($hasExistingCover && $hasExistingScreenshot) {
|
|
$shouldDownloadImages = false;
|
|
$this->logProgress("Scene {$sceneData['id']} already has images, skipping download");
|
|
} else {
|
|
$this->logProgress("Scene {$sceneData['id']} missing images - downloading");
|
|
}
|
|
}
|
|
|
|
if ($shouldDownloadImages) {
|
|
if (!empty($coverUrl)) {
|
|
// Validate URL before attempting download
|
|
if (filter_var($coverUrl, FILTER_VALIDATE_URL)) {
|
|
try {
|
|
$coverFilename = $this->imageDownloader->generateFilename($coverUrl, 'cover');
|
|
$localCoverPath = $this->imageDownloader->downloadImage($coverUrl, $coverFilename, 'adult_videos');
|
|
if ($localCoverPath) {
|
|
$sceneData['local_cover_path'] = $this->imageDownloader->getPublicUrl($localCoverPath);
|
|
$this->logProgress("Downloaded cover: " . $localCoverPath);
|
|
} else {
|
|
$this->logProgress("Failed to download cover from: " . $coverUrl);
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->logProgress("Exception downloading cover from {$coverUrl}: " . $e->getMessage());
|
|
}
|
|
} else {
|
|
$this->logProgress("Invalid cover URL: " . $coverUrl);
|
|
}
|
|
}
|
|
|
|
if (!empty($screenshotUrl)) {
|
|
// Validate URL before attempting download
|
|
if (filter_var($screenshotUrl, FILTER_VALIDATE_URL)) {
|
|
try {
|
|
$screenshotFilename = $this->imageDownloader->generateFilename($screenshotUrl, 'screenshot');
|
|
$localScreenshotPath = $this->imageDownloader->downloadImage($screenshotUrl, $screenshotFilename, 'adult_videos');
|
|
if ($localScreenshotPath) {
|
|
$sceneData['local_screenshot_path'] = $this->imageDownloader->getPublicUrl($localScreenshotPath);
|
|
$this->logProgress("Downloaded screenshot: " . $localScreenshotPath);
|
|
} else {
|
|
$this->logProgress("Failed to download screenshot from: " . $screenshotUrl);
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->logProgress("Exception downloading screenshot from {$screenshotUrl}: " . $e->getMessage());
|
|
}
|
|
} else {
|
|
$this->logProgress("Invalid screenshot URL: " . $screenshotUrl);
|
|
}
|
|
}
|
|
} else {
|
|
// Use existing image paths
|
|
$sceneData['local_cover_path'] = $existingMetadata['local_cover_path'] ?? null;
|
|
$sceneData['local_screenshot_path'] = $existingMetadata['local_screenshot_path'] ?? null;
|
|
}
|
|
// Handle performers/actors with full metadata
|
|
$performers = $sceneData['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 = [
|
|
'title' => $sceneData['title'] ?: 'Untitled Scene',
|
|
'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,
|
|
'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
|
|
'source_id' => $this->source['id'],
|
|
'external_id' => $sceneData['id'],
|
|
'metadata' => json_encode([
|
|
'stash_id' => $sceneData['id'],
|
|
'stash_url' => $sceneData['url'] ?? null,
|
|
'organized' => $sceneData['organized'] ?? false,
|
|
'o_counter' => $sceneData['o_counter'] ?? 0,
|
|
'performers' => $performers,
|
|
'actors' => $actors,
|
|
'file_info' => $sceneData['files'][0] ?? null,
|
|
'paths' => $sceneData['paths'] ?? null,
|
|
'cover_url' => $coverUrl,
|
|
'local_cover_path' => $sceneData['local_cover_path'] ?? null,
|
|
'screenshot_url' => $screenshotUrl,
|
|
'local_screenshot_path' => $sceneData['local_screenshot_path'] ?? null
|
|
])
|
|
];
|
|
|
|
if ($existingScene) {
|
|
// For existing scenes, check if we need to update images
|
|
$existingMetadata = json_decode($existingScene['metadata'], true);
|
|
|
|
// Only download images if they don't already exist locally
|
|
if (empty($existingMetadata['local_cover_path']) && !empty($coverUrl)) {
|
|
// Validate URL before attempting download
|
|
if (filter_var($coverUrl, FILTER_VALIDATE_URL)) {
|
|
try {
|
|
$coverFilename = $this->imageDownloader->generateFilename($coverUrl, 'cover');
|
|
$localCoverPath = $this->imageDownloader->downloadImage($coverUrl, $coverFilename, 'adult_videos');
|
|
if ($localCoverPath) {
|
|
$sceneData['local_cover_path'] = $this->imageDownloader->getPublicUrl($localCoverPath);
|
|
$this->logProgress("Downloaded cover: " . $localCoverPath);
|
|
} else {
|
|
$this->logProgress("Failed to download cover from: " . $coverUrl);
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->logProgress("Exception downloading cover from {$coverUrl}: " . $e->getMessage());
|
|
}
|
|
} else {
|
|
$this->logProgress("Invalid cover URL: " . $coverUrl);
|
|
}
|
|
} else {
|
|
// Keep existing local cover path
|
|
$sceneData['local_cover_path'] = $existingMetadata['local_cover_path'] ?? null;
|
|
if (!empty($sceneData['local_cover_path'])) {
|
|
$this->logProgress("Using existing cover: " . $sceneData['local_cover_path']);
|
|
}
|
|
}
|
|
|
|
if (empty($existingMetadata['local_screenshot_path']) && !empty($screenshotUrl)) {
|
|
// Validate URL before attempting download
|
|
if (filter_var($screenshotUrl, FILTER_VALIDATE_URL)) {
|
|
try {
|
|
$screenshotFilename = $this->imageDownloader->generateFilename($screenshotUrl, 'screenshot');
|
|
$localScreenshotPath = $this->imageDownloader->downloadImage($screenshotUrl, $screenshotFilename, 'adult_videos');
|
|
if ($localScreenshotPath) {
|
|
$sceneData['local_screenshot_path'] = $this->imageDownloader->getPublicUrl($localScreenshotPath);
|
|
$this->logProgress("Downloaded screenshot: " . $localScreenshotPath);
|
|
} else {
|
|
$this->logProgress("Failed to download screenshot from: " . $screenshotUrl);
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->logProgress("Exception downloading screenshot from {$screenshotUrl}: " . $e->getMessage());
|
|
}
|
|
} else {
|
|
$this->logProgress("Invalid screenshot URL: " . $screenshotUrl);
|
|
}
|
|
} else {
|
|
// Keep existing local screenshot path
|
|
$sceneData['local_screenshot_path'] = $existingMetadata['local_screenshot_path'] ?? null;
|
|
if (!empty($sceneData['local_screenshot_path'])) {
|
|
$this->logProgress("Using existing screenshot: " . $sceneData['local_screenshot_path']);
|
|
}
|
|
}
|
|
|
|
$adultVideoModel->update($existingScene['id'], $sceneData);
|
|
$adultVideoId = $existingScene['id'];
|
|
$this->updatedCount++;
|
|
|
|
// Create actor relationships for existing scene
|
|
$this->createActorRelationships($adultVideoId, $actors);
|
|
} else {
|
|
$adultVideoModel->create($sceneData);
|
|
$adultVideoId = $this->pdo->lastInsertId();
|
|
$this->newCount++;
|
|
|
|
// Create actor relationships for new scene
|
|
$this->createActorRelationships($adultVideoId, $actors);
|
|
}
|
|
}
|
|
|
|
private function syncMovie(array $movieData): void
|
|
{
|
|
$adultVideoModel = new AdultVideo($this->pdo);
|
|
|
|
// Check if movie already exists by stash_movie_id in metadata
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT id, metadata FROM adult_videos
|
|
WHERE source_id = :source_id
|
|
");
|
|
$stmt->execute(['source_id' => $this->source['id']]);
|
|
$existingMovies = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
|
|
|
$existingMovie = null;
|
|
foreach ($existingMovies as $movie) {
|
|
$metadata = json_decode($movie['metadata'], true);
|
|
if (isset($metadata['stash_movie_id']) && $metadata['stash_movie_id'] === $movieData['id']) {
|
|
$existingMovie = $movie;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$movieData = [
|
|
'title' => $movieData['name'] ?: 'Untitled Movie',
|
|
'overview' => $movieData['synopsis'] ?? null,
|
|
'director' => $movieData['director'] ?? null,
|
|
'release_date' => $movieData['date'] ? date('Y-m-d', strtotime($movieData['date'])) : null,
|
|
'runtime_minutes' => $movieData['duration'] ?? null,
|
|
'rating' => $movieData['rating100'] ? $movieData['rating100'] / 100 : null,
|
|
'source_id' => $this->source['id'],
|
|
'external_id' => $movieData['id'],
|
|
'metadata' => json_encode([
|
|
'stash_movie_id' => $movieData['id'],
|
|
'aliases' => $movieData['aliases'] ?? null,
|
|
'url' => $movieData['url'] ?? null
|
|
])
|
|
];
|
|
|
|
if ($existingMovie) {
|
|
$adultVideoModel->update($existingMovie['id'], $movieData);
|
|
$this->updatedCount++;
|
|
} else {
|
|
$adultVideoModel->create($movieData);
|
|
$this->newCount++;
|
|
}
|
|
}
|
|
|
|
private function syncActors(array $performers): array
|
|
{
|
|
$actors = [];
|
|
|
|
foreach ($performers as $performer) {
|
|
if (empty($performer['name'])) continue;
|
|
|
|
$actor = $this->getOrCreateActor($performer);
|
|
if ($actor) {
|
|
$actors[] = $actor;
|
|
}
|
|
}
|
|
|
|
return $actors;
|
|
}
|
|
|
|
private function getOrCreateActor(array $performer): ?array
|
|
{
|
|
$name = $performer['name'] ?? '';
|
|
if (empty($name)) return null;
|
|
|
|
// Check if actor already exists by name or alias
|
|
// First check by exact name
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT id, name, thumbnail_path, metadata FROM actors
|
|
WHERE name = :name
|
|
");
|
|
$stmt->execute(['name' => $name]);
|
|
$existingActor = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
// If not found by name, check aliases in PHP
|
|
if (!$existingActor) {
|
|
$stmt = $this->pdo->prepare("SELECT id, name, thumbnail_path, metadata FROM actors");
|
|
$stmt->execute();
|
|
$allActors = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
foreach ($allActors as $actor) {
|
|
$metadata = json_decode($actor['metadata'] ?? '{}', true);
|
|
$aliases = $metadata['aliases'] ?? [];
|
|
if (is_array($aliases) && in_array($name, $aliases)) {
|
|
$existingActor = $actor;
|
|
$this->logProgress("Found existing actor '{$actor['name']}' by alias '{$name}'");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prepare rich metadata from Stash performer data
|
|
$actorMetadata = [
|
|
'stash_id' => $performer['id'] ?? null,
|
|
'stash_url' => $performer['url'] ?? null,
|
|
'disambiguation' => $performer['disambiguation'] ?? '',
|
|
'gender' => $performer['gender'] ?? null,
|
|
'birth_date' => $performer['birthdate'] ?? null,
|
|
'death_date' => $performer['death_date'] ?? null,
|
|
'ethnicity' => $performer['ethnicity'] ?? null,
|
|
'country' => $performer['country'] ?? null,
|
|
'nationality' => $performer['country'] ?? null, // Map country to nationality
|
|
'eye_color' => $performer['eye_color'] ?? null,
|
|
'hair_color' => $performer['hair_color'] ?? null,
|
|
'height' => $performer['height_cm'] ? $performer['height_cm'] . 'cm' : null,
|
|
'measurements' => $performer['measurements'] ?? null,
|
|
'cup_size' => $this->extractCupSize($performer['measurements'] ?? ''),
|
|
'weight' => $performer['weight'] ? $performer['weight'] . 'kg' : null,
|
|
'piercings' => $performer['piercings'] ?? null,
|
|
'tattoos' => $performer['tattoos'] ?? null,
|
|
'fake_tits' => $performer['fake_tits'] ?? null,
|
|
'penis_length' => $performer['penis_length'] ?? null,
|
|
'circumcised' => $performer['circumcised'] ?? null,
|
|
'career_length' => $performer['career_length'] ?? null,
|
|
'aliases' => $performer['alias_list'] ?? [],
|
|
'favorite' => $performer['favorite'] ?? false,
|
|
'ignore_auto_tag' => $performer['ignore_auto_tag'] ?? false,
|
|
'scene_count' => $performer['scene_count'] ?? 0,
|
|
'details' => $performer['details'] ?? null,
|
|
'stash_created_at' => $performer['created_at'] ?? null,
|
|
'stash_updated_at' => $performer['updated_at'] ?? null,
|
|
'social_media' => [
|
|
'website' => $performer['url'] ?? null
|
|
],
|
|
'adult_specific' => [
|
|
'debut_year' => $this->extractDebutYear($performer['career_length'] ?? ''),
|
|
'retirement_year' => $this->extractRetirementYear($performer['career_length'] ?? ''),
|
|
'active' => $this->isActivePerformer($performer['career_length'] ?? ''),
|
|
'genres' => [],
|
|
'specialties' => []
|
|
]
|
|
];
|
|
|
|
// Try to download performer image if available
|
|
$thumbnailPath = null;
|
|
$imagePath = $performer['image_path'] ?? null;
|
|
if ($imagePath) {
|
|
// Validate image path before constructing URL
|
|
if (!empty(trim($imagePath))) {
|
|
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("Performer image URL for {$name}: " . $imageUrl);
|
|
$thumbnailFilename = $this->imageDownloader->generateFilename($imageUrl, 'actor');
|
|
$localImagePath = $this->imageDownloader->downloadImage($imageUrl, $thumbnailFilename, 'actors');
|
|
if ($localImagePath) {
|
|
$thumbnailPath = $this->imageDownloader->getPublicUrl($localImagePath);
|
|
$this->logProgress("Downloaded performer image: " . $localImagePath);
|
|
} else {
|
|
$this->logProgress("Failed to download performer image from: " . $imageUrl);
|
|
}
|
|
} else {
|
|
$this->logProgress("Invalid performer image URL constructed: " . $imageUrl);
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->logProgress("Exception downloading performer image for {$name} from {$imagePath}: " . $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($existingActor) {
|
|
// Update existing actor with new metadata if it's more complete
|
|
$existingMetadata = json_decode($existingActor['metadata'] ?? '{}', true);
|
|
|
|
// Check if we should update - prefer more complete data
|
|
$shouldUpdate = false;
|
|
if (empty($existingMetadata['stash_id']) && !empty($actorMetadata['stash_id'])) {
|
|
$shouldUpdate = true;
|
|
} elseif (!empty($thumbnailPath) && empty($existingActor['thumbnail_path'])) {
|
|
$shouldUpdate = true;
|
|
}
|
|
|
|
if ($shouldUpdate) {
|
|
$stmt = $this->pdo->prepare("
|
|
UPDATE actors
|
|
SET thumbnail_path = COALESCE(:thumbnail_path, thumbnail_path),
|
|
metadata = :metadata,
|
|
updated_at = NOW()
|
|
WHERE id = :id
|
|
");
|
|
$stmt->execute([
|
|
'id' => $existingActor['id'],
|
|
'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path'],
|
|
'metadata' => json_encode(array_merge($existingMetadata, $actorMetadata))
|
|
]);
|
|
$this->logProgress("Updated existing actor {$name} with Stash metadata");
|
|
}
|
|
|
|
return [
|
|
'id' => $existingActor['id'],
|
|
'name' => $existingActor['name'],
|
|
'thumbnail_path' => $thumbnailPath ?: $existingActor['thumbnail_path']
|
|
];
|
|
}
|
|
|
|
// Create new actor with full metadata
|
|
try {
|
|
$stmt = $this->pdo->prepare("
|
|
INSERT INTO actors (name, thumbnail_path, metadata, created_at, updated_at)
|
|
VALUES (:name, :thumbnail_path, :metadata, NOW(), NOW())
|
|
");
|
|
$stmt->execute([
|
|
'name' => $name,
|
|
'thumbnail_path' => $thumbnailPath,
|
|
'metadata' => json_encode($actorMetadata)
|
|
]);
|
|
$actorId = $this->pdo->lastInsertId();
|
|
|
|
$this->logProgress("Created new actor {$name} with full Stash metadata");
|
|
|
|
return [
|
|
'id' => $actorId,
|
|
'name' => $name,
|
|
'thumbnail_path' => $thumbnailPath
|
|
];
|
|
} catch (Exception $e) {
|
|
$this->logProgress("Failed to create actor {$name}: " . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function extractCupSize(string $measurements): ?string
|
|
{
|
|
if (empty($measurements)) return null;
|
|
|
|
// Try to extract cup size from measurements like "34C-24-35"
|
|
$parts = explode('-', $measurements);
|
|
if (count($parts) >= 1) {
|
|
$firstPart = trim($parts[0]);
|
|
// Look for cup size pattern (number followed by letter)
|
|
if (preg_match('/(\d+)([A-Z])/', $firstPart, $matches)) {
|
|
return $matches[2];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function extractDebutYear(string $careerLength): ?string
|
|
{
|
|
if (empty($careerLength)) return null;
|
|
|
|
// Extract debut year from patterns like "2015 -" or "2015 - 2020"
|
|
if (preg_match('/(\d{4})\s*-\s*(\d{4})?/', $careerLength, $matches)) {
|
|
return $matches[1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function extractRetirementYear(string $careerLength): ?string
|
|
{
|
|
if (empty($careerLength)) return null;
|
|
|
|
// Extract retirement year from patterns like "2015 - 2020"
|
|
if (preg_match('/\d{4}\s*-\s*(\d{4})/', $careerLength, $matches)) {
|
|
return $matches[1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function isActivePerformer(string $careerLength): bool
|
|
{
|
|
if (empty($careerLength)) return false;
|
|
|
|
// Check if career is still active (ends with " -")
|
|
return str_ends_with(trim($careerLength), '-');
|
|
}
|
|
|
|
protected function getProcessedCount(): int
|
|
{
|
|
return $this->processedCount;
|
|
}
|
|
|
|
protected function getNewCount(): int
|
|
{
|
|
return $this->newCount;
|
|
}
|
|
|
|
protected function getUpdatedCount(): int
|
|
{
|
|
return $this->updatedCount;
|
|
}
|
|
|
|
private function createActorRelationships(int $adultVideoId, array $actors): void
|
|
{
|
|
foreach ($actors as $actor) {
|
|
if (!isset($actor['id'])) continue;
|
|
|
|
try {
|
|
// Insert relationship into pivot table (ignore duplicates)
|
|
$stmt = $this->pdo->prepare("
|
|
INSERT IGNORE INTO actor_adult_video (adult_video_id, actor_id, created_at, updated_at)
|
|
VALUES (:adult_video_id, :actor_id, NOW(), NOW())
|
|
");
|
|
$stmt->execute([
|
|
'adult_video_id' => $adultVideoId,
|
|
'actor_id' => $actor['id']
|
|
]);
|
|
|
|
$this->logProgress("Created relationship: Adult Video {$adultVideoId} -> Actor {$actor['name']} ({$actor['id']})");
|
|
} catch (Exception $e) {
|
|
$this->logProgress("Failed to create relationship for Adult Video {$adultVideoId} and Actor {$actor['name']}: " . $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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...');
|
|
|
|
echo "Starting existing performers sync with Stash..\n";
|
|
// Get all existing actors from database
|
|
$stmt = $this->pdo->prepare("SELECT id, name, metadata FROM actors WHERE id IN (SELECT actor_id FROM actor_adult_video) 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
|
|
{
|
|
$this->logProgress("Starting cleanup - detecting deleted media in Stash...");
|
|
|
|
// Clean up scenes
|
|
$this->cleanupScenes();
|
|
|
|
// Clean up movies
|
|
$this->cleanupMovies();
|
|
|
|
$this->logProgress("Cleanup completed. Deleted {$this->deletedCount} items.");
|
|
}
|
|
|
|
private function cleanupScenes(): void
|
|
{
|
|
$this->logProgress("Checking for deleted scenes...");
|
|
|
|
try {
|
|
// Get all scenes from Stash
|
|
$stashScenes = $this->getStashScenes(0, 1000); // Get up to 1000 scenes for cleanup
|
|
$stashSceneIds = array_column($stashScenes, 'id');
|
|
$this->logProgress("Found " . count($stashSceneIds) . " scenes in Stash");
|
|
|
|
// Get all scenes from local database for this source
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT id, metadata FROM adult_videos WHERE source_id = :source_id
|
|
");
|
|
$stmt->execute(['source_id' => $this->source['id']]);
|
|
$localScenes = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
|
|
|
$this->logProgress("Found " . count($localScenes) . " scenes in local database");
|
|
|
|
$deletedCount = 0;
|
|
foreach ($localScenes as $localScene) {
|
|
$metadata = json_decode($localScene['metadata'], true);
|
|
$stashId = $metadata['stash_id'] ?? null;
|
|
|
|
if ($stashId && !in_array($stashId, $stashSceneIds)) {
|
|
// Scene exists in local DB but not in Stash - delete it
|
|
$this->deleteAdultVideo($localScene['id']);
|
|
$deletedCount++;
|
|
$this->deletedCount++;
|
|
}
|
|
}
|
|
|
|
$this->logProgress("Deleted {$deletedCount} scenes that no longer exist in Stash");
|
|
|
|
} catch (Exception $e) {
|
|
$this->logProgress("Error during scene cleanup: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function cleanupMovies(): void
|
|
{
|
|
$this->logProgress("Checking for deleted movies...");
|
|
|
|
try {
|
|
// Get all movies from Stash
|
|
$stashMovies = $this->getStashMovies();
|
|
$stashMovieIds = array_column($stashMovies, 'id');
|
|
$this->logProgress("Found " . count($stashMovieIds) . " movies in Stash");
|
|
|
|
// Get all movies from local database for this source
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT id, metadata FROM adult_videos WHERE source_id = :source_id
|
|
");
|
|
$stmt->execute(['source_id' => $this->source['id']]);
|
|
$localVideos = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
|
|
|
$this->logProgress("Found " . count($localVideos) . " videos in local database");
|
|
|
|
$deletedCount = 0;
|
|
foreach ($localVideos as $localVideo) {
|
|
$metadata = json_decode($localVideo['metadata'], true);
|
|
$stashMovieId = $metadata['stash_movie_id'] ?? null;
|
|
|
|
if ($stashMovieId && !in_array($stashMovieId, $stashMovieIds)) {
|
|
// Movie exists in local DB but not in Stash - delete it
|
|
$this->deleteAdultVideo($localVideo['id']);
|
|
$deletedCount++;
|
|
$this->deletedCount++;
|
|
}
|
|
}
|
|
|
|
$this->logProgress("Deleted {$deletedCount} movies that no longer exist in Stash");
|
|
|
|
} catch (Exception $e) {
|
|
$this->logProgress("Error during movie cleanup: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function deleteAdultVideo(int $videoId): void
|
|
{
|
|
try {
|
|
// Delete actor relationships first
|
|
$stmt = $this->pdo->prepare("DELETE FROM actor_adult_video WHERE adult_video_id = :adult_video_id");
|
|
$stmt->execute(['adult_video_id' => $videoId]);
|
|
|
|
// Delete the video
|
|
$stmt = $this->pdo->prepare("DELETE FROM adult_videos WHERE id = :id");
|
|
$stmt->execute(['id' => $videoId]);
|
|
|
|
$this->logProgress("Deleted adult video with ID: {$videoId}");
|
|
|
|
} catch (Exception $e) {
|
|
$this->logProgress("Error deleting adult video {$videoId}: " . $e->getMessage());
|
|
}
|
|
}
|
|
}
|