['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'm4v', 'mpg', 'mpeg'], 'tv_shows' => ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'm4v', 'mpg', 'mpeg'], 'music' => ['mp3', 'flac', 'wav', 'aac', 'ogg', 'm4a'], 'games' => ['iso', 'rom', 'nsp', 'xci', 'rvz', 'ciso', 'gcm', 'wbfs'], 'books' => ['epub', 'mobi', 'pdf', 'azw', 'azw3', 'djvu'], 'pictures' => ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'raw'] ]; /** * @inheritDoc */ public function sync($source, string $type = 'full', callable $progressCallback = null): array { $this->source = $source; $this->sourceId = $source['id'] ?? null; if (!$this->sourceId) { throw new Exception('Source ID is required for sync'); } try { $this->logProgress("Starting {$type} sync for local source: " . ($source['name'] ?? 'Unknown')); $path = $source['path'] ?? null; if (empty($path) || !is_dir($path)) { throw new Exception("Invalid or inaccessible source path: {$path}"); } // Determine the media type from the source configuration $mediaType = $this->determineMediaType($source); // Get all files from the source directory recursively $files = $this->scanDirectory($path, $mediaType); $this->logProgress(sprintf('Found %d files to process', count($files))); // Get existing media items from the database $existingItems = $this->getExistingMediaItems($mediaType); $this->logProgress(sprintf('Found %d existing items in database', count($existingItems))); $result = [ 'total_items' => count($files), 'processed_items' => 0, 'new_items' => 0, 'updated_items' => 0, 'deleted_items' => 0, 'errors' => [] ]; // Process each file foreach ($files as $filePath => $fileInfo) { try { $relativePath = $this->getRelativePath($path, $filePath); $fileKey = $this->generateFileKey($relativePath, $fileInfo); if (isset($existingItems[$fileKey])) { // Update existing item if needed $item = $existingItems[$fileKey]; $updated = $this->updateMediaItem($item, $filePath, $fileInfo, $mediaType); if ($updated) { $result['updated_items']++; $this->logProgress("Updated: {$relativePath}"); } // Remove from existing items to track deletions unset($existingItems[$fileKey]); } else { // Add new item $this->createMediaItem($filePath, $fileInfo, $mediaType, $relativePath); $result['new_items']++; $this->logProgress("Added: {$relativePath}"); } $result['processed_items']++; // Update progress if ($progressCallback) { $progress = (int)(($result['processed_items'] / $result['total_items']) * 100); $progressCallback($progress, "Processing: {$relativePath}"); } } catch (\Exception $e) { $errorMsg = "Error processing {$filePath}: " . $e->getMessage(); $this->logProgress($errorMsg, 'ERROR'); $result['errors'][] = $errorMsg; } } // Handle deleted files if ($type === 'full' && !empty($existingItems)) { foreach ($existingItems as $item) { try { $this->deleteMediaItem($item, $mediaType); $result['deleted_items']++; $this->logProgress("Deleted: {$item['file_path']}"); } catch (\Exception $e) { $errorMsg = "Error deleting {$item['file_path']}: " . $e->getMessage(); $this->logProgress($errorMsg, 'ERROR'); $result['errors'][] = $errorMsg; } } } $this->logProgress("Sync completed successfully"); return array_merge($result, [ 'success' => true, 'message' => 'Sync completed successfully', ]); } catch (\Exception $e) { $errorMsg = 'Sync failed: ' . $e->getMessage(); $this->logProgress($errorMsg, 'ERROR'); return [ 'success' => false, 'message' => $errorMsg, 'errors' => [$errorMsg] ]; } } /** * @inheritDoc */ public function getSupportedTypes(): array { return ['local', 'file']; } /** * Determine the media type from the source configuration */ protected function determineMediaType(array $source): string { // First check if media_type is explicitly set in the source config if (!empty($source['media_type'])) { return strtolower($source['media_type']); } // Otherwise, try to guess from the path or name $path = strtolower($source['path'] ?? ''); $name = strtolower($source['name'] ?? ''); foreach (array_keys($this->supportedExtensions) as $type) { if (strpos($path, $type) !== false || strpos($name, $type) !== false) { return $type; } } // Default to 'other' if we can't determine the type return 'other'; } /** * Scan a directory recursively for media files */ protected function scanDirectory(string $path, string $mediaType): array { $files = []; $extensions = $this->supportedExtensions[$mediaType] ?? []; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); /** @var SplFileInfo $file */ foreach ($iterator as $file) { if ($file->isFile()) { $ext = strtolower($file->getExtension()); // If we have specific extensions for this media type, filter by them if (!empty($extensions) && !in_array($ext, $extensions)) { continue; } $files[$file->getPathname()] = [ 'size' => $file->getSize(), 'modified' => $file->getMTime(), 'extension' => $ext, 'path' => $file->getPathname(), 'filename' => $file->getFilename() ]; } } return $files; } /** * Get existing media items from the database */ protected function getExistingMediaItems(string $mediaType): array { $mediaItem = new MediaItem($this->pdo); $items = $mediaItem->where('source_id', $this->sourceId) ->where('media_type', $mediaType) ->get(); $result = []; foreach ($items as $item) { $result[$item['file_key']] = $item; } return $result; } /** * Generate a unique key for a file */ protected function generateFileKey(string $relativePath, array $fileInfo): string { return md5($relativePath . $fileInfo['size'] . $fileInfo['modified']); } /** * Get the relative path from the base path */ protected function getRelativePath(string $basePath, string $filePath): string { return ltrim(str_replace('\\', '/', substr($filePath, strlen($basePath))), '/'); } /** * Create a new media item in the database */ protected function createMediaItem(string $filePath, array $fileInfo, string $mediaType, string $relativePath): void { $mediaItem = new MediaItem($this->pdo); $data = [ 'source_id' => $this->sourceId, 'media_type' => $mediaType, 'file_path' => $relativePath, 'file_name' => $fileInfo['filename'], 'file_size' => $fileInfo['size'], 'file_modified' => date('Y-m-d H:i:s', $fileInfo['modified']), 'file_extension' => $fileInfo['extension'], 'file_key' => $this->generateFileKey($relativePath, $fileInfo), 'metadata' => json_encode([ 'original_path' => $filePath, 'imported_at' => date('Y-m-d H:i:s') ]), 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]; $mediaItem->create($data); } /** * Update an existing media item if needed */ protected function updateMediaItem(array $item, string $filePath, array $fileInfo, string $mediaType): bool { $needsUpdate = false; $updates = []; // Check if file has been modified if ($item['file_size'] != $fileInfo['size'] || $item['file_modified'] != date('Y-m-d H:i:s', $fileInfo['modified'])) { $updates = [ 'file_size' => $fileInfo['size'], 'file_modified' => date('Y-m-d H:i:s', $fileInfo['modified']), 'updated_at' => date('Y-m-d H:i:s') ]; $needsUpdate = true; } // Update metadata if needed $metadata = json_decode($item['metadata'] ?? '{}', true); $metadata['last_checked'] = date('Y-m-d H:i:s'); $updates['metadata'] = json_encode($metadata); if ($needsUpdate) { $mediaItem = new MediaItem($this->pdo); $mediaItem->update($item['id'], $updates); return true; } return false; } /** * Delete a media item from the database */ protected function deleteMediaItem(array $item, string $mediaType): void { $mediaItem = new MediaItem($this->pdo); $mediaItem->delete($item['id']); } }