diff --git a/.gitignore b/.gitignore index 0575642..815777d 100644 --- a/.gitignore +++ b/.gitignore @@ -148,3 +148,4 @@ composer.lock /public/public/images/adult_videos /public/public/images/backdrops /public/public/images/posters +/storage/images diff --git a/app/Controllers/ActorController.php b/app/Controllers/ActorController.php index e4997a5..c59ff73 100644 --- a/app/Controllers/ActorController.php +++ b/app/Controllers/ActorController.php @@ -78,6 +78,28 @@ class ActorController extends Controller $stmt->execute(['actor_id' => $actorId]); $tvShows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + + + foreach ($scenes as &$scene) { + if (!empty($scene['metadata'])) { + $metadata = json_decode($scene['metadata'], true); + + // Use local cover path if available, otherwise fall back to original URL + if (!empty($metadata['local_cover_path'])) { + $scene['poster_url'] = $metadata['local_cover_path']; + } elseif (!empty($metadata['cover_url'])) { + $scene['poster_url'] = $metadata['cover_url']; + } + + // Add actors data if available + if (!empty($metadata['actors'])) { + $scene['actors'] = $metadata['actors']; + } + } + } + + return $this->view->render($response, 'actor/show.twig', [ 'title' => $actor['name'], 'actor' => $actor, @@ -86,7 +108,6 @@ class ActorController extends Controller 'tv_shows' => $tvShows ]); } - public function index(Request $request, Response $response, $args) { // Get all actors with their media counts from all types diff --git a/app/Controllers/AdminBaseController.php b/app/Controllers/AdminBaseController.php new file mode 100644 index 0000000..52d19b9 --- /dev/null +++ b/app/Controllers/AdminBaseController.php @@ -0,0 +1,70 @@ +pdo = $pdo; + $this->view = $view; + } + + /** + * Render a template + */ + protected function render(Response $response, string $template, array $data = []): Response + { + // Add common admin data + $data['auth'] = [ + 'check' => isset($_SESSION['user_id']), + 'user' => [ + 'username' => $_SESSION['username'] ?? 'Admin', + 'is_admin' => $_SESSION['is_admin'] ?? false + ] + ]; + + // Add current route for active menu highlighting + $route = $this->getCurrentRoute(); + if ($route) { + $data['current_route'] = $route; + } + + return $this->view->render($response, $template, $data); + } + + /** + * Get current route name + */ + protected function getCurrentRoute(): ?string + { + $route = $_SERVER['REQUEST_URI'] ?? '/'; + $basePath = '/admin/'; + + if (strpos($route, $basePath) === 0) { + $route = substr($route, strlen($basePath)); + $parts = explode('/', $route); + return $parts[0] ?: 'index'; + } + + return null; + } + + /** + * Return JSON response + */ + protected function json(Response $response, $data, int $status = 200): Response + { + $response->getBody()->write(json_encode($data)); + return $response + ->withHeader('Content-Type', 'application/json') + ->withStatus($status); + } +} diff --git a/app/Controllers/AdminController.php b/app/Controllers/AdminController.php index aa3f235..896dcf8 100644 --- a/app/Controllers/AdminController.php +++ b/app/Controllers/AdminController.php @@ -15,13 +15,13 @@ use App\Services\ExophaseSyncService; use PDO; use Slim\Views\Twig; -class AdminController extends Controller +class AdminController extends AdminBaseController { - private PDO $pdo; + protected PDO $pdo; public function __construct(PDO $pdo, Twig $view) { - parent::__construct($view); + parent::__construct($pdo, $view); $this->pdo = $pdo; } @@ -33,7 +33,7 @@ class AdminController extends Controller $syncLogModel = new SyncLog($this->pdo); $recentSyncs = SyncLog::getRecent($this->pdo, 10); - return $this->view->render($response, 'admin/index.twig', [ + return $this->render($response, 'admin/index.twig', [ 'title' => 'Admin Dashboard', 'sources' => $sources, 'recent_syncs' => $recentSyncs @@ -119,14 +119,23 @@ class AdminController extends Controller return min(100, round(($processed / $total) * 100, 2)); } + public function settings(Request $request, Response $response, $args) + { + return $this->render($response, 'admin/settings.twig', [ + 'title' => 'Admin Settings', + 'current_route' => 'settings' + ]); + } + public function sources(Request $request, Response $response, $args) { $sourceModel = new Source($this->pdo); $sources = $sourceModel->findAll(); - return $this->view->render($response, 'admin/sources.twig', [ + return $this->render($response, 'admin/sources.twig', [ 'title' => 'Source Management', - 'sources' => $sources + 'sources' => $sources, + 'current_route' => 'sources' ]); } diff --git a/app/Controllers/AdultController.php b/app/Controllers/AdultController.php index 8ffb526..85b3bb9 100644 --- a/app/Controllers/AdultController.php +++ b/app/Controllers/AdultController.php @@ -41,7 +41,7 @@ class AdultController extends Controller // Use local cover path if available, otherwise fall back to original URL if (!empty($metadata['local_cover_path'])) { - $video['poster_url'] = '/public/images/'.$metadata['local_cover_path']; + $video['poster_url'] = $metadata['local_cover_path']; } elseif (!empty($metadata['cover_url'])) { $video['poster_url'] = $metadata['cover_url']; } @@ -103,13 +103,13 @@ class AdultController extends Controller // Add local image paths and other metadata to the video data for template compatibility if (!empty($metadata['local_cover_path'])) { - $adultVideo['poster_url'] = '/public/images/'.$metadata['local_cover_path']; + $adultVideo['poster_url'] = '/images/'.$metadata['local_cover_path']; } elseif (!empty($metadata['cover_url'])) { $adultVideo['poster_url'] = $metadata['cover_url']; } if (!empty($metadata['local_screenshot_path'])) { - $adultVideo['screenshot_url'] = '/public/images/'.$metadata['local_screenshot_path']; + $adultVideo['screenshot_url'] = '/images/'.$metadata['local_screenshot_path']; } // Add actors data if available diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index cf02b11..8470c74 100644 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -7,22 +7,22 @@ use Psr\Http\Message\ServerRequestInterface as Request; use App\Models\Game; use App\Models\Movie; use App\Models\TvShow; -use App\Models\MusicArtist; +use App\Models\AdultVideo; use App\Models\SyncLog; use Slim\Views\Twig; class DashboardController extends Controller { - public function __construct(Twig $view) + private \PDO $pdo; + public function __construct(\PDO $pdo, Twig $view) { parent::__construct($view); + $this->pdo = $pdo; } public function index(Request $request, Response $response, $args) { - $pdo = $this->view->getEnvironment()->getGlobals()['pdo'] ?? null; - - if (!$pdo) { + if (!$this->pdo) { return $this->view->render($response, 'dashboard/index.twig', [ 'title' => 'Dashboard', 'stats' => [ @@ -38,22 +38,24 @@ class DashboardController extends Controller } // Get statistics from models - $gameStats = Game::getStats($pdo); - $movieStats = Movie::getStats($pdo); - $tvShowStats = TvShow::getStats($pdo); - $musicStats = MusicArtist::getStats($pdo); - $syncStats = SyncLog::getStats($pdo); + $gameStats = Game::getStats($this->pdo); + $movieStats = Movie::getStats($this->pdo); + $tvShowStats = TvShow::getStats($this->pdo); + $adultStats = AdultVideo::getStats($this->pdo); + // $musicStats = MusicArtist::getStats($this->pdo); + //$syncStats = SyncLog::getStats($this->pdo); // Get recent activity - $recentGames = Game::getRecent($pdo, 5); - $recentMovies = Movie::getRecent($pdo, 5); - $recentSyncs = SyncLog::getRecent($pdo, 5); + $recentGames = Game::getRecent($this->pdo, 5); + $recentMovies = Movie::getRecent($this->pdo, 5); + $recentSyncs = SyncLog::getRecent($this->pdo, 5); // Calculate total media count $totalMedia = ($gameStats['total_games'] ?? 0) + ($movieStats['total_movies'] ?? 0) + ($tvShowStats['total_shows'] ?? 0) + - ($musicStats['total_artists'] ?? 0); + ($musicStats['total_artists'] ?? 0)+ + ($adultStats['total_adult_videos'] ?? 0); $stats = [ 'total_media' => $totalMedia, @@ -63,11 +65,13 @@ class DashboardController extends Controller 'total_episodes' => $tvShowStats['total_episodes'] ?? 0, 'total_music' => $musicStats['total_artists'] ?? 0, 'total_playtime' => $gameStats['total_playtime'] ?? 0, + 'total_adult_videos' => $adultStats['total_adult_videos'] ?? 0, 'watched_movies' => $movieStats['watched_movies'] ?? 0, 'favorite_games' => $gameStats['favorite_games'] ?? 0, 'favorite_movies' => $movieStats['favorite_movies'] ?? 0, 'favorite_shows' => $tvShowStats['favorite_shows'] ?? 0, 'favorite_music' => $musicStats['favorite_artists'] ?? 0, + 'favorite_adult_videos' => $adultStats['favorite_adult_videos'] ?? 0, ]; return $this->view->render($response, 'dashboard/index.twig', [ diff --git a/app/Controllers/ImageController.php b/app/Controllers/ImageController.php new file mode 100644 index 0000000..34fd896 --- /dev/null +++ b/app/Controllers/ImageController.php @@ -0,0 +1,53 @@ +withStatus(404, 'Image not found'); + } + + // Get file extension and set appropriate content type + $extension = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION)); + $contentTypes = [ + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + 'svg' => 'image/svg+xml', + ]; + + $contentType = $contentTypes[$extension] ?? 'application/octet-stream'; + + // Read and serve the file + $fileContent = file_get_contents($fullPath); + + $response = $response->withHeader('Content-Type', $contentType); + $response = $response->withHeader('Cache-Control', 'public, max-age=3600'); // Cache for 1 hour + $response->getBody()->write($fileContent); + + return $response; + } +} diff --git a/app/Controllers/MediaSourceController.php b/app/Controllers/MediaSourceController.php new file mode 100644 index 0000000..c892e28 --- /dev/null +++ b/app/Controllers/MediaSourceController.php @@ -0,0 +1,219 @@ +source = new Source($pdo); + $this->syncLog = new SyncLog($pdo); + } + + // List all sources + public function index(Request $request, Response $response, array $args): Response + { + $sources = $this->source->all(); + + return $this->render($response, 'admin/sources.twig', [ + 'sources' => $sources, + 'current_route' => 'sources' + ]); + } + + // Show create form + public function create(Request $request, Response $response, array $args): Response + { + return $this->render($response, 'admin/sources/create.twig', [ + 'current_route' => 'sources' + ]); + } + + // Store new source + public function store(Request $request, Response $response, array $args): Response + { + $data = $request->getParsedBody(); + + // Basic validation + if (empty($data['name']) || empty($data['type']) || empty($data['path'])) { + $this->flash->addMessage('error', 'Name, type, and path are required'); + return $response->withHeader('Location', '/admin/sources/create')->withStatus(302); + } + + try { + $sourceData = [ + 'name' => $data['name'], + 'type' => $data['type'], + 'path' => $data['path'], + 'username' => $data['username'] ?? null, + 'password' => !empty($data['password']) ? password_hash($data['password'], PASSWORD_DEFAULT) : null, + 'is_active' => isset($data['is_active']) ? 1 : 0, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s') + ]; + + $this->source->create($sourceData); + $this->flash->addMessage('success', 'Source created successfully'); + return $response->withHeader('Location', '/admin/sources')->withStatus(302); + + } catch (\Exception $e) { + $this->flash->addMessage('error', 'Error creating source: ' . $e->getMessage()); + return $response->withHeader('Location', '/admin/sources/create')->withStatus(302); + } + } + + // Show edit form + public function edit(Request $request, Response $response, array $args): Response + { + $id = $args['id']; + $source = $this->source->find($id); + + if (!$source) { + $this->flash->addMessage('error', 'Source not found'); + return $response->withHeader('Location', '/admin/sources')->withStatus(302); + } + + return $this->render($response, 'admin/sources/edit.twig', [ + 'source' => $source, + 'current_route' => 'sources' + ]); + } + + // Update source + public function update(Request $request, Response $response, array $args): Response + { + $id = $args['id']; + $data = $request->getParsedBody(); + + try { + $sourceData = [ + 'name' => $data['name'], + 'type' => $data['type'], + 'path' => $data['path'], + 'username' => $data['username'] ?? null, + 'is_active' => isset($data['is_active']) ? 1 : 0, + 'updated_at' => date('Y-m-d H:i:s') + ]; + + // Only update password if provided + if (!empty($data['password'])) { + $sourceData['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } + + $this->source->update($id, $sourceData); + $this->flash->addMessage('success', 'Source updated successfully'); + return $response->withHeader('Location', '/admin/sources')->withStatus(302); + + } catch (\Exception $e) { + $this->flash->addMessage('error', 'Error updating source: ' . $e->getMessage()); + return $response->withHeader('Location', '/admin/sources/' . $id . '/edit')->withStatus(302); + } + } + + // Delete source + public function destroy(Request $request, Response $response, array $args): Response + { + $id = $args['id']; + + try { + $this->source->delete($id); + $this->flash->addMessage('success', 'Source deleted successfully'); + } catch (\Exception $e) { + $this->flash->addMessage('error', 'Error deleting source: ' . $e->getMessage()); + } + + return $response->withHeader('Location', '/admin/sources')->withStatus(302); + } + + // Start sync for a source + public function startSync(Request $request, Response $response, array $args): Response + { + $sourceId = $args['id']; + $source = $this->source->find($sourceId); + + if (!$source) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Source not found' + ], 404); + } + + try { + // Create a sync log entry + $logId = $this->syncLog->create([ + 'source_id' => $sourceId, + 'type' => 'full', + 'status' => 'pending', + 'started_at' => date('Y-m-d H:i:s'), + 'created_at' => date('Y-m-d H:i:s') + ]); + + // Start sync in background (you'll need to implement this) + $this->startBackgroundSync($sourceId, $logId); + + return $this->json($response, [ + 'success' => true, + 'message' => 'Sync started', + 'log_id' => $logId + ]); + + } catch (\Exception $e) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Error starting sync: ' . $e->getMessage() + ], 500); + } + } + + // Get sync status + public function syncStatus(Request $request, Response $response, array $args): Response + { + $logId = $args['log_id'] ?? null; + + if (!$logId) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Log ID is required' + ], 400); + } + + $log = $this->syncLog->find($logId); + + if (!$log) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Sync log not found' + ], 404); + } + + return $this->json($response, [ + 'success' => true, + 'status' => $log['status'], + 'progress' => $log['progress'] ?? 0, + 'message' => $log['message'] ?? '' + ]); + } + + // Start background sync process + private function startBackgroundSync($sourceId, $logId) + { + // This is a simplified example - you'll need to implement this based on your needs + $command = sprintf( + 'php %s/console.php sync:source %d --log=%d > /dev/null 2>&1 &', + dirname(__DIR__, 3), + $sourceId, + $logId + ); + + exec($command); + } +} diff --git a/app/Controllers/SettingsController.php b/app/Controllers/SettingsController.php new file mode 100644 index 0000000..c1f7165 --- /dev/null +++ b/app/Controllers/SettingsController.php @@ -0,0 +1,158 @@ +pdo = $pdo; + } + + public function index(Request $request, Response $response, $args) + { + $settings = $this->getSettings(); + $sources = $this->getSources(); + + return $this->view->render($response, 'admin/settings.twig', [ + 'title' => 'Admin Settings', + 'settings' => $settings, + 'sources' => $sources + ]); + } + + public function save(Request $request, Response $response, $args) + { + $data = $request->getParsedBody(); + + // Save general settings + $this->saveGeneralSettings($data); + + // Save source-specific settings + if (isset($data['sources']) && is_array($data['sources'])) { + $this->saveSourceSettings($data['sources']); + } + + return $this->view->render($response, 'admin/settings.twig', [ + 'title' => 'Admin Settings', + 'settings' => $this->getSettings(), + 'sources' => $this->getSources(), + 'success' => 'Settings saved successfully!' + ]); + } + + private function getSettings(): array + { + // Get general application settings + $settings = []; + + // Get media type visibility settings + $stmt = $this->pdo->prepare("SELECT setting_key, setting_value FROM settings WHERE setting_key LIKE 'media_visibility_%'"); + $stmt->execute(); + $mediaVisibilitySettings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); + + $settings['media_visibility'] = [ + 'games' => $mediaVisibilitySettings['media_visibility_games'] ?? 'authenticated', // Default: authenticated users only + 'movies' => $mediaVisibilitySettings['media_visibility_movies'] ?? 'authenticated', + 'tvshows' => $mediaVisibilitySettings['media_visibility_tvshows'] ?? 'authenticated', + 'music' => $mediaVisibilitySettings['media_visibility_music'] ?? 'authenticated', + 'adult' => $mediaVisibilitySettings['media_visibility_adult'] ?? 'authenticated', // Adult content requires auth by default + 'actors' => $mediaVisibilitySettings['media_visibility_actors'] ?? 'authenticated' + ]; + + // You can extend this to include more settings like: + // - Sync intervals + // - Default sync types + // - Notification preferences + // - Theme settings + // - etc. + + return $settings; + } + + private function getSources(): array + { + $stmt = $this->pdo->prepare("SELECT * FROM sources ORDER BY display_name"); + $stmt->execute(); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + private function saveGeneralSettings(array $data): void + { + // Save general settings to a settings table or config file + // For now, we'll store them in a simple settings table + + foreach ($data as $key => $value) { + if (strpos($key, 'setting_') === 0) { + $settingKey = substr($key, 8); // Remove 'setting_' prefix + + // Check if setting exists + $stmt = $this->pdo->prepare("SELECT id FROM settings WHERE setting_key = :key LIMIT 1"); + $stmt->execute(['key' => $settingKey]); + $existing = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($existing) { + // Update existing setting + $stmt = $this->pdo->prepare("UPDATE settings SET setting_value = :value WHERE setting_key = :key"); + $stmt->execute(['key' => $settingKey, 'value' => $value]); + } else { + // Insert new setting + $stmt = $this->pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (:key, :value)"); + $stmt->execute(['key' => $settingKey, 'value' => $value]); + } + } + } + + // Save media visibility settings + if (isset($data['media_visibility']) && is_array($data['media_visibility'])) { + foreach ($data['media_visibility'] as $mediaType => $visibility) { + $settingKey = "media_visibility_{$mediaType}"; + + // Check if setting exists + $stmt = $this->pdo->prepare("SELECT id FROM settings WHERE setting_key = :key LIMIT 1"); + $stmt->execute(['key' => $settingKey]); + $existing = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($existing) { + // Update existing setting + $stmt = $this->pdo->prepare("UPDATE settings SET setting_value = :value WHERE setting_key = :key"); + $stmt->execute(['key' => $settingKey, 'value' => $visibility]); + } else { + // Insert new setting + $stmt = $this->pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (:key, :value)"); + $stmt->execute(['key' => $settingKey, 'value' => $visibility]); + } + } + } + } + + private function saveSourceSettings(array $sources): void + { + foreach ($sources as $sourceId => $sourceData) { + // Update source configuration + $config = isset($sourceData['config']) ? json_encode($sourceData['config']) : '{}'; + + $stmt = $this->pdo->prepare(" + UPDATE sources + SET api_url = :api_url, api_key = :api_key, config = :config, is_active = :is_active + WHERE id = :id + "); + + $stmt->execute([ + 'id' => $sourceId, + 'api_url' => $sourceData['api_url'] ?? '', + 'api_key' => $sourceData['api_key'] ?? '', + 'config' => $config, + 'is_active' => isset($sourceData['is_active']) ? 1 : 0 + ]); + } + } +} diff --git a/app/Controllers/SyncController.php b/app/Controllers/SyncController.php new file mode 100644 index 0000000..1671fe8 --- /dev/null +++ b/app/Controllers/SyncController.php @@ -0,0 +1,294 @@ +source = new Source($pdo); + $this->syncLog = new SyncLog($pdo); + + // Initialize sync services + $this->syncServices = [ + 'jellyfin' => new JellyfinSyncService($pdo), + 'local' => new LocalSyncService($pdo), + 'samba' => new SambaSyncService($pdo), + 'nfs' => new NfsSyncService($pdo) + ]; + } + + // Show sync dashboard + public function index(Request $request, Response $response, array $args): Response + { + // Get recent sync logs + $recentLogs = $this->syncLog->orderBy('created_at', 'desc')->limit(10)->get(); + + // Get sync statistics + $stats = [ + 'total_syncs' => $this->syncLog->count(), + 'successful_syncs' => $this->syncLog->where('status', 'completed')->count(), + 'failed_syncs' => $this->syncLog->where('status', 'failed')->count(), + 'pending_syncs' => $this->syncLog->where('status', 'pending')->count(), + ]; + + return $this->render($response, 'admin/sync/index.twig', [ + 'recent_logs' => $recentLogs, + 'stats' => $stats, + 'current_route' => 'sync' + ]); + } + + // Start a new sync + public function start(Request $request, Response $response, array $args): Response + { + $data = $request->getParsedBody(); + $type = $data['type'] ?? 'full'; // full, scan, update + $sourceId = $data['source_id'] ?? null; + + try { + // Create a new sync log + $logData = [ + 'source_id' => $sourceId, + 'sync_type' => $type, + 'status' => 'pending', + 'started_at' => date('Y-m-d H:i:s'), + 'created_at' => date('Y-m-d H:i:s') + ]; + + $logId = $this->syncLog->create($logData); + + // Start sync in background + $this->startBackgroundSync($sourceId, $logId, $type); + + return $this->json($response, [ + 'success' => true, + 'message' => 'Sync started', + 'log_id' => $logId + ]); + + } catch (\Exception $e) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Error starting sync: ' . $e->getMessage() + ], 500); + } + } + + // Get sync status + public function status(Request $request, Response $response, array $args): Response + { + $logId = $args['log_id'] ?? null; + + if (!$logId) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Log ID is required' + ], 400); + } + + $log = $this->syncLog->find($logId); + + if (!$log) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Sync log not found' + ], 404); + } + + // Get detailed status from the sync service if available + $status = [ + 'status' => $log['status'], + 'progress' => (int)($log['progress'] ?? 0), + 'message' => $log['message'] ?? '', + 'started_at' => $log['started_at'], + 'completed_at' => $log['completed_at'] ?? null, + 'total_items' => (int)($log['total_items'] ?? 0), + 'processed_items' => (int)($log['processed_items'] ?? 0), + 'new_items' => (int)($log['new_items'] ?? 0), + 'updated_items' => (int)($log['updated_items'] ?? 0), + 'deleted_items' => (int)($log['deleted_items'] ?? 0) + ]; + + return $this->json($response, [ + 'success' => true, + 'log' => $status + ]); + } + + // Cancel a running sync + public function cancel(Request $request, Response $response, array $args): Response + { + $logId = $args['log_id'] ?? null; + + if (!$logId) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Log ID is required' + ], 400); + } + + try { + // Update the log status to cancelled + $this->syncLog->update($logId, [ + 'status' => 'cancelled', + 'completed_at' => date('Y-m-d H:i:s'), + 'message' => 'Sync cancelled by user' + ]); + + // TODO: Send a signal to the running process to cancel + + return $this->json($response, [ + 'success' => true, + 'message' => 'Sync cancelled' + ]); + + } catch (\Exception $e) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Error cancelling sync: ' . $e->getMessage() + ], 500); + } + } + + // Clear sync logs + public function clearLogs(Request $request, Response $response, array $args): Response + { + $type = $request->getParsedBody()['type'] ?? 'completed'; // completed, all + + try { + if ($type === 'all') { + $this->syncLog->query('TRUNCATE TABLE sync_logs'); + } else { + $this->syncLog->where('status', 'completed')->delete(); + $this->syncLog->where('status', 'failed')->delete(); + $this->syncLog->where('status', 'cancelled')->delete(); + } + + return $this->json($response, [ + 'success' => true, + 'message' => 'Logs cleared successfully' + ]); + + } catch (\Exception $e) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Error clearing logs: ' . $e->getMessage() + ], 500); + } + } + + // Start background sync process + private function startBackgroundSync($sourceId, $logId, $type = 'full') + { + // This is a simplified example - you'll need to implement this based on your needs + $command = sprintf( + 'php %s/console.php sync:start --log=%d --type=%s %s > /dev/null 2>&1 &', + dirname(__DIR__, 3), // Path to your project root + $logId, + escapeshellarg($type), + $sourceId ? '--source=' . $sourceId : '' + ); + + exec($command); + } + + // Process sync (called from CLI) + public function processSync($sourceId, $logId, $type = 'full') + { + try { + $log = $this->syncLog->find($logId); + + if (!$log) { + throw new \Exception('Sync log not found'); + } + + // Update log status to started + $this->syncLog->update($logId, [ + 'status' => 'in_progress', + 'started_at' => date('Y-m-d H:i:s'), + 'message' => 'Sync started' + ]); + + $source = null; + if ($sourceId) { + $source = $this->source->find($sourceId); + if (!$source) { + throw new \Exception('Source not found'); + } + } + + // Get the appropriate sync service + $service = $this->getSyncService($source ? $source['type'] : 'local'); + + // Start sync + $result = $service->sync($source, $type, function($progress, $message) use ($logId) { + // Update progress callback + $this->updateSyncProgress($logId, $progress, $message); + }); + + // Update log with final status + $this->syncLog->update($logId, [ + 'status' => $result['success'] ? 'completed' : 'failed', + 'completed_at' => date('Y-m-d H:i:s'), + 'message' => $result['message'] ?? 'Sync completed', + 'total_items' => $result['total_items'] ?? 0, + 'processed_items' => $result['processed_items'] ?? 0, + 'new_items' => $result['new_items'] ?? 0, + 'updated_items' => $result['updated_items'] ?? 0, + 'deleted_items' => $result['deleted_items'] ?? 0, + 'errors' => !empty($result['errors']) ? json_encode($result['errors']) : null + ]); + + return $result; + + } catch (\Exception $e) { + // Update log with error + if (isset($logId) && $this->syncLog) { + $this->syncLog->update($logId, [ + 'status' => 'failed', + 'completed_at' => date('Y-m-d H:i:s'), + 'message' => 'Error: ' . $e->getMessage() + ]); + } + + throw $e; + } + } + + // Update sync progress + private function updateSyncProgress($logId, $progress, $message = '') + { + $this->syncLog->update($logId, [ + 'progress' => $progress, + 'message' => $message, + 'updated_at' => date('Y-m-d H:i:s') + ]); + } + + // Get the appropriate sync service + private function getSyncService($type) + { + $type = strtolower($type); + + if (!isset($this->syncServices[$type])) { + throw new \Exception("Unsupported source type: $type"); + } + + return $this->syncServices[$type]; + } +} diff --git a/app/Controllers/TvShowController.php b/app/Controllers/TvShowController.php index 91d9b6d..8847311 100644 --- a/app/Controllers/TvShowController.php +++ b/app/Controllers/TvShowController.php @@ -41,7 +41,11 @@ class TvShowController extends Controller $totalPages = ceil($totalCount / $perPage); $hasNextPage = $page < $totalPages; $hasPrevPage = $page > 1; - +/* + echo '
';
+ print_r($tvshows);
+ die();
+*/
return $this->view->render($response, 'tvshows/index.twig', [
'title' => 'TV Shows',
'tvshows' => $tvshows,
@@ -107,4 +111,67 @@ class TvShowController extends Controller
'seasons' => $seasons
]);
}
+
+ public function delete(Request $request, Response $response, $args)
+ {
+ $tvShowId = (int) $args['id'];
+
+ // Get TV show details to access metadata
+ $stmt = $this->pdo->prepare("
+ SELECT t.*
+ FROM tv_shows t
+ WHERE t.id = :id
+ ");
+ $stmt->execute(['id' => $tvShowId]);
+ $tvShow = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ if (!$tvShow) {
+ return $response->withStatus(404);
+ }
+
+ // Decode metadata to find image paths
+ $metadata = json_decode($tvShow['metadata'], true);
+
+ // Delete associated images
+ $imagesDeleted = [];
+ if (!empty($metadata['local_poster_path'])) {
+ $posterPath = __DIR__ . '/../storage/images/' . $metadata['local_poster_path'];
+ if (file_exists($posterPath)) {
+ unlink($posterPath);
+ $imagesDeleted[] = $metadata['local_poster_path'];
+ }
+ }
+
+ if (!empty($metadata['local_backdrop_path'])) {
+ $backdropPath = __DIR__ . '/../storage/images/' . $metadata['local_backdrop_path'];
+ if (file_exists($backdropPath)) {
+ unlink($backdropPath);
+ $imagesDeleted[] = $metadata['local_backdrop_path'];
+ }
+ }
+
+ // Delete actor relationships
+ $stmt = $this->pdo->prepare("
+ DELETE FROM actor_tv_show
+ WHERE tv_show_id = :tv_show_id
+ ");
+ $stmt->execute(['tv_show_id' => $tvShowId]);
+
+ // Delete the TV show record
+ $stmt = $this->pdo->prepare("DELETE FROM tv_shows WHERE id = :id");
+ $result = $stmt->execute(['id' => $tvShowId]);
+
+ if ($result) {
+ return $response->withJson([
+ 'success' => true,
+ 'message' => 'TV show deleted successfully',
+ 'images_deleted' => $imagesDeleted
+ ]);
+ } else {
+ return $response->withJson([
+ 'success' => false,
+ 'message' => 'Failed to delete TV show'
+ ], 500);
+ }
+ }
}
diff --git a/app/Http/Middleware/MediaVisibilityMiddleware.php b/app/Http/Middleware/MediaVisibilityMiddleware.php
new file mode 100644
index 0000000..cec74f7
--- /dev/null
+++ b/app/Http/Middleware/MediaVisibilityMiddleware.php
@@ -0,0 +1,68 @@
+getUri()->getPath();
+
+ // Map routes to media types
+ $mediaRoutes = [
+ '/media/games' => 'games',
+ '/media/movies' => 'movies',
+ '/media/tv-shows' => 'tvshows',
+ '/media/music' => 'music',
+ '/media/adult' => 'adult',
+ '/media/actors' => 'actors'
+ ];
+
+ foreach ($mediaRoutes as $route => $mediaType) {
+ if (strpos($path, $route) === 0) {
+ // Check if this media type is visible to the current user
+ if (!$this->isMediaTypeVisible($mediaType)) {
+ // Redirect to login or show 404 based on configuration
+ if (!is_logged_in()) {
+ return $handler->handle($request)->withStatus(401)->withHeader('Location', '/login');
+ } else {
+ return $handler->handle($request)->withStatus(404);
+ }
+ }
+ break;
+ }
+ }
+
+ return $handler->handle($request);
+ }
+
+ private function isMediaTypeVisible(string $mediaType): bool
+ {
+ // Get database connection
+ $pdo = \App\Database\Database::getInstance();
+
+ // Get media visibility setting
+ $stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = :key LIMIT 1");
+ $stmt->execute(['key' => "media_visibility_{$mediaType}"]);
+ $visibility = $stmt->fetchColumn() ?: 'authenticated'; // Default to authenticated only
+
+ // Check user authentication status
+ $isLoggedIn = is_logged_in();
+
+ switch ($visibility) {
+ case 'public':
+ return true; // Visible to everyone
+ case 'authenticated':
+ return $isLoggedIn; // Visible only to authenticated users
+ case 'hidden':
+ return false; // Hidden from all users
+ default:
+ return $isLoggedIn; // Default to authenticated only
+ }
+ }
+}
diff --git a/app/Models/AdultVideo.php b/app/Models/AdultVideo.php
index 2a9699e..0e0e0da 100644
--- a/app/Models/AdultVideo.php
+++ b/app/Models/AdultVideo.php
@@ -166,4 +166,19 @@ class AdultVideo extends Model
'cast' => $castString
]);
}
+
+ /**
+ * Get TV show statistics
+ */
+ public static function getStats(\PDO $pdo): array
+ {
+ $stmt = $pdo->query("
+ SELECT
+ COUNT(*) as total_adult_videos,
+ COUNT(CASE WHEN is_favorite = 1 THEN 1 END) as favorite_adult_videos,
+ AVG(rating) as avg_rating
+ FROM adult_videos
+ ");
+ return $stmt->fetch(\PDO::FETCH_ASSOC);
+ }
}
diff --git a/app/Services/JellyfinSyncService.php b/app/Services/JellyfinSyncService.php
index c9b371a..524d95c 100644
--- a/app/Services/JellyfinSyncService.php
+++ b/app/Services/JellyfinSyncService.php
@@ -790,7 +790,7 @@ class JellyfinSyncService extends BaseSyncService
try {
// Create images directory structure if it doesn't exist
- $imagesDir = "public/images/{$type}";
+ $imagesDir = __DIR__ . "/../../storage/images/{$type}";
if (!is_dir($imagesDir)) {
if (!mkdir($imagesDir, 0755, true)) {
$this->logProgress("Warning: Could not create images directory: {$imagesDir}");
diff --git a/app/Services/LocalSyncService.php b/app/Services/LocalSyncService.php
new file mode 100644
index 0000000..a795b09
--- /dev/null
+++ b/app/Services/LocalSyncService.php
@@ -0,0 +1,313 @@
+ ['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']);
+ }
+}
diff --git a/app/Services/StashSyncService.php b/app/Services/StashSyncService.php
index 69ae9c4..eb7d58e 100644
--- a/app/Services/StashSyncService.php
+++ b/app/Services/StashSyncService.php
@@ -36,7 +36,7 @@ class StashSyncService extends BaseSyncService
'verify' => false // Disable SSL verification for problematic servers
]);
- $this->imageDownloader = new ImageDownloader('public/images', $this->apiKey);
+ $this->imageDownloader = new ImageDownloader(__DIR__ . '/../../storage/images', $this->apiKey);
}
protected function executeSync(string $syncType): void
diff --git a/app/Services/SyncServiceInterface.php b/app/Services/SyncServiceInterface.php
new file mode 100644
index 0000000..ff883c5
--- /dev/null
+++ b/app/Services/SyncServiceInterface.php
@@ -0,0 +1,23 @@
+imageDownloader = new ImageDownloader('public/images');
+ $this->imageDownloader = new ImageDownloader(__DIR__ . '/../../storage/images');
}
protected function executeSync(string $syncType): void
diff --git a/app/Utils/ImageDownloader.php b/app/Utils/ImageDownloader.php
index d74d8da..26373ba 100644
--- a/app/Utils/ImageDownloader.php
+++ b/app/Utils/ImageDownloader.php
@@ -10,7 +10,7 @@ class ImageDownloader
private Client $httpClient;
private string $basePath;
- public function __construct(string $basePath = 'public/images', ?string $apiKey = null)
+ public function __construct(string $basePath = 'storage/images', ?string $apiKey = null)
{
$headers = [
'User-Agent' => 'MediaCollector/1.0'
@@ -25,7 +25,13 @@ class ImageDownloader
'headers' => $headers,
'verify' => false // Disable SSL verification for problematic servers
]);
- $this->basePath = rtrim($basePath, '/');
+
+ // Convert relative path to absolute path
+ if (strpos($basePath, '/') !== 0) {
+ $this->basePath = __DIR__ . '/../' . $basePath;
+ } else {
+ $this->basePath = $basePath;
+ }
}
/**
@@ -115,6 +121,8 @@ class ImageDownloader
return true;
}
}
+
+ return false; // Not a valid image type
}
public function saveImage(string $imageData, string $filename, string $subfolder = ''): ?string
@@ -167,9 +175,9 @@ class ImageDownloader
return null;
}
- // Remove the public/ prefix to get the web-accessible path
+ // Remove the absolute basePath prefix to get the relative path
$relativePath = str_replace($this->basePath . '/', '', $localPath);
- return '/' . $relativePath;
+ return '/images/' . $relativePath;
}
}
diff --git a/app/helpers.php b/app/helpers.php
index ea48236..de3d659 100644
--- a/app/helpers.php
+++ b/app/helpers.php
@@ -164,9 +164,29 @@ function array_to_object(array $array): object
}
/**
- * Convert object to array recursively
+ * Check if a media type is visible to the current user
*/
-function object_to_array(object $object): array
+function is_media_type_visible(string $mediaType): bool
{
- return json_decode(json_encode($object), true);
+ // Get database connection
+ $pdo = \App\Database\Database::getInstance();
+
+ // Get media visibility setting
+ $stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = :key LIMIT 1");
+ $stmt->execute(['key' => "media_visibility_{$mediaType}"]);
+ $visibility = $stmt->fetchColumn() ?: 'authenticated'; // Default to authenticated only
+
+ // Check user authentication status
+ $isLoggedIn = is_logged_in();
+
+ switch ($visibility) {
+ case 'public':
+ return true; // Visible to everyone
+ case 'authenticated':
+ return $isLoggedIn; // Visible only to authenticated users
+ case 'hidden':
+ return false; // Hidden from all users
+ default:
+ return $isLoggedIn; // Default to authenticated only
+ }
}
diff --git a/public/index.php b/public/index.php
index 490ca00..36e539f 100644
--- a/public/index.php
+++ b/public/index.php
@@ -55,6 +55,11 @@ $container->set('view', function () use ($container) {
$_SERVER['HTTP_HOST'] ?? 'localhost'
);
}));
+
+ // Add media visibility function
+ $twig->getEnvironment()->addFunction(new TwigFunction('is_media_type_visible', function ($mediaType) {
+ return is_media_type_visible($mediaType);
+ }));
// Placeholder path_for function - will be updated after routes are registered
$twig->getEnvironment()->addFunction(new TwigFunction('path_for', function ($name, $data = [], $queryParams = []) {
@@ -84,6 +89,12 @@ $container->set('view', function () use ($container) {
case 'admin.index':
$basePath = '/admin';
break;
+ case 'admin.settings':
+ $basePath = '/admin/settings';
+ break;
+ case 'admin.sources':
+ $basePath = '/admin/sources';
+ break;
case 'admin.sync':
$basePath = '/admin/sync/' . ($data['id'] ?? '');
break;
@@ -144,6 +155,9 @@ $container->set('view', function () use ($container) {
$authService = $container->get(\App\Services\AuthService::class);
return $authService->generateCSRFToken();
}));
+
+
+
$twig->getEnvironment()->addFilter(new TwigFilter('format_duration', function ($minutes) {
if (!$minutes || $minutes == 0) {
return '0m';
@@ -199,7 +213,7 @@ $container->set(\App\Controllers\GameController::class, function ($c) {
});
$container->set(\App\Controllers\DashboardController::class, function ($c) {
- return new \App\Controllers\DashboardController($c->get('view'));
+ return new \App\Controllers\DashboardController($c->get(PDO::class), $c->get('view'));
});
$container->set(\App\Controllers\MovieController::class, function ($c) {
@@ -226,14 +240,25 @@ $container->set(\App\Controllers\SearchController::class, function ($c) {
return new \App\Controllers\SearchController($c->get(PDO::class), $c->get('view'));
});
+$container->set(\App\Controllers\ImageController::class, function ($c) {
+ return new \App\Controllers\ImageController($c->get('view'));
+});
+
+$container->set(\App\Controllers\SettingsController::class, function ($c) {
+ return new \App\Controllers\SettingsController($c->get(PDO::class), $c->get('view'));
+});
// Register middleware
$container->set(\App\Http\Middleware\AuthMiddleware::class, function ($c) {
return new \App\Http\Middleware\AuthMiddleware($c->get(\App\Services\AuthService::class));
});
-$container->set(\App\Http\Middleware\AdminMiddleware::class, function ($c) {
- return new \App\Http\Middleware\AdminMiddleware($c->get(\App\Services\AuthService::class));
+$container->set(\App\Controllers\MediaSourceController::class, function ($c) {
+ return new \App\Controllers\MediaSourceController($c->get(PDO::class), $c->get('view'));
+});
+
+$container->set(\App\Http\Middleware\MediaVisibilityMiddleware::class, function ($c) {
+ return new \App\Http\Middleware\MediaVisibilityMiddleware();
});
// Create App with DI Container
diff --git a/resources/views/actor/index.twig b/resources/views/actor/index.twig
index c355789..7037d43 100644
--- a/resources/views/actor/index.twig
+++ b/resources/views/actor/index.twig
@@ -12,12 +12,12 @@
{% if actors %}
{% for actor in actors %}
-