This commit is contained in:
Lars Behrends
2025-10-18 22:03:30 +02:00
parent f4c1cfc164
commit ca2d3a6960
45 changed files with 4827 additions and 326 deletions

View File

@@ -31,8 +31,9 @@ class StashSyncService extends BaseSyncService
'headers' => [
'User-Agent' => 'MediaCollector/1.0',
'Content-Type' => 'application/json',
'ApiKey' => $this->apiKey // Now safe to access
]
'ApiKey' => $this->apiKey // Stash API key for authentication
],
'verify' => false // Disable SSL verification for problematic servers
]);
$this->imageDownloader = new ImageDownloader('public/images', $this->apiKey);
@@ -60,21 +61,70 @@ class StashSyncService extends BaseSyncService
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
$page = 0;
$perPage = 50; // Smaller batch size for reliability
$totalPages = ceil($totalCount / $perPage);
do {
$scenes = $this->getStashScenes($page * $perPage, $perPage);
$this->logProgress("Processing page {$page} with " . count($scenes) . " scenes...");
for ($page = 0; $page < $totalPages; $page++) {
try {
$offset = $page * $perPage;
$scenes = $this->getStashScenes($offset, $perPage);
foreach ($scenes as $sceneData) {
$this->syncScene($sceneData);
$this->processedCount++;
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']})");
$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"
]);
}
$page++;
} while (count($scenes) === $perPage); // Continue if we got a full 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) {
@@ -83,6 +133,48 @@ class StashSyncService extends BaseSyncService
}
}
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 {
@@ -118,15 +210,12 @@ class StashSyncService extends BaseSyncService
width
height
}
paths {
screenshot
}
performers {
id
name
disambiguation
url
gender
gender
birthdate
ethnicity
country
@@ -166,24 +255,31 @@ class StashSyncService extends BaseSyncService
]
];
$this->logProgress("Fetching Stash scenes: offset={$offset}, limit={$limit}");
$response = $this->httpClient->post("{$this->baseUrl}/graphql", [
'json' => [
'query' => $query,
'variables' => $variables
],
'timeout' => 30
'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 response');
$this->logProgress('No scenes data in Stash response');
return [];
}
return $data['data']['findScenes']['scenes'];
$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());
}
}
@@ -309,28 +405,66 @@ class StashSyncService extends BaseSyncService
$coverUrl = $screenshotUrl;
}
if (!empty($coverUrl)) {
$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);
// 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("Failed to download cover from: " . $coverUrl);
$this->logProgress("Scene {$sceneData['id']} missing images - downloading");
}
}
if (!empty($screenshotUrl)) {
$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);
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
$performers = $sceneData['performers'] ?? [];
$actorNames = [];
@@ -366,11 +500,75 @@ class StashSyncService extends BaseSyncService
];
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);
}
}
@@ -457,26 +655,38 @@ class StashSyncService extends BaseSyncService
// Try to download performer image if available
$thumbnailPath = null;
if ($imagePath) {
// 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/" . $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/" . $imagePath;
}
$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);
// 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());
}
}
}
@@ -517,6 +727,29 @@ class StashSyncService extends BaseSyncService
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());
}
}
}
protected function getDeletedCount(): int
{
return 0; // Stash doesn't provide deletion info in this context