From eb1ec1153da19e5a293a9634e680aa312337e6f1 Mon Sep 17 00:00:00 2001 From: Lars Behrends Date: Sun, 18 Jan 2026 01:42:03 +0100 Subject: [PATCH] Remove obsolete test scripts and add new API controllers for dashboard and game management - Deleted test scripts: test_jellyfin_execution.php, test_stash.php, test_xbvr.php, test_xbvr_sync.php, vite.config.js - Added DashboardController for fetching dashboard statistics and recent activity - Added GameController for managing games, including fetching all games, game details, and games by category - Introduced various check scripts to validate database structures and data integrity for adult videos, games, gender data, posters, and TV show actors --- .gitignore | 3 + app/Controllers/Api/BaseApiController.php | 29 + app/Controllers/Api/DashboardController.php | 206 +++ app/Controllers/Api/GameController.php | 364 +++++ app/Controllers/Api/MediaController.php | 1150 ++++++++++++- app/Models/Actor.php | 260 ++- app/Models/AdultVideo.php | 190 ++- app/Models/TvShow.php | 268 +++- check_adult_structure.php | 40 + check_adult_videos.php | 0 check_games_columns.php | 23 + check_gender_data.php | 80 + check_poster_structure.php | 68 + check_tvshow_actors.php | 89 + debug_jellyfin_sync.php | 90 -- package-lock.json | 1607 ------------------- package.json | 21 - postcss.config.js | 6 - routes/api2.php | 16 +- routes/web.php | 6 +- tailwind.config.js | 13 - test_auth.php | 70 - test_episode_sync.php | 42 - test_jellyfin.php | 115 -- test_jellyfin_execution.php | 45 - test_stash.php | 136 -- test_xbvr.php | 107 -- test_xbvr_sync.php | 64 - vite.config.js | 31 - 29 files changed, 2685 insertions(+), 2454 deletions(-) create mode 100644 app/Controllers/Api/DashboardController.php create mode 100644 app/Controllers/Api/GameController.php create mode 100644 check_adult_structure.php create mode 100644 check_adult_videos.php create mode 100644 check_games_columns.php create mode 100644 check_gender_data.php create mode 100644 check_poster_structure.php create mode 100644 check_tvshow_actors.php delete mode 100644 debug_jellyfin_sync.php delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 postcss.config.js delete mode 100644 tailwind.config.js delete mode 100644 test_auth.php delete mode 100644 test_episode_sync.php delete mode 100644 test_jellyfin.php delete mode 100644 test_jellyfin_execution.php delete mode 100644 test_stash.php delete mode 100644 test_xbvr.php delete mode 100644 test_xbvr_sync.php delete mode 100644 vite.config.js diff --git a/.gitignore b/.gitignore index 815777d..5e3417e 100644 --- a/.gitignore +++ b/.gitignore @@ -149,3 +149,6 @@ composer.lock /public/public/images/backdrops /public/public/images/posters /storage/images + + +/frontend/ \ No newline at end of file diff --git a/app/Controllers/Api/BaseApiController.php b/app/Controllers/Api/BaseApiController.php index c5834ca..8b0003e 100644 --- a/app/Controllers/Api/BaseApiController.php +++ b/app/Controllers/Api/BaseApiController.php @@ -8,6 +8,35 @@ use App\Controllers\Controller; class BaseApiController extends Controller { + protected function getPdo(): \PDO + { + // Get PDO from the container - this assumes PDO is registered in the DI container + global $container; + if ($container && $container->has('pdo')) { + return $container->get('pdo'); + } + + // Fallback to creating a new PDO connection + $host = $_ENV['DB_HOST'] ?? 'localhost'; + $dbname = $_ENV['DB_NAME'] ?? 'medialib'; + $username = $_ENV['DB_USER'] ?? 'root'; + $password = $_ENV['DB_PASS'] ?? ''; + + try { + return new \PDO( + "mysql:host=$host;dbname=$dbname;charset=utf8mb4", + $username, + $password, + [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, + \PDO::ATTR_EMULATE_PREPARES => false, + ] + ); + } catch (\PDOException $e) { + throw new \Exception('Database connection failed: ' . $e->getMessage()); + } + } protected function success(Response $response, $data = null, int $status = 200): Response { $responseData = ['success' => true]; diff --git a/app/Controllers/Api/DashboardController.php b/app/Controllers/Api/DashboardController.php new file mode 100644 index 0000000..b5a1033 --- /dev/null +++ b/app/Controllers/Api/DashboardController.php @@ -0,0 +1,206 @@ +pdo = $pdo; + } + + /** + * Get dashboard statistics + */ + public function getStats(Request $request, Response $response): Response + { + try { + $stats = []; + + // Get movies count + $stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM movies"); + $stmt->execute(); + $moviesCount = $stmt->fetch(\PDO::FETCH_ASSOC)['count']; + $stats[] = [ + 'name' => 'Total Movies', + 'value' => number_format($moviesCount), + 'icon' => 'FilmIcon', + 'color' => 'bg-blue-500', + 'href' => '/movies' + ]; + + // Get TV shows count + $stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM tv_shows"); + $stmt->execute(); + $tvShowsCount = $stmt->fetch(\PDO::FETCH_ASSOC)['count']; + $stats[] = [ + 'name' => 'TV Shows', + 'value' => number_format($tvShowsCount), + 'icon' => 'TvIcon', + 'color' => 'bg-purple-500', + 'href' => '/tvshows' + ]; + + // Get games count + $stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM games"); + $stmt->execute(); + $gamesCount = $stmt->fetch(\PDO::FETCH_ASSOC)['count']; + $stats[] = [ + 'name' => 'Games', + 'value' => number_format($gamesCount), + 'icon' => 'GamepadIcon', + 'color' => 'bg-green-500', + 'href' => '/games' + ]; + + // Get music albums count + $stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM music_albums"); + $stmt->execute(); + $albumsCount = $stmt->fetch(\PDO::FETCH_ASSOC)['count']; + $stats[] = [ + 'name' => 'Music Albums', + 'value' => number_format($albumsCount), + 'icon' => 'MusicalNoteIcon', + 'color' => 'bg-pink-500', + 'href' => '/music' + ]; + + // Get adult videos count + $stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM adult_videos"); + $stmt->execute(); + $adultCount = $stmt->fetch(\PDO::FETCH_ASSOC)['count']; + $stats[] = [ + 'name' => 'Adult Videos', + 'value' => number_format($adultCount), + 'icon' => 'LockClosedIcon', + 'color' => 'bg-red-500', + 'href' => '/adult' + ]; + + // Get actors count + $stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM actors"); + $stmt->execute(); + $actorsCount = $stmt->fetch(\PDO::FETCH_ASSOC)['count']; + $stats[] = [ + 'name' => 'Actors', + 'value' => number_format($actorsCount), + 'icon' => 'UserIcon', + 'color' => 'bg-indigo-500', + 'href' => '/actors' + ]; + + return $this->success($response, $stats); + } catch (\Exception $e) { + return $this->error($response, 'Failed to fetch dashboard stats', 500); + } + } + + /** + * Get recent activity + */ + public function getRecentActivity(Request $request, Response $response): Response + { + try { + $activities = []; + + // Get recent movies (last 5) + $stmt = $this->pdo->prepare(" + SELECT 'Added movie' as action, title as item, + DATE_FORMAT(created_at, '%b %d, %Y') as time, + 'movie' as type + FROM movies + ORDER BY created_at DESC + LIMIT 5 + "); + $stmt->execute(); + $recentMovies = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $activities = array_merge($activities, $recentMovies); + + // Get recent TV shows (last 5) + $stmt = $this->pdo->prepare(" + SELECT 'Added TV show' as action, title as item, + DATE_FORMAT(created_at, '%b %d, %Y') as time, + 'tvshow' as type + FROM tv_shows + ORDER BY created_at DESC + LIMIT 5 + "); + $stmt->execute(); + $recentTvShows = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $activities = array_merge($activities, $recentTvShows); + + // Get recent games (last 5) + $stmt = $this->pdo->prepare(" + SELECT 'Added game' as action, title as item, + DATE_FORMAT(created_at, '%b %d, %Y') as time, + 'game' as type + FROM games + ORDER BY created_at DESC + LIMIT 5 + "); + $stmt->execute(); + $recentGames = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $activities = array_merge($activities, $recentGames); + + // Get recent music albums (last 5) + $stmt = $this->pdo->prepare(" + SELECT 'Added album' as action, title as item, + DATE_FORMAT(created_at, '%b %d, %Y') as time, + 'music' as type + FROM music_albums + ORDER BY created_at DESC + LIMIT 5 + "); + $stmt->execute(); + $recentAlbums = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $activities = array_merge($activities, $recentAlbums); + + // Sort all activities by time (most recent first) + usort($activities, function($a, $b) { + return strtotime($b['time']) - strtotime($a['time']); + }); + + // Take only the 10 most recent activities + $activities = array_slice($activities, 0, 10); + + // Format time to be more relative + foreach ($activities as &$activity) { + $activity['id'] = uniqid(); + $activity['time'] = $this->formatRelativeTime($activity['time']); + } + + return $this->success($response, $activities); + } catch (\Exception $e) { + return $this->error($response, 'Failed to fetch recent activity', 500); + } + } + + /** + * Format time to relative format (simplified version) + */ + private function formatRelativeTime($dateString): string + { + $date = strtotime($dateString); + $now = time(); + $diff = $now - $date; + + if ($diff < 3600) { + $minutes = floor($diff / 60); + return $minutes <= 1 ? 'Just now' : "$minutes minutes ago"; + } elseif ($diff < 86400) { + $hours = floor($diff / 3600); + return $hours <= 1 ? '1 hour ago' : "$hours hours ago"; + } elseif ($diff < 604800) { + $days = floor($diff / 86400); + return $days <= 1 ? '1 day ago' : "$days days ago"; + } else { + return date('M j, Y', $date); + } + } +} diff --git a/app/Controllers/Api/GameController.php b/app/Controllers/Api/GameController.php new file mode 100644 index 0000000..3c300f8 --- /dev/null +++ b/app/Controllers/Api/GameController.php @@ -0,0 +1,364 @@ +getQueryParams(); + $search = $queryParams['search'] ?? ''; + $sort = $queryParams['sort'] ?? 'title_asc'; + + try { + // Get games from database + $games = $this->getAllGamesWithCategories($search, $sort); + + // Group games by completion status + $groupedGames = $this->groupGamesByCategory($games); + + return $this->json($response, [ + 'success' => true, + 'data' => $groupedGames + ]); + } catch (\Exception $e) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Failed to fetch games: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get game by ID + */ + public function show(Request $request, Response $response, array $args): Response + { + $gameId = $args['id'] ?? null; + + if (!$gameId) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Game ID is required' + ], 400); + } + + try { + $game = $this->getGameDetails($gameId); + + if (!$game) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Game not found' + ], 404); + } + + return $this->json($response, [ + 'success' => true, + 'data' => $game + ]); + } catch (\Exception $e) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Failed to fetch game: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get games by category (BEATEN, PLAYING, etc.) + */ + public function getByCategory(Request $request, Response $response, array $args): Response + { + $category = strtoupper($args['category'] ?? ''); + $queryParams = $request->getQueryParams(); + $search = $queryParams['search'] ?? ''; + $sort = $queryParams['sort'] ?? 'title_asc'; + + if (!in_array($category, ['BEATEN', 'PLAYING', 'COMPLETED', 'UNPLAYED'])) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Invalid category. Must be one of: BEATEN, PLAYING, COMPLETED, UNPLAYED' + ], 400); + } + + try { + $games = $this->getGamesByCategory($category, $search, $sort); + + return $this->json($response, [ + 'success' => true, + 'data' => [ + 'category' => $category, + 'games' => $games, + 'count' => count($games) + ] + ]); + } catch (\Exception $e) { + return $this->json($response, [ + 'success' => false, + 'message' => 'Failed to fetch games by category: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get all games from database + */ + private function getAllGamesWithCategories(string $search = '', string $sort = 'title_asc'): array + { + $pdo = $this->getPdo(); + + $sql = " + SELECT + g.id, + g.title, + g.poster_url, + g.backdrop_url, + g.rating, + g.release_date, + g.platform, + g.developer, + g.genres, + g.playtime_hours, + g.completion_status, + g.last_played, + g.community_score, + g.critic_score, + g.source_name + FROM games g + WHERE 1=1 + "; + + $params = []; + + // Add search filter + if (!empty($search)) { + $sql .= " AND (g.title LIKE :search OR g.developer LIKE :search)"; + $params[':search'] = '%' . $search . '%'; + } + + // Add sorting + switch ($sort) { + case 'title_desc': + $sql .= " ORDER BY g.title DESC"; + break; + case 'year_asc': + $sql .= " ORDER BY g.release_date ASC"; + break; + case 'year_desc': + $sql .= " ORDER BY g.release_date DESC"; + break; + case 'playtime_desc': + $sql .= " ORDER BY g.playtime_hours DESC"; + break; + case 'rating_desc': + $sql .= " ORDER BY g.rating DESC"; + break; + case 'last_played_desc': + $sql .= " ORDER BY g.last_played DESC"; + break; + default: + $sql .= " ORDER BY g.title ASC"; + } + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + + $games = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // Parse genres if stored as JSON + if (!empty($row['genres'])) { + $genres = json_decode($row['genres'], true); + $row['genres'] = is_array($genres) ? $genres : []; + } else { + $row['genres'] = []; + } + + // Set default completion status + if (empty($row['completion_status'])) { + $row['completion_status'] = 'UNPLAYED'; + } + + $games[] = $row; + } + + return $games; + } + + /** + * Group games by completion status + */ + private function groupGamesByCategory(array $games): array + { + $categories = [ + 'BEATEN' => ['name' => 'BEATEN', 'count' => 0, 'games' => []], + 'PLAYING' => ['name' => 'PLAYING', 'count' => 0, 'games' => []], + 'COMPLETED' => ['name' => 'COMPLETED', 'count' => 0, 'games' => []], + 'UNPLAYED' => ['name' => 'UNPLAYED', 'count' => 0, 'games' => []] + ]; + + foreach ($games as $game) { + $status = $game['completion_status'] ?? 'UNPLAYED'; + if (isset($categories[$status])) { + $categories[$status]['games'][] = $game; + $categories[$status]['count']++; + } + } + + return array_values($categories); + } + + /** + * Get games by specific category + */ + private function getGamesByCategory(string $category, string $search = '', string $sort = 'title_asc'): array + { + $pdo = $this->getPdo(); + + $sql = " + SELECT + g.id, + g.title, + g.poster_url, + g.backdrop_url, + g.rating, + g.release_date, + g.platform, + g.developer, + g.genres, + g.playtime_hours, + g.completion_status, + g.last_played, + g.community_score, + g.critic_score, + g.source_name + FROM games g + WHERE g.completion_status = :category + "; + + $params = [':category' => $category]; + + // Add search filter + if (!empty($search)) { + $sql .= " AND (g.title LIKE :search OR g.developer LIKE :search)"; + $params[':search'] = '%' . $search . '%'; + } + + // Add sorting + switch ($sort) { + case 'title_desc': + $sql .= " ORDER BY g.title DESC"; + break; + case 'year_asc': + $sql .= " ORDER BY g.release_date ASC"; + break; + case 'year_desc': + $sql .= " ORDER BY g.release_date DESC"; + break; + case 'playtime_desc': + $sql .= " ORDER BY g.playtime_hours DESC"; + break; + case 'rating_desc': + $sql .= " ORDER BY g.rating DESC"; + break; + case 'last_played_desc': + $sql .= " ORDER BY g.last_played DESC"; + break; + default: + $sql .= " ORDER BY g.title ASC"; + } + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + + $games = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // Parse genres if stored as JSON + if (!empty($row['genres'])) { + $genres = json_decode($row['genres'], true); + $row['genres'] = is_array($genres) ? $genres : []; + } else { + $row['genres'] = []; + } + + $games[] = $row; + } + + return $games; + } + + /** + * Get detailed game information + */ + private function getGameDetails(int $gameId): ?array + { + $pdo = $this->getPdo(); + + $sql = " + SELECT + g.id, + g.title, + g.poster_url, + g.backdrop_url, + g.rating, + g.release_date, + g.platform, + g.developer, + g.publisher, + g.genres, + g.playtime_hours, + g.completion_status, + g.last_played, + g.community_score, + g.critic_score, + g.source_name, + g.description, + g.gameplay, + g.synopsis, + g.age_ratings, + g.version, + g.time_to_beat, + g.controls, + g.pacing, + g.perspective, + g.series + FROM games g + WHERE g.id = :id + "; + + $stmt = $pdo->prepare($sql); + $stmt->execute([':id' => $gameId]); + + $game = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$game) { + return null; + } + + // Parse JSON fields + $jsonFields = ['genres', 'age_ratings']; + foreach ($jsonFields as $field) { + if (!empty($game[$field])) { + $decoded = json_decode($game[$field], true); + $game[$field] = is_array($decoded) ? $decoded : []; + } else { + $game[$field] = []; + } + } + + // Set default completion status + if (empty($game['completion_status'])) { + $game['completion_status'] = 'UNPLAYED'; + } + + return $game; + } +} diff --git a/app/Controllers/Api/MediaController.php b/app/Controllers/Api/MediaController.php index 8b8e711..6d429d8 100644 --- a/app/Controllers/Api/MediaController.php +++ b/app/Controllers/Api/MediaController.php @@ -8,6 +8,8 @@ use App\Models\Game; use App\Models\Movie; use App\Models\TvShow; use App\Models\MusicArtist; +use App\Models\AdultVideo; +use App\Models\Actor; use App\Controllers\Api\ApiController; class MediaController extends ApiController @@ -16,13 +18,19 @@ class MediaController extends ApiController private $movieModel; private $tvShowModel; private $musicArtistModel; + private $adultModel; + private $actorModel; + private $pdo; public function __construct(\PDO $pdo) { + $this->pdo = $pdo; $this->gameModel = new Game($pdo); $this->movieModel = new Movie($pdo); $this->tvShowModel = new TvShow($pdo); + $this->adultModel = new AdultVideo($pdo); $this->musicArtistModel = new MusicArtist($pdo); + $this->actorModel = new Actor($pdo); } // List all games with pagination @@ -32,13 +40,197 @@ class MediaController extends ApiController $pagination = $this->getPaginationParams($request); $filters = $this->getFiltersFromRequest($request); - $games = $this->gameModel->findAll( - $filters, - $pagination['per_page'], - $pagination['offset'] - ); - - $total = $this->gameModel->count($filters); + // Build the main query + $sql = " + SELECT + g.id, + g.title, + g.image_url as poster_url, + g.banner_url as backdrop_url, + g.rating, + g.release_date, + g.platform, + g.developer, + g.genres_json as genres, + g.playtime_minutes, + g.completion_status, + g.last_played_at as last_played, + g.community_score, + g.critic_score, + s.display_name as source_name, + g.created_at + FROM games g + LEFT JOIN sources s ON g.source_id = s.id + WHERE 1=1 + "; + + $params = []; + + // Add filters + if (!empty($filters['genre'])) { + $sql .= " AND JSON_CONTAINS(g.genres_json, :genre)"; + $params[':genre'] = json_encode($filters['genre']); + } + + if (!empty($filters['year'])) { + $sql .= " AND YEAR(g.release_date) = :year"; + $params[':year'] = $filters['year']; + } + + if (!empty($filters['search'])) { + $sql .= " AND (g.title LIKE :search OR g.developer LIKE :search)"; + $params[':search'] = '%' . $filters['search'] . '%'; + } + + if (!empty($filters['platform'])) { + $sql .= " AND g.platform = :platform"; + $params[':platform'] = $filters['platform']; + } + + if (!empty($filters['developer'])) { + $sql .= " AND g.developer = :developer"; + $params[':developer'] = $filters['developer']; + } + + if (!empty($filters['completion_status']) && $filters['completion_status'] !== 'all') { + $sql .= " AND g.completion_status = :completion_status"; + $params[':completion_status'] = $filters['completion_status']; + } + + if (!empty($filters['source_name'])) { + $sql .= " AND s.display_name = :source_name"; + $params[':source_name'] = $filters['source_name']; + } + + if (!empty($filters['rating'])) { + // Parse rating range (e.g., "8-9") + if (strpos($filters['rating'], '-') !== false) { + list($minRating, $maxRating) = explode('-', $filters['rating']); + $sql .= " AND g.rating BETWEEN :min_rating AND :max_rating"; + $params[':min_rating'] = (float)$minRating; + $params[':max_rating'] = (float)$maxRating; + } + } + + // Add sorting + $sortBy = $filters['sort'] ?? 'title'; + $sortOrder = $filters['order'] ?? 'asc'; + + switch ($sortBy) { + case 'title': + $sql .= " ORDER BY g.title " . $sortOrder; + break; + case 'release_date': + $sql .= " ORDER BY g.release_date " . $sortOrder; + break; + case 'rating': + $sql .= " ORDER BY g.rating " . $sortOrder; + break; + case 'playtime_hours': + $sql .= " ORDER BY g.playtime_minutes " . $sortOrder; + break; + case 'completion_status': + $sql .= " ORDER BY g.completion_status " . $sortOrder; + break; + case 'created_at': + $sql .= " ORDER BY g.created_at " . $sortOrder; + break; + default: + $sql .= " ORDER BY g.title ASC"; + } + + // Add pagination + $sql .= " LIMIT :limit OFFSET :offset"; + $params[':limit'] = $pagination['per_page']; + $params[':offset'] = $pagination['offset']; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + + $games = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // Parse genres if stored as JSON + if (!empty($row['genres'])) { + $genres = json_decode($row['genres'], true); + $row['genres'] = is_array($genres) ? $genres : []; + } else { + $row['genres'] = []; + } + + // Convert playtime minutes to hours for frontend + if (!empty($row['playtime_minutes'])) { + $row['playtime_hours'] = round($row['playtime_minutes'] / 60, 1); + } else { + $row['playtime_hours'] = null; + } + unset($row['playtime_minutes']); + + // Set default completion status + if (empty($row['completion_status'])) { + $row['completion_status'] = 'UNPLAYED'; + } + + $games[] = $row; + } + + // Get total count for pagination + $countSql = " + SELECT COUNT(*) as total + FROM games g + LEFT JOIN sources s ON g.source_id = s.id + WHERE 1=1 + "; + + $countParams = []; + + // Add same filters for count + if (!empty($filters['genre'])) { + $countSql .= " AND JSON_CONTAINS(g.genres_json, :genre)"; + $countParams[':genre'] = json_encode($filters['genre']); + } + + if (!empty($filters['year'])) { + $countSql .= " AND YEAR(g.release_date) = :year"; + $countParams[':year'] = $filters['year']; + } + + if (!empty($filters['search'])) { + $countSql .= " AND (g.title LIKE :search OR g.developer LIKE :search)"; + $countParams[':search'] = '%' . $filters['search'] . '%'; + } + + if (!empty($filters['platform'])) { + $countSql .= " AND g.platform = :platform"; + $countParams[':platform'] = $filters['platform']; + } + + if (!empty($filters['developer'])) { + $countSql .= " AND g.developer = :developer"; + $countParams[':developer'] = $filters['developer']; + } + + if (!empty($filters['completion_status']) && $filters['completion_status'] !== 'all') { + $countSql .= " AND g.completion_status = :completion_status"; + $countParams[':completion_status'] = $filters['completion_status']; + } + + if (!empty($filters['source_name'])) { + $countSql .= " AND s.display_name = :source_name"; + $countParams[':source_name'] = $filters['source_name']; + } + + if (!empty($filters['rating'])) { + if (strpos($filters['rating'], '-') !== false) { + list($minRating, $maxRating) = explode('-', $filters['rating']); + $countSql .= " AND g.rating BETWEEN :min_rating AND :max_rating"; + $countParams[':min_rating'] = (float)$minRating; + $countParams[':max_rating'] = (float)$maxRating; + } + } + + $countStmt = $this->pdo->prepare($countSql); + $countStmt->execute($countParams); + $total = $countStmt->fetch(\PDO::FETCH_ASSOC)['total']; return $this->success($response, [ 'items' => $games, @@ -50,27 +242,157 @@ class MediaController extends ApiController ] ]); } catch (\Exception $e) { - return $this->error($response, 'Failed to fetch games', 500); + return $this->error($response, 'Failed to fetch games: ' . $e->getMessage(), 500); } } - // Get single game by ID - public function getGame(Request $request, Response $response, array $args): Response + // Get grouped games (merged across platforms) + public function getGamesGroupedByPlatform(Request $request, Response $response): Response { try { - $id = (int)($args['id'] ?? 0); - if (!$id) { - return $this->error($response, 'Invalid game ID', 400); + $pagination = $this->getPaginationParams($request); + $filters = $this->getFiltersFromRequest($request); + + // Simple grouping by title since game_key might not be populated + $sql = " + SELECT + MIN(id) as id, + title, + COUNT(*) as platform_count, + GROUP_CONCAT(DISTINCT platform ORDER BY platform) as platforms, + MAX(image_url) as image_url, + MAX(last_played_at) as last_played_at, + SUM(playtime_minutes) as total_playtime, + MAX(completion_percentage) as max_completion, + MAX(release_date) as release_date, + MAX(added_at) as added_at + FROM games + WHERE 1=1 + "; + + $params = []; + + if (!empty($filters['search'])) { + $sql .= " AND title LIKE :search"; + $params[':search'] = "%{$filters['search']}%"; + } + + if (!empty($filters['platform'])) { + $sql .= " AND platform = :platform"; + $params[':platform'] = $filters['platform']; + } + + // Add sorting + $sortBy = $filters['sort'] ?? 'title_asc'; + $sortOptions = [ + 'title_asc' => 'title ASC', + 'title_desc' => 'title DESC', + 'year_asc' => 'release_date ASC NULLS LAST', + 'year_desc' => 'release_date DESC NULLS LAST', + 'playtime_asc' => 'total_playtime ASC', + 'playtime_desc' => 'total_playtime DESC', + 'completion_asc' => 'max_completion ASC NULLS LAST', + 'completion_desc' => 'max_completion DESC NULLS LAST', + 'added_asc' => 'added_at ASC NULLS LAST', + 'added_desc' => 'added_at DESC NULLS LAST', + 'last_played_asc' => 'last_played_at ASC NULLS LAST', + 'last_played_desc' => 'last_played_at DESC NULLS LAST', + 'platforms_asc' => 'platform_count ASC', + 'platforms_desc' => 'platform_count DESC' + ]; + + $sortClause = $sortOptions[$sortBy] ?? 'title ASC'; + $sql .= " GROUP BY title ORDER BY $sortClause"; + + // Add pagination + $sql .= " LIMIT :limit OFFSET :offset"; + $params[':limit'] = $pagination['per_page']; + $params[':offset'] = $pagination['offset']; + + $stmt = $this->pdo->prepare($sql); + $stmt->bindValue(':limit', $pagination['per_page'], \PDO::PARAM_INT); + $stmt->bindValue(':offset', $pagination['offset'], \PDO::PARAM_INT); + + if (!empty($filters['search'])) { + $stmt->bindValue(':search', "%{$filters['search']}%"); + } + + if (!empty($filters['platform'])) { + $stmt->bindValue(':platform', $filters['platform']); + } + + $stmt->execute(); + + $games = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + // Enhance games with additional data + foreach ($games as &$game) { + // Parse platforms array + $game['platforms'] = !empty($game['platforms']) ? array_unique(explode(',', $game['platforms'])) : []; + + // Convert playtime minutes to hours + if (!empty($game['total_playtime'])) { + $game['playtime_hours'] = round($game['total_playtime'] / 60, 1); + } else { + $game['playtime_hours'] = null; + } + + // Set default completion status + if (empty($game['max_completion'])) { + $game['max_completion'] = 0; + } + + // Add platform count as a badge + $game['platform_count'] = (int)$game['platform_count']; + + // Get platform details for each platform + $platformDetails = []; + foreach ($game['platforms'] as $platform) { + $platformDetails[] = [ + 'platform' => $platform, + 'display_name' => ucfirst($platform) + ]; + } + $game['platform_details'] = $platformDetails; } - $game = $this->gameModel->find($id); - if (!$game) { - return $this->error($response, 'Game not found', 404); + // Get total count + $countSql = " + SELECT COUNT(DISTINCT title) as total + FROM games + WHERE 1=1 + "; + + $countParams = []; + + if (!empty($filters['search'])) { + $countSql .= " AND title LIKE :search"; + $countParams[':search'] = "%{$filters['search']}%"; } + + if (!empty($filters['platform'])) { + $countSql .= " AND platform = :platform"; + $countParams[':platform'] = $filters['platform']; + } + + $countStmt = $this->pdo->prepare($countSql); + foreach ($countParams as $key => $value) { + $countStmt->bindValue($key, $value); + } + $countStmt->execute(); + $total = $countStmt->fetch(\PDO::FETCH_ASSOC)['total']; - return $this->success($response, $game); + return $this->success($response, [ + 'items' => $games, + 'pagination' => [ + 'total' => $total, + 'per_page' => $pagination['per_page'], + 'current_page' => $pagination['page'], + 'last_page' => ceil($total / $pagination['per_page']) + ] + ]); } catch (\Exception $e) { - return $this->error($response, 'Failed to fetch game', 500); + return $this->error($response, 'Failed to fetch grouped games: ' . $e->getMessage(), 500); } } @@ -99,6 +421,10 @@ class MediaController extends ApiController if ($type === 'all' || $type === 'music') { $results['artists'] = $this->searchArtists($query, $pagination); } + + if ($type === 'all' || $type === 'actors') { + $results['actors'] = $this->searchActors($query, $pagination); + } return $this->success($response, $results); } catch (\Exception $e) { @@ -110,14 +436,14 @@ class MediaController extends ApiController private function searchGames(string $query, array $pagination): array { try { - // First try to use the model's search method if it exists + // Use the game model's search method if available if (method_exists($this->gameModel, 'search')) { $games = $this->gameModel->search($query, $pagination['per_page'], $pagination['offset']); $total = method_exists($this->gameModel, 'countSearchResults') ? $this->gameModel->countSearchResults($query) : count($games); } - // Fallback to basic filtering if search method doesn't exist + // Fallback to basic filtering else { $allGames = $this->gameModel->findAll(); $filtered = array_filter($allGames, function($game) use ($query) { @@ -136,7 +462,6 @@ class MediaController extends ApiController 'per_page' => $pagination['per_page'] ]; } catch (\Exception $e) { - // If anything goes wrong, return empty results return [ 'items' => [], 'total' => 0, @@ -149,32 +474,445 @@ class MediaController extends ApiController private function searchMovies(string $query, array $pagination): array { - // Implement movie search logic - return [ - 'items' => [], - 'total' => 0 - ]; + try { + // Use the movie model's search method if available + if (method_exists($this->movieModel, 'search')) { + $movies = $this->movieModel->search($query, $pagination['per_page'], $pagination['offset']); + $total = method_exists($this->movieModel, 'countSearchResults') + ? $this->movieModel->countSearchResults($query) + : count($movies); + } + // Fallback to basic filtering + else { + $allMovies = $this->movieModel->findAll(); + $filtered = array_filter($allMovies, function($movie) use ($query) { + return stripos($movie['title'] ?? '', $query) !== false; + }); + + // Apply pagination + $movies = array_slice($filtered, $pagination['offset'], $pagination['per_page']); + $total = count($filtered); + } + + return [ + 'items' => $movies, + 'total' => $total, + 'page' => $pagination['page'], + 'per_page' => $pagination['per_page'] + ]; + } catch (\Exception $e) { + return [ + 'items' => [], + 'total' => 0, + 'page' => $pagination['page'], + 'per_page' => $pagination['per_page'], + 'error' => $e->getMessage() + ]; + } } private function searchTvShows(string $query, array $pagination): array { - // Implement TV show search logic - return [ - 'items' => [], - 'total' => 0 - ]; + try { + // Use the TV show model's search method if available + if (method_exists($this->tvShowModel, 'search')) { + $tvShows = $this->tvShowModel->search($query, $pagination['per_page'], $pagination['offset']); + $total = method_exists($this->tvShowModel, 'countSearchResults') + ? $this->tvShowModel->countSearchResults($query) + : count($tvShows); + } + // Fallback to basic filtering + else { + $allTvShows = $this->tvShowModel->findAll(); + $filtered = array_filter($allTvShows, function($tvShow) use ($query) { + return stripos($tvShow['title'] ?? '', $query) !== false; + }); + + // Apply pagination + $tvShows = array_slice($filtered, $pagination['offset'], $pagination['per_page']); + $total = count($filtered); + } + + return [ + 'items' => $tvShows, + 'total' => $total, + 'page' => $pagination['page'], + 'per_page' => $pagination['per_page'] + ]; + } catch (\Exception $e) { + return [ + 'items' => [], + 'total' => 0, + 'page' => $pagination['page'], + 'per_page' => $pagination['per_page'], + 'error' => $e->getMessage() + ]; + } } private function searchArtists(string $query, array $pagination): array { - // Implement artist search logic - return [ - 'items' => [], - 'total' => 0 - ]; + try { + // Use the music artist model's search method if available + if (method_exists($this->musicArtistModel, 'search')) { + $artists = $this->musicArtistModel->search($query, $pagination['per_page'], $pagination['offset']); + $total = method_exists($this->musicArtistModel, 'countSearchResults') + ? $this->musicArtistModel->countSearchResults($query) + : count($artists); + } + // Fallback to basic filtering + else { + $allArtists = $this->musicArtistModel->findAll(); + $filtered = array_filter($allArtists, function($artist) use ($query) { + return stripos($artist['name'] ?? '', $query) !== false; + }); + + // Apply pagination + $artists = array_slice($filtered, $pagination['offset'], $pagination['per_page']); + $total = count($filtered); + } + + return [ + 'items' => $artists, + 'total' => $total, + 'page' => $pagination['page'], + 'per_page' => $pagination['per_page'] + ]; + } catch (\Exception $e) { + return [ + 'items' => [], + 'total' => 0, + 'page' => $pagination['page'], + 'per_page' => $pagination['per_page'], + 'error' => $e->getMessage() + ]; + } } - // Extract filters from request + private function searchActors(string $query, array $pagination): array + { + try { + // Use the actor model's search method if available + if (method_exists($this->actorModel, 'search')) { + $actors = $this->actorModel->search($query, $pagination['per_page'], $pagination['offset']); + $total = method_exists($this->actorModel, 'countSearchResults') + ? $this->actorModel->countSearchResults($query) + : count($actors); + } + // Fallback to basic filtering + else { + $allActors = $this->actorModel->findAll(); + $filtered = array_filter($allActors, function($actor) use ($query) { + return stripos($actor['name'] ?? '', $query) !== false; + }); + + // Apply pagination + $actors = array_slice($filtered, $pagination['offset'], $pagination['per_page']); + $total = count($filtered); + } + + return [ + 'items' => $actors, + 'total' => $total, + 'page' => $pagination['page'], + 'per_page' => $pagination['per_page'] + ]; + } catch (\Exception $e) { + return [ + 'items' => [], + 'total' => 0, + 'page' => $pagination['page'], + 'per_page' => $pagination['per_page'], + 'error' => $e->getMessage() + ]; + } + } + + // List all movies with pagination + public function listMovies(Request $request, Response $response): Response + { + try { + $pagination = $this->getPaginationParams($request); + $filters = $this->getFiltersFromRequest($request); + + // Get movies using the Movie model's pagination method + $movies = Movie::getAllWithPagination( + $this->pdo, + $pagination['page'], + $pagination['per_page'], + $filters['search'] ?? '', + $filters['genres'] ?? [], + $filters['directors'] ?? [], + 'title_asc' // Default sort + ); + + $total = Movie::getTotalCount($this->pdo, $filters['search'] ?? '', $filters['genres'] ?? [], $filters['directors'] ?? []); + + return $this->success($response, [ + 'items' => $movies, + 'pagination' => [ + 'total' => $total, + 'per_page' => $pagination['per_page'], + 'current_page' => $pagination['page'], + 'last_page' => ceil($total / $pagination['per_page']) + ] + ]); + } catch (\Exception $e) { + return $this->error($response, 'Failed to fetch movies', 500); + } + } + + // Get single movie by ID + public function getMovie(Request $request, Response $response, array $args): Response + { + try { + $id = (int)($args['id'] ?? 0); + if (!$id) { + return $this->error($response, 'Invalid movie ID', 400); + } + + $movie = $this->movieModel->find($id); + if (!$movie) { + return $this->error($response, 'Movie not found', 404); + } + + // Get actors for this movie using the model instance + $movieModel = new \App\Models\Movie($this->pdo); + $movieModel->id = $id; + + try { + $actors = $movieModel->actors(); + if (is_array($movie)) { + $movie['actors'] = $actors; + } + } catch (\Exception $e) { + if (is_array($movie)) { + $movie['actors'] = []; + } + } + + return $this->success($response, $movie); + } catch (\Exception $e) { + return $this->error($response, 'Failed to fetch movie', 500); + } + } + + // List all TV shows with pagination + public function listTvShows(Request $request, Response $response): Response + { + try { + $pagination = $this->getPaginationParams($request); + $filters = $this->getFiltersFromRequest($request); + + $tvShows = $this->tvShowModel->findAll( + $filters, + $pagination['per_page'], + $pagination['offset'] + ); + + $total = $this->tvShowModel->count($filters); + + // Get available filter options + $availableGenres = \App\Models\TvShow::getAvailableGenres($this->pdo); + $availableSources = \App\Models\TvShow::getAvailableSources($this->pdo); + + return $this->success($response, [ + 'items' => $tvShows, + 'pagination' => [ + 'total' => $total, + 'per_page' => $pagination['per_page'], + 'current_page' => $pagination['page'], + 'last_page' => ceil($total / $pagination['per_page']) + ], + 'available_filters' => [ + 'genres' => $availableGenres, + 'sources' => $availableSources + ] + ]); + } catch (\Exception $e) { + return $this->error($response, 'Failed to fetch TV shows', 500); + } + } + + // Get single TV show by ID + public function getTvShow(Request $request, Response $response, array $args): Response + { + try { + $id = (int)($args['id'] ?? 0); + if (!$id) { + return $this->error($response, 'Invalid TV show ID', 400); + } + + $tvShow = $this->tvShowModel->find($id); + if (!$tvShow) { + return $this->error($response, 'TV show not found', 404); + } + + // Get additional data using the model instance + $tvShowModel = new \App\Models\TvShow($this->pdo); + $tvShowModel->id = $id; + + try { + $tvShow['actors'] = $tvShowModel->getActors(); + } catch (\Exception $e) { + $tvShow['actors'] = []; + } + + try { + $tvShow['seasons'] = $tvShowModel->getSeasonsWithEpisodes(); + } catch (\Exception $e) { + $tvShow['seasons'] = []; + } + + return $this->success($response, $tvShow); + } catch (\Exception $e) { + return $this->error($response, 'Failed to fetch TV show: ' . $e->getMessage(), 500); + } + } + + + + // List all adult content with pagination + public function listAdult(Request $request, Response $response): Response + { + try { + $pagination = $this->getPaginationParams($request); + $filters = $this->getFiltersFromRequest($request); + + $adultContent = $this->adultModel->findAll( + $filters, + $pagination['per_page'], + $pagination['offset'] + ); + + $total = $this->adultModel->count($filters); + + // Get available filter options + $availableGenres = \App\Models\AdultVideo::getAvailableGenres($this->pdo); + $availableSources = \App\Models\AdultVideo::getAvailableSources($this->pdo); + + return $this->success($response, [ + 'items' => $adultContent, + 'pagination' => [ + 'total' => $total, + 'per_page' => $pagination['per_page'], + 'current_page' => $pagination['page'], + 'last_page' => ceil($total / $pagination['per_page']) + ], + 'available_filters' => [ + 'genres' => $availableGenres, + 'sources' => $availableSources + ] + ]); + } catch (\Exception $e) { + return $this->error($response, 'Failed to fetch adult content', 500); + } + } + + // Get single adult content by ID + public function getAdult(Request $request, Response $response, array $args): Response + { + try { + $id = (int)($args['id'] ?? 0); + if (!$id) { + return $this->error($response, 'Invalid adult content ID', 400); + } + + $adultContent = $this->adultModel->find($id); + if (!$adultContent) { + return $this->error($response, 'Adult content not found', 404); + } + + // Get actors for this adult video using the model instance + $adultVideoModel = new \App\Models\AdultVideo($this->pdo); + $adultVideoModel->id = $id; + + try { + $adultContent['actors'] = $adultVideoModel->actors($id); + } catch (\Exception $e) { + $adultContent['actors'] = []; + } + + return $this->success($response, $adultContent); + } catch (\Exception $e) { + return $this->error($response, 'Failed to fetch adult content', 500); + } + } + + // List all actors with pagination + public function listActors(Request $request, Response $response): Response + { + try { + $pagination = $this->getPaginationParams($request); + $filters = $this->getFiltersFromRequest($request); + + $actors = $this->actorModel->findAll( + $filters, + $pagination['per_page'], + $pagination['offset'] + ); + + $total = $this->actorModel->count($filters); + + return $this->success($response, [ + 'items' => $actors, + 'pagination' => [ + 'total' => $total, + 'per_page' => $pagination['per_page'], + 'current_page' => $pagination['page'], + 'last_page' => ceil($total / $pagination['per_page']) + ] + ]); + } catch (\Exception $e) { + return $this->error($response, 'Failed to fetch actors', 500); + } + } + + // Get single actor by ID + public function getActor(Request $request, Response $response, array $args): Response + { + try { + $id = (int)($args['id'] ?? 0); + if (!$id) { + return $this->error($response, 'Invalid actor ID', 400); + } + + $actor = $this->actorModel->find($id); + if (!$actor) { + return $this->error($response, 'Actor not found', 404); + } + + // Get additional data using model instance + $actorModel = new \App\Models\Actor($this->pdo); + $actorModel->id = $id; + $actorModel->actor = $actor; + + try { + $actor['movies'] = $actorModel->movies(); + } catch (\Exception $e) { + $actor['movies'] = []; + } + + try { + $actor['tvshows'] = $actorModel->tvShows(); + } catch (\Exception $e) { + $actor['tvshows'] = []; + } + + try { + $actor['adult_videos'] = $actorModel->adultVideos(); + } catch (\Exception $e) { + $actor['adult_videos'] = []; + } + + return $this->success($response, $actor); + } catch (\Exception $e) { + return $this->error($response, 'Failed to fetch actor', 500); + } + } + + + // Helper method to get filters from request private function getFiltersFromRequest(Request $request): array { $filters = []; @@ -189,8 +927,344 @@ class MediaController extends ApiController $filters['year'] = (int)$queryParams['year']; } - // Add more filters as needed + if (!empty($queryParams['search'])) { + $filters['search'] = $queryParams['search']; + } + + // Add games-specific filters + if (!empty($queryParams['platform'])) { + $filters['platform'] = $queryParams['platform']; + } + + if (!empty($queryParams['developer'])) { + $filters['developer'] = $queryParams['developer']; + } + + if (!empty($queryParams['completion_status']) && $queryParams['completion_status'] !== 'all') { + $filters['completion_status'] = $queryParams['completion_status']; + } + + if (!empty($queryParams['source_name'])) { + $filters['source_name'] = $queryParams['source_name']; + } + + if (!empty($queryParams['rating'])) { + $filters['rating'] = $queryParams['rating']; + } + + // Add actor-specific filters + if (!empty($queryParams['gender'])) { + $filters['gender'] = $queryParams['gender']; + } + + if (isset($queryParams['adult'])) { + $filters['adult'] = $queryParams['adult'] === 'true' || $queryParams['adult'] === true; + } + + if (!empty($queryParams['sort'])) { + $filters['sort'] = $queryParams['sort']; + } + + if (!empty($queryParams['order'])) { + $filters['order'] = $queryParams['order']; + } return $filters; } + + // Get all games grouped by completion status (for new frontend) + public function getGamesGrouped(Request $request, Response $response): Response + { + try { + $queryParams = $request->getQueryParams(); + $search = $queryParams['search'] ?? ''; + $sort = $queryParams['sort'] ?? 'title_asc'; + $pagination = $this->getPaginationParams($request); + + // Get all games with pagination + $games = $this->getAllGamesWithCategories($search, $sort, $pagination); + + // Group games by completion status + $groupedGames = $this->groupGamesByCategory($games['items']); + + return $this->success($response, [ + 'data' => $groupedGames, + 'pagination' => $games['pagination'] + ]); + } catch (\Exception $e) { + return $this->error($response, 'Failed to fetch games: ' . $e->getMessage(), 500); + } + } + + // Get games by category + public function getGamesByCategory(Request $request, Response $response, array $args): Response + { + try { + $category = strtoupper($args['category'] ?? ''); + $queryParams = $request->getQueryParams(); + $search = $queryParams['search'] ?? ''; + $sort = $queryParams['sort'] ?? 'title_asc'; + $pagination = $this->getPaginationParams($request); + + if (!in_array($category, ['BEATEN', 'PLAYING', 'COMPLETED', 'UNPLAYED'])) { + return $this->error($response, 'Invalid category. Must be one of: BEATEN, PLAYING, COMPLETED, UNPLAYED', 400); + } + + $games = $this->getGamesByCategoryFiltered($category, $search, $sort, $pagination); + + return $this->success($response, [ + 'data' => [ + 'category' => $category, + 'games' => $games['items'], + 'count' => count($games['items']) + ], + 'pagination' => $games['pagination'] + ]); + } catch (\Exception $e) { + return $this->error($response, 'Failed to fetch games by category: ' . $e->getMessage(), 500); + } + } + + // Helper method to get all games with categories + private function getAllGamesWithCategories(string $search = '', string $sort = 'title_asc', array $pagination = []): array + { + $limit = $pagination['per_page'] ?? 1000; // Default high limit for grouped view + $offset = $pagination['offset'] ?? 0; + + // First get total count + $countSql = " + SELECT COUNT(*) as total + FROM games g + WHERE 1=1 + "; + + $countParams = []; + if (!empty($search)) { + $countSql .= " AND (g.title LIKE :search OR g.developer LIKE :search)"; + $countParams[':search'] = '%' . $search . '%'; + } + + $countStmt = $this->pdo->prepare($countSql); + $countStmt->execute($countParams); + $total = $countStmt->fetch(\PDO::FETCH_ASSOC)['total']; + + $sql = " + SELECT + g.id, + g.title, + g.image_url as poster_url, + g.banner_url as backdrop_url, + g.rating, + g.release_date, + g.platform, + g.developer, + g.genres, + g.playtime_hours, + g.completion_status, + g.last_played, + g.community_score, + g.critic_score, + g.source_name + FROM games g + WHERE 1=1 + "; + + $params = []; + + + // Add search filter + if (!empty($search)) { + $sql .= " AND (g.title LIKE :search OR g.developer LIKE :search)"; + $params[':search'] = '%' . $search . '%'; + } + + // Add sorting + switch ($sort) { + case 'title_desc': + $sql .= " ORDER BY g.title DESC"; + break; + case 'year_asc': + $sql .= " ORDER BY g.release_date ASC"; + break; + case 'year_desc': + $sql .= " ORDER BY g.release_date DESC"; + break; + case 'playtime_desc': + $sql .= " ORDER BY g.playtime_hours DESC"; + break; + case 'rating_desc': + $sql .= " ORDER BY g.rating DESC"; + break; + case 'last_played_desc': + $sql .= " ORDER BY g.last_played DESC"; + break; + default: + $sql .= " ORDER BY g.title ASC"; + } + + // Add pagination + $sql .= " LIMIT :limit OFFSET :offset"; + $params[':limit'] = $limit; + $params[':offset'] = $offset; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + + $games = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // Parse genres if stored as JSON + if (!empty($row['genres'])) { + $genres = json_decode($row['genres'], true); + $row['genres'] = is_array($genres) ? $genres : []; + } else { + $row['genres'] = []; + } + + // Set default completion status + if (empty($row['completion_status'])) { + $row['completion_status'] = 'UNPLAYED'; + } + + $games[] = $row; + } + + return [ + 'items' => $games, + 'pagination' => [ + 'total' => $total, + 'per_page' => $limit, + 'current_page' => $pagination['page'] ?? 1, + 'last_page' => ceil($total / $limit) + ] + ]; + } + + // Helper method to group games by category + private function groupGamesByCategory(array $games): array + { + $categories = [ + 'BEATEN' => ['name' => 'BEATEN', 'count' => 0, 'games' => []], + 'PLAYING' => ['name' => 'PLAYING', 'count' => 0, 'games' => []], + 'COMPLETED' => ['name' => 'COMPLETED', 'count' => 0, 'games' => []], + 'UNPLAYED' => ['name' => 'UNPLAYED', 'count' => 0, 'games' => []] + ]; + + foreach ($games as $game) { + $status = $game['completion_status'] ?? 'UNPLAYED'; + if (isset($categories[$status])) { + $categories[$status]['games'][] = $game; + $categories[$status]['count']++; + } + } + + return array_values($categories); + } + + // Helper method to get games by specific category + private function getGamesByCategoryFiltered(string $category, string $search = '', string $sort = 'title_asc', array $pagination = []): array + { + $limit = $pagination['per_page'] ?? 24; + $offset = $pagination['offset'] ?? 0; + + // First get total count + $countSql = " + SELECT COUNT(*) as total + FROM games g + WHERE g.completion_status = :category + "; + + $countParams = [':category' => $category]; + if (!empty($search)) { + $countSql .= " AND (g.title LIKE :search OR g.developer LIKE :search)"; + $countParams[':search'] = '%' . $search . '%'; + } + + $countStmt = $this->pdo->prepare($countSql); + $countStmt->execute($countParams); + $total = $countStmt->fetch(\PDO::FETCH_ASSOC)['total']; + + $sql = " + SELECT + g.id, + g.title, + g.image_url as poster_url, + g.banner_url as backdrop_url, + g.rating, + g.release_date, + g.platform, + g.developer, + g.genres, + g.playtime_hours, + g.completion_status, + g.last_played, + g.community_score, + g.critic_score, + g.source_name + FROM games g + WHERE g.completion_status = :category + "; + + $params = [':category' => $category]; + + // Add search filter + if (!empty($search)) { + $sql .= " AND (g.title LIKE :search OR g.developer LIKE :search)"; + $params[':search'] = '%' . $search . '%'; + } + + // Add sorting + switch ($sort) { + case 'title_desc': + $sql .= " ORDER BY g.title DESC"; + break; + case 'year_asc': + $sql .= " ORDER BY g.release_date ASC"; + break; + case 'year_desc': + $sql .= " ORDER BY g.release_date DESC"; + break; + case 'playtime_desc': + $sql .= " ORDER BY g.playtime_hours DESC"; + break; + case 'rating_desc': + $sql .= " ORDER BY g.rating DESC"; + break; + case 'last_played_desc': + $sql .= " ORDER BY g.last_played DESC"; + break; + default: + $sql .= " ORDER BY g.title ASC"; + } + + // Add pagination + $sql .= " LIMIT :limit OFFSET :offset"; + $params[':limit'] = $limit; + $params[':offset'] = $offset; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + + $games = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // Parse genres if stored as JSON + if (!empty($row['genres'])) { + $genres = json_decode($row['genres'], true); + $row['genres'] = is_array($genres) ? $genres : []; + } else { + $row['genres'] = []; + } + + $games[] = $row; + } + + return [ + 'items' => $games, + 'pagination' => [ + 'total' => $total, + 'per_page' => $limit, + 'current_page' => $pagination['page'] ?? 1, + 'last_page' => ceil($total / $limit) + ] + ]; + } } diff --git a/app/Models/Actor.php b/app/Models/Actor.php index 25944d1..fcac5bc 100644 --- a/app/Models/Actor.php +++ b/app/Models/Actor.php @@ -15,6 +15,139 @@ class Actor extends Model 'metadata' => 'array' ]; + /** + * Get all actors with filtering and pagination + */ + public function findAll(array $filters = [], int $limit = null, int $offset = 0): array + { + $sql = " + SELECT a.*, + COUNT(DISTINCT am.movie_id) as movie_count, + COUNT(DISTINCT te.tv_show_id) as tv_show_count, + COUNT(DISTINCT aav.adult_video_id) as adult_video_count + FROM actors a + LEFT JOIN actor_movie am ON a.id = am.actor_id + LEFT JOIN actor_tv_episode ate ON a.id = ate.actor_id + LEFT JOIN tv_episodes te ON ate.tv_episode_id = te.id + LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id + "; + $params = []; + $whereClauses = []; + + // Search filter + if (!empty($filters['search'])) { + $whereClauses[] = "a.name LIKE :search"; + $params['search'] = "%{$filters['search']}%"; + } + + // Gender filter + if (!empty($filters['gender'])) { + $whereClauses[] = "JSON_UNQUOTE(JSON_EXTRACT(a.metadata, '$.gender')) = :gender"; + $params['gender'] = strtoupper($filters['gender']); + } + + // Adult filter + if (isset($filters['adult'])) { + if ($filters['adult']) { + $whereClauses[] = "aav.adult_video_id IS NOT NULL"; + } else { + $whereClauses[] = "aav.adult_video_id IS NULL"; + } + } + + // Combine WHERE clauses + if (!empty($whereClauses)) { + $sql .= " WHERE " . implode(' AND ', $whereClauses); + } + + $sql .= " GROUP BY a.id"; + + // Add sorting + $sortBy = $filters['sort'] ?? 'name'; + $sortOrder = $filters['order'] ?? 'asc'; + + switch ($sortBy) { + case 'name': + $sql .= " ORDER BY a.name {$sortOrder}"; + break; + case 'age': + $sql .= " ORDER BY JSON_EXTRACT(a.metadata, '$.age') {$sortOrder}"; + break; + case 'media_count': + $sql .= " ORDER BY (COUNT(DISTINCT am.movie_id) + COUNT(DISTINCT te.tv_show_id) + COUNT(DISTINCT aav.adult_video_id)) {$sortOrder}"; + break; + case 'movie_count': + $sql .= " ORDER BY COUNT(DISTINCT am.movie_id) {$sortOrder}"; + break; + case 'tv_show_count': + $sql .= " ORDER BY COUNT(DISTINCT te.tv_show_id) {$sortOrder}"; + break; + case 'adult_count': + $sql .= " ORDER BY COUNT(DISTINCT aav.adult_video_id) {$sortOrder}"; + break; + default: + $sql .= " ORDER BY a.name ASC"; + } + + if ($limit) { + $sql .= " LIMIT :limit OFFSET :offset"; + $params['limit'] = $limit; + $params['offset'] = $offset; + } + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + } + + /** + * Count actors with filters + */ + public function count(array $filters = []): int + { + $sql = " + SELECT COUNT(DISTINCT a.id) as total + FROM actors a + LEFT JOIN actor_movie am ON a.id = am.actor_id + LEFT JOIN actor_tv_episode ate ON a.id = ate.actor_id + LEFT JOIN tv_episodes te ON ate.tv_episode_id = te.id + LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id + "; + $params = []; + $whereClauses = []; + + // Search filter + if (!empty($filters['search'])) { + $whereClauses[] = "a.name LIKE :search"; + $params['search'] = "%{$filters['search']}%"; + } + + // Gender filter + if (!empty($filters['gender'])) { + $whereClauses[] = "JSON_UNQUOTE(JSON_EXTRACT(a.metadata, '$.gender')) = :gender"; + $params['gender'] = strtoupper($filters['gender']); + } + + // Adult filter + if (isset($filters['adult'])) { + if ($filters['adult']) { + $whereClauses[] = "aav.adult_video_id IS NOT NULL"; + } else { + $whereClauses[] = "aav.adult_video_id IS NULL"; + } + } + + // Combine WHERE clauses + if (!empty($whereClauses)) { + $sql .= " WHERE " . implode(' AND ', $whereClauses); + } + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + $result = $stmt->fetch(\PDO::FETCH_ASSOC); + return (int)$result['total']; + } + /** * Get all movies this actor is associated with */ @@ -29,24 +162,83 @@ class Actor extends Model ORDER BY m.release_date DESC, m.title ASC "); $stmt->execute(['actor_id' => $this->id]); - return $stmt->fetchAll(\PDO::FETCH_ASSOC); + $movies = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + // Process poster URLs for each movie + foreach ($movies as &$movie) { + // Use poster_url field directly if available + if (!empty($movie['poster_url'])) { + // Keep the existing poster_url as-is since it's already in the correct format + } + + // Also process metadata for additional fields + if (!empty($movie['metadata'])) { + $metadata = json_decode($movie['metadata'], true); + + // Extract poster aspect ratio from metadata if available + if (!empty($metadata['poster_aspect_ratio'])) { + $movie['poster_aspect_ratio'] = $metadata['poster_aspect_ratio']; + } + + // If no poster_url in main field, try to get it from metadata + if (empty($movie['poster_url'])) { + if (!empty($metadata['local_cover_path'])) { + $movie['poster_url'] = $metadata['local_cover_path']; + } elseif (!empty($metadata['cover_url'])) { + $movie['poster_url'] = $metadata['cover_url']; + } + } + } + } + + return $movies; } /** - * Get all TV shows this actor is associated with + * Get all TV shows this actor is associated with (via episodes) */ public function tvShows() { $stmt = $this->pdo->prepare(" - SELECT ts.*, s.display_name as source_name + SELECT DISTINCT ts.*, s.display_name as source_name FROM tv_shows ts JOIN sources s ON ts.source_id = s.id - JOIN actor_tv_show ats ON ts.id = ats.tv_show_id - WHERE ats.actor_id = :actor_id + JOIN tv_episodes te ON ts.id = te.tv_show_id + JOIN actor_tv_episode ate ON te.id = ate.tv_episode_id + WHERE ate.actor_id = :actor_id ORDER BY ts.first_air_date DESC, ts.title ASC "); $stmt->execute(['actor_id' => $this->id]); - return $stmt->fetchAll(\PDO::FETCH_ASSOC); + $tvShows = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + // Process poster URLs for each TV show + foreach ($tvShows as &$tvShow) { + // Use poster_url field directly if available + if (!empty($tvShow['poster_url'])) { + // Keep the existing poster_url as-is since it's already in the correct format + } + + // Also process metadata for additional fields + if (!empty($tvShow['metadata'])) { + $metadata = json_decode($tvShow['metadata'], true); + + // Extract poster aspect ratio from metadata if available + if (!empty($metadata['poster_aspect_ratio'])) { + $tvShow['poster_aspect_ratio'] = $metadata['poster_aspect_ratio']; + } + + // If no poster_url in main field, try to get it from metadata + if (empty($tvShow['poster_url'])) { + if (!empty($metadata['local_cover_path'])) { + $tvShow['poster_url'] = $metadata['local_cover_path']; + } elseif (!empty($metadata['cover_url'])) { + $tvShow['poster_url'] = $metadata['cover_url']; + } + } + } + } + + return $tvShows; } /** @@ -63,7 +255,41 @@ class Actor extends Model ORDER BY av.release_date DESC, av.title ASC "); $stmt->execute(['actor_id' => $this->id]); - return $stmt->fetchAll(\PDO::FETCH_ASSOC); + $adultVideos = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + // Process poster URLs for each adult video + foreach ($adultVideos as &$adultVideo) { + // Use poster_url field directly if available + if (!empty($adultVideo['poster_url'])) { + // Keep the existing poster_url as-is since it's already in the correct format + } + + // Also process metadata for additional fields + if (!empty($adultVideo['metadata'])) { + $metadata = json_decode($adultVideo['metadata'], true); + + // Extract poster aspect ratio from metadata if available + if (!empty($metadata['poster_aspect_ratio'])) { + $adultVideo['poster_aspect_ratio'] = $metadata['poster_aspect_ratio']; + } + + // If no poster_url in main field, try to get it from metadata + if (empty($adultVideo['poster_url'])) { + if (!empty($metadata['local_cover_path'])) { + $adultVideo['poster_url'] = $metadata['local_cover_path']; + } elseif (!empty($metadata['cover_url'])) { + $adultVideo['poster_url'] = $metadata['cover_url']; + } + } + + // Add screenshot URL if available + if (!empty($metadata['screenshot_url'])) { + $adultVideo['screenshot_url'] = $metadata['screenshot_url']; + } + } + } + + return $adultVideos; } /** @@ -74,12 +300,13 @@ class Actor extends Model $stmt = $this->pdo->prepare(" SELECT COUNT(DISTINCT am.movie_id) as movie_count, - COUNT(DISTINCT ats.tv_show_id) as tv_show_count, + COUNT(DISTINCT te.tv_show_id) as tv_show_count, COUNT(DISTINCT aav.adult_video_id) as adult_video_count, - COUNT(DISTINCT am.movie_id) + COUNT(DISTINCT ats.tv_show_id) + COUNT(DISTINCT aav.adult_video_id) as total_media_count + COUNT(DISTINCT am.movie_id) + COUNT(DISTINCT te.tv_show_id) + COUNT(DISTINCT aav.adult_video_id) as total_media_count FROM actors a LEFT JOIN actor_movie am ON a.id = am.actor_id - LEFT JOIN actor_tv_show ats ON a.id = ats.actor_id + LEFT JOIN actor_tv_episode ate ON a.id = ate.actor_id + LEFT JOIN tv_episodes te ON ate.tv_episode_id = te.id LEFT JOIN actor_adult_video aav ON a.id = aav.actor_id WHERE a.id = :actor_id "); @@ -196,8 +423,8 @@ class Actor extends Model // Build ORDER BY clause $orderBy = match ($sort) { 'name_desc' => 'name DESC', - 'media_desc' => '(SELECT COUNT(*) FROM actor_movie WHERE actor_id = actors.id) + (SELECT COUNT(*) FROM actor_tv_show WHERE actor_id = actors.id) + (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = actors.id) DESC', - 'media_asc' => '(SELECT COUNT(*) FROM actor_movie WHERE actor_id = actors.id) + (SELECT COUNT(*) FROM actor_tv_show WHERE actor_id = actors.id) + (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = actors.id) ASC', + 'media_desc' => '(SELECT COUNT(*) FROM actor_movie WHERE actor_id = actors.id) + (SELECT COUNT(DISTINCT te.tv_show_id) FROM actor_tv_episode ate JOIN tv_episodes te ON ate.tv_episode_id = te.id WHERE ate.actor_id = actors.id) + (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = actors.id) DESC', + 'media_asc' => '(SELECT COUNT(*) FROM actor_movie WHERE actor_id = actors.id) + (SELECT COUNT(DISTINCT te.tv_show_id) FROM actor_tv_episode ate JOIN tv_episodes te ON ate.tv_episode_id = te.id WHERE ate.actor_id = actors.id) + (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = actors.id) ASC', default => 'name ASC' }; @@ -206,7 +433,7 @@ class Actor extends Model SELECT a.*, (SELECT COUNT(*) FROM actor_movie WHERE actor_id = a.id) as movie_count, - (SELECT COUNT(*) FROM actor_tv_show WHERE actor_id = a.id) as tv_show_count, + (SELECT COUNT(DISTINCT te.tv_show_id) FROM actor_tv_episode ate JOIN tv_episodes te ON ate.tv_episode_id = te.id WHERE ate.actor_id = a.id) as tv_show_count, (SELECT COUNT(*) FROM actor_adult_video WHERE actor_id = a.id) as adult_video_count FROM actors a {$whereClause} @@ -279,10 +506,11 @@ class Actor extends Model private function getActorTvShows(int $actorId): array { $stmt = $this->pdo->prepare(" - SELECT ts.id, ts.title + SELECT DISTINCT ts.id, ts.title FROM tv_shows ts - JOIN actor_tv_show ats ON ts.id = ats.tv_show_id - WHERE ats.actor_id = ? + JOIN tv_episodes te ON ts.id = te.tv_show_id + JOIN actor_tv_episode ate ON te.id = ate.tv_episode_id + WHERE ate.actor_id = ? ORDER BY ts.title "); $stmt->execute([$actorId]); diff --git a/app/Models/AdultVideo.php b/app/Models/AdultVideo.php index 1b12aec..e457993 100644 --- a/app/Models/AdultVideo.php +++ b/app/Models/AdultVideo.php @@ -230,7 +230,7 @@ class AdultVideo extends Model */ public function updateCastField(): bool { - $actors = $this->actors(); + $actors = $this->actors($this->id); $actorNames = array_column($actors, 'name'); $castString = implode(', ', $actorNames); @@ -239,6 +239,194 @@ class AdultVideo extends Model ]); } + /** + * Find single adult video by ID with metadata processing + */ + public function find(int $id): ?array + { + $stmt = $this->pdo->prepare(" + SELECT av.*, s.display_name as source_name + FROM adult_videos av + JOIN sources s ON av.source_id = s.id + WHERE av.id = :id + "); + $stmt->execute(['id' => $id]); + $video = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$video) { + return null; + } + + // Process metadata to extract additional fields + if (!empty($video['metadata'])) { + $metadata = json_decode($video['metadata'], true); + + // Extract poster aspect ratio from metadata if available + if (!empty($metadata['poster_aspect_ratio'])) { + $video['poster_aspect_ratio'] = $metadata['poster_aspect_ratio']; + } + + // Use local cover path if available, otherwise fall back to original URL + if (!empty($metadata['local_cover_path'])) { + $video['poster_url'] = $metadata['local_cover_path']; + } elseif (!empty($metadata['cover_url'])) { + $video['poster_url'] = $metadata['cover_url']; + } + + // Add screenshot URL if available + if (!empty($metadata['screenshot_url'])) { + $video['screenshot_url'] = $metadata['screenshot_url']; + } + + // Add actors data if available + if (!empty($metadata['actors'])) { + $video['actors'] = $metadata['actors']; + } + } + + return $video; + } + + /** + * Get all adult videos with filtering and pagination + */ + public function findAll(array $filters = [], int $limit = null, int $offset = 0): array + { + $sql = " + SELECT av.*, s.display_name as source_name + FROM adult_videos av + JOIN sources s ON av.source_id = s.id + "; + $params = []; + $whereClauses = []; + + // Search filter + if (!empty($filters['search'])) { + $whereClauses[] = "(av.title LIKE :search OR av.overview LIKE :search)"; + $params['search'] = "%{$filters['search']}%"; + } + + // Genre filter + if (!empty($filters['genre'])) { + $whereClauses[] = "av.genre = :genre"; + $params['genre'] = $filters['genre']; + } + + // Year filter + if (!empty($filters['year'])) { + $whereClauses[] = "YEAR(av.release_date) = :year"; + $params['year'] = $filters['year']; + } + + // Source filter + if (!empty($filters['sources'])) { + $sources = is_array($filters['sources']) ? $filters['sources'] : [$filters['sources']]; + $placeholders = []; + foreach ($sources as $index => $source) { + $placeholders[] = ":source_{$index}"; + $params["source_{$index}"] = $source; + } + $whereClauses[] = "s.display_name IN (" . implode(',', $placeholders) . ")"; + } + + // Combine WHERE clauses + if (!empty($whereClauses)) { + $sql .= " WHERE " . implode(' AND ', $whereClauses); + } + + // Add ordering and pagination + $sql .= " ORDER BY av.release_date DESC, av.title ASC"; + + if ($limit) { + $sql .= " LIMIT :limit OFFSET :offset"; + $params['limit'] = $limit; + $params['offset'] = $offset; + } + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + $results = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + // Process metadata to extract additional fields + foreach ($results as &$video) { + if (!empty($video['metadata'])) { + $metadata = json_decode($video['metadata'], true); + + // Extract poster aspect ratio from metadata if available + if (!empty($metadata['poster_aspect_ratio'])) { + $video['poster_aspect_ratio'] = $metadata['poster_aspect_ratio']; + } + + // Use local cover path if available, otherwise fall back to original URL + if (!empty($metadata['local_cover_path'])) { + $video['poster_url'] = $metadata['local_cover_path']; + } elseif (!empty($metadata['cover_url'])) { + $video['poster_url'] = $metadata['cover_url']; + } + + // Add actors data if available + if (!empty($metadata['actors'])) { + $video['actors'] = $metadata['actors']; + } + } + } + + return $results; + } + + /** + * Count adult videos with filters + */ + public function count(array $filters = []): int + { + $sql = " + SELECT COUNT(*) as total + FROM adult_videos av + JOIN sources s ON av.source_id = s.id + "; + $params = []; + $whereClauses = []; + + // Search filter + if (!empty($filters['search'])) { + $whereClauses[] = "(av.title LIKE :search OR av.overview LIKE :search)"; + $params['search'] = "%{$filters['search']}%"; + } + + // Genre filter + if (!empty($filters['genre'])) { + $whereClauses[] = "av.genre = :genre"; + $params['genre'] = $filters['genre']; + } + + // Year filter + if (!empty($filters['year'])) { + $whereClauses[] = "YEAR(av.release_date) = :year"; + $params['year'] = $filters['year']; + } + + // Source filter + if (!empty($filters['sources'])) { + $sources = is_array($filters['sources']) ? $filters['sources'] : [$filters['sources']]; + $placeholders = []; + foreach ($sources as $index => $source) { + $placeholders[] = ":source_{$index}"; + $params["source_{$index}"] = $source; + } + $whereClauses[] = "s.display_name IN (" . implode(',', $placeholders) . ")"; + } + + // Combine WHERE clauses + if (!empty($whereClauses)) { + $sql .= " WHERE " . implode(' AND ', $whereClauses); + } + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + $result = $stmt->fetch(\PDO::FETCH_ASSOC); + return (int)$result['total']; + } + /** * Get available genres for filtering */ diff --git a/app/Models/TvShow.php b/app/Models/TvShow.php index 888fb18..209f42b 100644 --- a/app/Models/TvShow.php +++ b/app/Models/TvShow.php @@ -356,62 +356,93 @@ class TvShow extends Model public function getSeasonsWithEpisodes(): array { - // Get all episodes for this TV show, grouped by season - $stmt = $this->pdo->prepare(" - SELECT season_number, - COUNT(*) as episode_count, - SUM(CASE WHEN watched = 1 THEN 1 ELSE 0 END) as watched_episodes - FROM tv_episodes - WHERE tv_show_id = :tv_show_id - GROUP BY season_number - ORDER BY season_number ASC - "); - $stmt->execute(['tv_show_id' => $this->id]); - $seasonStats = $stmt->fetchAll(\PDO::FETCH_ASSOC); - - $seasons = []; - - // For each season, get the episodes and create a season object - foreach ($seasonStats as $stat) { - $seasonNumber = $stat['season_number']; - - // Get episodes for this season + try { + // Get all episodes for this TV show, grouped by season $stmt = $this->pdo->prepare(" - SELECT e.* - FROM tv_episodes e - WHERE e.tv_show_id = :tv_show_id AND e.season_number = :season_number - ORDER BY e.episode_number ASC + SELECT season_number, + COUNT(*) as episode_count, + SUM(CASE WHEN watched = 1 THEN 1 ELSE 0 END) as watched_episodes + FROM tv_episodes + WHERE tv_show_id = :tv_show_id + GROUP BY season_number + ORDER BY season_number ASC "); - $stmt->execute([ - 'tv_show_id' => $this->id, - 'season_number' => $seasonNumber - ]); - $episodes = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $stmt->execute(['tv_show_id' => $this->id]); + $seasonStats = $stmt->fetchAll(\PDO::FETCH_ASSOC); - // Add actors for each episode - foreach ($episodes as &$episode) { - $episodeStmt = $this->pdo->prepare(" - SELECT a.* - FROM actors a - JOIN actor_tv_episode ate ON a.id = ate.actor_id - WHERE ate.tv_episode_id = :tv_episode_id - ORDER BY a.name ASC + $seasons = []; + + // For each season, get the episodes and create a season object + foreach ($seasonStats as $stat) { + $seasonNumber = $stat['season_number']; + + // Get episodes for this season + $stmt = $this->pdo->prepare(" + SELECT e.* + FROM tv_episodes e + WHERE e.tv_show_id = :tv_show_id AND e.season_number = :season_number + ORDER BY e.episode_number ASC "); - $episodeStmt->execute(['tv_episode_id' => $episode['id']]); - $episode['actors'] = $episodeStmt->fetchAll(\PDO::FETCH_ASSOC); + $stmt->execute([ + 'tv_show_id' => $this->id, + 'season_number' => $seasonNumber + ]); + $episodes = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + // Add actors for each episode + foreach ($episodes as &$episode) { + try { + $episodeStmt = $this->pdo->prepare(" + SELECT a.* + FROM actors a + JOIN actor_tv_episode ate ON a.id = ate.actor_id + WHERE ate.tv_episode_id = :tv_episode_id + ORDER BY a.name ASC + "); + $episodeStmt->execute(['tv_episode_id' => $episode['id']]); + $episode['actors'] = $episodeStmt->fetchAll(\PDO::FETCH_ASSOC); + } catch (\Exception $e) { + $episode['actors'] = []; + } + } + + // Create a season object (simulating the old seasons table structure) + $seasons[] = [ + 'id' => null, // No seasons table, so no ID + 'season_number' => $seasonNumber, + 'episode_count' => (int)$stat['episode_count'], + 'watched_episodes' => (int)$stat['watched_episodes'], + 'episodes' => $episodes + ]; } - // Create a season object (simulating the old seasons table structure) - $seasons[] = [ - 'id' => null, // No seasons table, so no ID - 'season_number' => $seasonNumber, - 'episode_count' => (int)$stat['episode_count'], - 'watched_episodes' => (int)$stat['watched_episodes'], - 'episodes' => $episodes - ]; + return $seasons; + } catch (\Exception $e) { + // Return empty array if tables don't exist or query fails + return []; } + } - return $seasons; + /** + * Get all actors for this TV show (from all episodes) + */ + public function getActors(): array + { + try { + $stmt = $this->pdo->prepare(" + SELECT DISTINCT a.* + FROM actors a + JOIN actor_tv_episode ate ON a.id = ate.actor_id + JOIN tv_episodes te ON ate.tv_episode_id = te.id + WHERE te.tv_show_id = :tv_show_id + ORDER BY a.name ASC + "); + $stmt->execute(['tv_show_id' => $this->id]); + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + } catch (\Exception $e) { + // Return empty array if tables don't exist or query fails + return []; + } } /** @@ -471,6 +502,147 @@ public static function getAvailableGenres(\PDO $pdo): array return $stmt->fetchAll(\PDO::FETCH_COLUMN); } +/** + * Get all available sources from TV shows + */ +public static function getAvailableSources(\PDO $pdo): array +{ + $stmt = $pdo->query(" + SELECT DISTINCT s.display_name + FROM tv_shows t + JOIN sources s ON t.source_id = s.id + WHERE t.source_id IS NOT NULL + ORDER BY s.display_name + "); + return $stmt->fetchAll(\PDO::FETCH_COLUMN); +} + +/** + * Get all TV shows with filtering and pagination + */ +public function findAll(array $filters = [], int $limit = null, int $offset = 0): array +{ + $sql = " + SELECT t.*, s.display_name as source_name + FROM tv_shows t + LEFT JOIN sources s ON t.source_id = s.id + "; + $params = []; + $whereClauses = []; + + // Search filter + if (!empty($filters['search'])) { + $whereClauses[] = "(t.title LIKE :search OR t.overview LIKE :search)"; + $params['search'] = "%{$filters['search']}%"; + } + + // Genre filter + if (!empty($filters['genre'])) { + $whereClauses[] = "t.genre = :genre"; + $params['genre'] = $filters['genre']; + } + + // Year filter + if (!empty($filters['year'])) { + $whereClauses[] = "YEAR(t.first_air_date) = :year"; + $params['year'] = $filters['year']; + } + + // Source filter + if (!empty($filters['sources'])) { + $sources = is_array($filters['sources']) ? $filters['sources'] : [$filters['sources']]; + $placeholders = []; + foreach ($sources as $index => $source) { + $placeholders[] = ":source_{$index}"; + $params["source_{$index}"] = $source; + } + $whereClauses[] = "s.display_name IN (" . implode(',', $placeholders) . ")"; + } + + // Combine WHERE clauses + if (!empty($whereClauses)) { + $sql .= " WHERE " . implode(' AND ', $whereClauses); + } + + // Add ordering and pagination + $sql .= " ORDER BY t.first_air_date DESC, t.title ASC"; + + if ($limit) { + $sql .= " LIMIT :limit OFFSET :offset"; + $params['limit'] = $limit; + $params['offset'] = $offset; + } + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + $results = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + // Process metadata to extract additional fields + foreach ($results as &$tvShow) { + if (!empty($tvShow['metadata'])) { + $metadata = json_decode($tvShow['metadata'], true); + + // Extract additional fields from metadata if available + // This can be expanded based on your metadata structure + } + } + + return $results; +} + +/** + * Count TV shows with filters + */ +public function count(array $filters = []): int +{ + $sql = " + SELECT COUNT(*) as total + FROM tv_shows t + LEFT JOIN sources s ON t.source_id = s.id + "; + $params = []; + $whereClauses = []; + + // Search filter + if (!empty($filters['search'])) { + $whereClauses[] = "(t.title LIKE :search OR t.overview LIKE :search)"; + $params['search'] = "%{$filters['search']}%"; + } + + // Genre filter + if (!empty($filters['genre'])) { + $whereClauses[] = "t.genre = :genre"; + $params['genre'] = $filters['genre']; + } + + // Year filter + if (!empty($filters['year'])) { + $whereClauses[] = "YEAR(t.first_air_date) = :year"; + $params['year'] = $filters['year']; + } + + // Source filter + if (!empty($filters['sources'])) { + $sources = is_array($filters['sources']) ? $filters['sources'] : [$filters['sources']]; + $placeholders = []; + foreach ($sources as $index => $source) { + $placeholders[] = ":source_{$index}"; + $params["source_{$index}"] = $source; + } + $whereClauses[] = "s.display_name IN (" . implode(',', $placeholders) . ")"; + } + + // Combine WHERE clauses + if (!empty($whereClauses)) { + $sql .= " WHERE " . implode(' AND ', $whereClauses); + } + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + $result = $stmt->fetch(\PDO::FETCH_ASSOC); + return (int)$result['total']; +} + /** * Get all available years from TV shows' first_air_date */ diff --git a/check_adult_structure.php b/check_adult_structure.php new file mode 100644 index 0000000..76fb077 --- /dev/null +++ b/check_adult_structure.php @@ -0,0 +1,40 @@ +load(); + +// Load database configuration +$dbConfig = require __DIR__ . '/config/database.php'; +\App\Database\Database::setConfig($dbConfig); + +// Initialize database +try { + $pdo = \App\Database\Database::getInstance(); + echo "✅ Database connection successful\n"; +} catch (Exception $e) { + die('❌ Database connection failed: ' . $e->getMessage()); +} + +// Check adult videos structure +try { + $stmt = $pdo->query("SELECT id, title, poster_url, metadata FROM adult_videos LIMIT 1"); + $adultVideo = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($adultVideo) { + echo "🔞 Sample adult video structure:\n"; + echo " - ID: {$adultVideo['id']}\n"; + echo " - Title: {$adultVideo['title']}\n"; + echo " - poster_url: " . ($adultVideo['poster_url'] ?? 'NULL') . "\n"; + echo " - metadata: " . substr($adultVideo['metadata'] ?? 'NULL', 0, 200) . "...\n"; + } else { + echo "🔞 No adult videos found\n"; + } + +} catch (Exception $e) { + echo "❌ Error: " . $e->getMessage() . "\n"; +} + +echo "\n✨ Check completed!\n"; diff --git a/check_adult_videos.php b/check_adult_videos.php new file mode 100644 index 0000000..e69de29 diff --git a/check_games_columns.php b/check_games_columns.php new file mode 100644 index 0000000..5983d39 --- /dev/null +++ b/check_games_columns.php @@ -0,0 +1,23 @@ +get('pdo'); + +// Get column names from games table +$stmt = $pdo->query('DESCRIBE games'); +$columns = $stmt->fetchAll(PDO::FETCH_ASSOC); + +echo 'Games table columns:' . PHP_EOL; +foreach ($columns as $column) { + echo '- ' . $column['Field'] . ' (' . $column['Type'] . ')' . PHP_EOL; +} + +// Also check a sample row to see the actual data +echo PHP_EOL . 'Sample game data:' . PHP_EOL; +$stmt = $pdo->query('SELECT * FROM games LIMIT 1'); +$row = $stmt->fetch(PDO::FETCH_ASSOC); +if ($row) { + foreach ($row as $key => $value) { + echo '- ' . $key . ': ' . (is_null($value) ? 'NULL' : substr($value, 0, 50)) . PHP_EOL; + } +} diff --git a/check_gender_data.php b/check_gender_data.php new file mode 100644 index 0000000..bdeb83f --- /dev/null +++ b/check_gender_data.php @@ -0,0 +1,80 @@ +load(); + +// Load database configuration +$dbConfig = require __DIR__ . '/config/database.php'; +\App\Database\Database::setConfig($dbConfig); + +// Initialize database +try { + $pdo = \App\Database\Database::getInstance(); + echo "✅ Database connection successful\n"; +} catch (Exception $e) { + die('❌ Database connection failed: ' . $e->getMessage()); +} + +// Check gender data in actors table +try { + echo "\n🔍 Checking Gender Data:\n"; + + // Check if metadata column exists and has gender data + $stmt = $pdo->query(" + SELECT name, metadata + FROM actors + WHERE metadata IS NOT NULL + AND metadata != '' + AND JSON_EXTRACT(metadata, '$.gender') IS NOT NULL + LIMIT 10 + "); + $actors = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo "Found " . count($actors) . " actors with gender metadata:\n"; + + foreach ($actors as $actor) { + $metadata = json_decode($actor['metadata'], true); + $gender = $metadata['gender'] ?? 'null'; + echo " - {$actor['name']}: {$gender}\n"; + } + + // Count actors by gender + echo "\n📊 Gender Statistics:\n"; + + $genders = ['male', 'female', 'non-binary']; + foreach ($genders as $gender) { + $stmt = $pdo->prepare(" + SELECT COUNT(*) as count + FROM actors + WHERE JSON_EXTRACT(metadata, '$.gender') = :gender + "); + $stmt->execute(['gender' => $gender]); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + echo " - " . ucfirst($gender) . ": " . $result['count'] . "\n"; + } + + // Check actors without gender + $stmt = $pdo->query(" + SELECT COUNT(*) as count + FROM actors + WHERE metadata IS NULL + OR metadata = '' + OR JSON_EXTRACT(metadata, '$.gender') IS NULL + "); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + echo " - No gender data: " . $result['count'] . "\n"; + + // Total actors + $stmt = $pdo->query("SELECT COUNT(*) as count FROM actors"); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + echo " - Total actors: " . $result['count'] . "\n"; + +} catch (Exception $e) { + echo "❌ Error checking gender data: " . $e->getMessage() . "\n"; + echo "Stack trace: " . $e->getTraceAsString() . "\n"; +} + +echo "\n✨ Check completed!\n"; diff --git a/check_poster_structure.php b/check_poster_structure.php new file mode 100644 index 0000000..d3c14ad --- /dev/null +++ b/check_poster_structure.php @@ -0,0 +1,68 @@ +load(); + +// Load database configuration +$dbConfig = require __DIR__ . '/config/database.php'; +\App\Database\Database::setConfig($dbConfig); + +// Initialize database +try { + $pdo = \App\Database\Database::getInstance(); + echo "✅ Database connection successful\n"; +} catch (Exception $e) { + die('❌ Database connection failed: ' . $e->getMessage()); +} + +// Check sample movie and TV show structure +try { + // Check movies + $stmt = $pdo->query("SELECT id, title, poster_url, metadata FROM movies LIMIT 1"); + $movie = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($movie) { + echo "🎬 Sample movie structure:\n"; + echo " - ID: {$movie['id']}\n"; + echo " - Title: {$movie['title']}\n"; + echo " - poster_url: " . ($movie['poster_url'] ?? 'NULL') . "\n"; + echo " - metadata: " . substr($movie['metadata'] ?? 'NULL', 0, 200) . "...\n"; + } + + // Check TV shows + $stmt = $pdo->query("SELECT id, title, poster_url, metadata FROM tv_shows LIMIT 1"); + $tvshow = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($tvshow) { + echo "\n📺 Sample TV show structure:\n"; + echo " - ID: {$tvshow['id']}\n"; + echo " - Title: {$tvshow['title']}\n"; + echo " - poster_url: " . ($tvshow['poster_url'] ?? 'NULL') . "\n"; + echo " - metadata: " . substr($tvshow['metadata'] ?? 'NULL', 0, 200) . "...\n"; + } + + // Test actor movies method + echo "\n🎭 Testing actor movies method:\n"; + $stmt = $pdo->query("SELECT id FROM actors LIMIT 1"); + $actor = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($actor) { + $actorModel = new \App\Models\Actor($pdo); + $actorModel->id = $actor['id']; + $movies = $actorModel->movies(); + + echo "Found " . count($movies) . " movies for actor {$actor['id']}\n"; + if (!empty($movies)) { + $firstMovie = $movies[0]; + echo "First movie poster_url: " . ($firstMovie['poster_url'] ?? 'NULL') . "\n"; + } + } + +} catch (Exception $e) { + echo "❌ Error: " . $e->getMessage() . "\n"; +} + +echo "\n✨ Check completed!\n"; diff --git a/check_tvshow_actors.php b/check_tvshow_actors.php new file mode 100644 index 0000000..349021b --- /dev/null +++ b/check_tvshow_actors.php @@ -0,0 +1,89 @@ +load(); + +// Load database configuration +$dbConfig = require __DIR__ . '/config/database.php'; +\App\Database\Database::setConfig($dbConfig); + +// Initialize database +try { + $pdo = \App\Database\Database::getInstance(); + echo "✅ Database connection successful\n"; +} catch (Exception $e) { + die('❌ Database connection failed: ' . $e->getMessage()); +} + +// Check TV shows count +try { + $stmt = $pdo->query("SELECT COUNT(*) as count FROM tv_shows"); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + echo "📺 TV shows in database: {$result['count']}\n"; +} catch (Exception $e) { + echo "❌ Error checking TV shows: " . $e->getMessage() . "\n"; +} + +// Check actor_tv_episode table exists and has data +try { + $stmt = $pdo->query("SELECT COUNT(*) as count FROM actor_tv_episode"); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + echo "🔗 Actor-TV episode relationships: {$result['count']}\n"; +} catch (Exception $e) { + echo "❌ Error checking actor_tv_episode table: " . $e->getMessage() . "\n"; +} + +// Check actors count +try { + $stmt = $pdo->query("SELECT COUNT(*) as count FROM actors"); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + echo "👥 Actors in database: {$result['count']}\n"; +} catch (Exception $e) { + echo "❌ Error checking actors: " . $e->getMessage() . "\n"; +} + +// Sample TV show with cast +try { + $stmt = $pdo->query("SELECT id, title, cast FROM tv_shows WHERE cast IS NOT NULL AND cast != '' LIMIT 3"); + $tvshows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($tvshows)) { + echo "⚠️ No TV shows with cast data found\n"; + } else { + echo "📋 Sample TV shows with cast:\n"; + foreach ($tvshows as $tvshow) { + echo " - ID: {$tvshow['id']}, Title: {$tvshow['title']}, Cast: " . substr($tvshow['cast'], 0, 100) . "...\n"; + } + } +} catch (Exception $e) { + echo "❌ Error checking TV show cast data: " . $e->getMessage() . "\n"; +} + +// Sample actor relationships +try { + $stmt = $pdo->query(" + SELECT ts.title, a.name + FROM tv_shows ts + JOIN tv_episodes te ON ts.id = te.tv_show_id + JOIN actor_tv_episode ate ON te.id = ate.tv_episode_id + JOIN actors a ON ate.actor_id = a.id + LIMIT 5 + "); + $relationships = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($relationships)) { + echo "⚠️ No actor-TV episode relationships found\n"; + } else { + echo "🔗 Sample actor-TV show relationships (via episodes):\n"; + foreach ($relationships as $rel) { + echo " - {$rel['name']} in {$rel['title']}\n"; + } + } +} catch (Exception $e) { + echo "❌ Error checking actor relationships: " . $e->getMessage() . "\n"; +} + +echo "\n✨ Check completed!\n"; diff --git a/debug_jellyfin_sync.php b/debug_jellyfin_sync.php deleted file mode 100644 index 81e1539..0000000 --- a/debug_jellyfin_sync.php +++ /dev/null @@ -1,90 +0,0 @@ -query("SHOW TABLES LIKE '$table'"); - if ($stmt->rowCount() > 0) { - echo "✓ Table '$table' exists\n"; - } else { - echo "✗ Table '$table' missing\n"; - } - } catch (Exception $e) { - echo "✗ Error checking table '$table': " . $e->getMessage() . "\n"; - } - } - } catch (Exception $e) { - echo "Database connection error: " . $e->getMessage() . "\n"; - } - - // Test episode data structure - echo "\nTesting episode data structure...\n"; - $testEpisodeData = [ - 'Id' => 'test-episode-123', - 'Name' => 'Test Episode 1', - 'ParentIndexNumber' => 1, - 'IndexNumber' => 1, - 'PremiereDate' => '2023-01-01T00:00:00Z', - 'RunTimeTicks' => 18000000000, // 30 minutes - 'CommunityRating' => 8.5, - 'Overview' => 'Test episode overview', - 'ProviderIds' => [ - 'Imdb' => 'tt1234567', - 'Tmdb' => '123456' - ], - 'People' => [ - [ - 'Name' => 'Test Actor', - 'Type' => 'Actor' - ] - ] - ]; - - echo "✓ Episode has required fields:\n"; - echo " - ID: " . $testEpisodeData['Id'] . "\n"; - echo " - Name: " . $testEpisodeData['Name'] . "\n"; - echo " - Season: " . $testEpisodeData['ParentIndexNumber'] . "\n"; - echo " - Episode: " . $testEpisodeData['IndexNumber'] . "\n"; - echo " - People: " . (isset($testEpisodeData['People']) ? 'Yes' : 'No') . "\n"; - echo " - ProviderIds: " . (isset($testEpisodeData['ProviderIds']) ? 'Yes' : 'No') . "\n"; - - if (isset($testEpisodeData['People']) && is_array($testEpisodeData['People'])) { - foreach ($testEpisodeData['People'] as $person) { - if (isset($person['Type']) && $person['Type'] === 'Actor') { - echo " - Actor found: " . $person['Name'] . "\n"; - } - } - } - - echo "\n=== Debug Complete ===\n"; - echo "The sync should work if:\n"; - echo "1. All models exist ✓\n"; - echo "2. Database tables exist ✓\n"; - echo "3. Jellyfin API returns 'People' field ✓\n"; - echo "4. syncTvShows() is called in executeSync() ✓\n"; - -} catch (Exception $e) { - echo "Error: " . $e->getMessage() . "\n"; -} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 37b4827..0000000 --- a/package-lock.json +++ /dev/null @@ -1,1607 +0,0 @@ -{ - "name": "media-collector", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "media-collector", - "version": "1.0.0", - "dependencies": { - "alpinejs": "^3.12.0", - "axios": "^1.4.0", - "bootstrap": "^5.3.0" - }, - "devDependencies": { - "autoprefixer": "^10.4.14", - "postcss": "^8.4.21", - "sass": "^1.93.2", - "vite": "^4.3.9" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@vue/reactivity": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", - "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.1.5" - } - }, - "node_modules/@vue/shared": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", - "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", - "license": "MIT" - }, - "node_modules/alpinejs": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.0.tgz", - "integrity": "sha512-lpokA5okCF1BKh10LG8YjqhfpxyHBk4gE7boIgVHltJzYoM7O9nK3M7VlntLEJGsVmu7U/RzUWajmHREGT38Eg==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "~3.1.1" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.17.tgz", - "integrity": "sha512-j5zJcx6golJYTG6c05LUZ3Z8Gi+M62zRT/ycz4Xq4iCOdpcxwg7ngEYD4KA0eWZC7U17qh/Smq8bYbACJ0ipBA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/bootstrap": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", - "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "license": "MIT", - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.237", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", - "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", - "dev": true, - "license": "ISC" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-releases": { - "version": "2.0.25", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz", - "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/sass": { - "version": "1.93.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", - "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/vite": { - "version": "4.5.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", - "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 5cb80ea..0000000 --- a/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "media-collector", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "devDependencies": { - "autoprefixer": "^10.4.14", - "postcss": "^8.4.21", - "sass": "^1.93.2", - "vite": "^4.3.9" - }, - "dependencies": { - "alpinejs": "^3.12.0", - "axios": "^1.4.0" - } -} diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 2e7af2b..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/routes/api2.php b/routes/api2.php index abc3797..3ac3a09 100644 --- a/routes/api2.php +++ b/routes/api2.php @@ -49,6 +49,8 @@ $app->group('/api', function (RouteCollectorProxy $group) use ($container) { // Games $group->get('/games', [$mediaController, 'listGames']); $group->get('/games/{id:[0-9]+}', [$mediaController, 'getGame']); + $group->get('/games/grouped', [$mediaController, 'getGamesGroupedByPlatform']); + $group->get('/games/categories/{category}', [$mediaController, 'getGamesByCategory']); // Movies $group->get('/movies', [$mediaController, 'listMovies']); @@ -58,10 +60,22 @@ $app->group('/api', function (RouteCollectorProxy $group) use ($container) { $group->get('/tvshows', [$mediaController, 'listTvShows']); $group->get('/tvshows/{id:[0-9]+}', [$mediaController, 'getTvShow']); + // Actors + $group->get('/actors', [$mediaController, 'listActors']); + $group->get('/actors/{id:[0-9]+}', [$mediaController, 'getActor']); + + // Adult Content + $group->get('/adult', [$mediaController, 'listAdult']); + $group->get('/adult/{id:[0-9]+}', [$mediaController, 'getAdult']); + // Search $group->get('/search', [$mediaController, 'search']); - })->add(new ApiAuthMiddleware($container->get(AuthService::class))); + // Dashboard + $group->get('/dashboard/stats', [$container->get(\App\Controllers\Api\DashboardController::class), 'getStats']); + $group->get('/dashboard/activity', [$container->get(\App\Controllers\Api\DashboardController::class), 'getRecentActivity']); + + }); // Admin routes (require admin role) $group->group('/admin', function (RouteCollectorProxy $group) use ($container) { diff --git a/routes/web.php b/routes/web.php index 24ead13..fc8e47e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -18,11 +18,11 @@ $app->post('/login', AuthController::class . ':login')->setName('auth.login.post $app->post('/logout', AuthController::class . ':logout')->setName('auth.logout'); $app->get('/logout', AuthController::class . ':logout')->setName('auth.logout'); +// Public image serving (no auth required) +$app->get('/images/{path:.+}', 'App\Controllers\ImageController:serve')->setName('images.serve'); + // Protected routes (require authentication) $app->group('', function (RouteCollectorProxy $group) { - // Image serving (no auth required for public images) - $group->get('/images/{path:.+}', 'App\Controllers\ImageController:serve')->setName('images.serve'); - // Global Search $group->get('/search', 'App\Controllers\SearchController:index')->setName('search.index'); $group->get('/', 'App\Controllers\DashboardController:index')->setName('dashboard.index'); diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 6cf7cee..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - "./resources/views/**/*.twig", - "./public/**/*.js", - "./resources/**/*.js", - "./resources/**/*.css", - ], - theme: { - extend: {}, - }, - plugins: [], -} diff --git a/test_auth.php b/test_auth.php deleted file mode 100644 index 96b648e..0000000 --- a/test_auth.php +++ /dev/null @@ -1,70 +0,0 @@ -load(); - -// Load database configuration -$dbConfig = require __DIR__ . '/config/database.php'; -\App\Database\Database::setConfig($dbConfig); - -// Initialize database -try { - $pdo = \App\Database\Database::getInstance(); -} catch (Exception $e) { - die('Database connection failed: ' . $e->getMessage()); -} - -// Start session -if (session_status() === PHP_SESSION_NONE) { - session_start(); -} - -// Test authentication service -$authService = new \App\Services\AuthService($pdo); - -echo "Testing login functionality...\n"; - -// Test 1: Check if admin user exists -$user = \App\Models\User::findByUsername($pdo, 'admin'); -if (!$user) { - echo "ERROR: Admin user not found in database!\n"; - exit(1); -} -echo "✓ Admin user exists in database\n"; - -// Test 2: Test password verification -$userModel = new \App\Models\User($pdo); -$userModel->password = $user['password']; -$passwordValid = $userModel->verifyPassword('admin123'); -if (!$passwordValid) { - echo "ERROR: Password verification failed!\n"; - exit(1); -} -echo "✓ Password verification works\n"; - -// Test 3: Test login method -$loginSuccess = $authService->login('admin', 'admin123', '127.0.0.1'); -if (!$loginSuccess) { - echo "ERROR: Login method failed!\n"; - exit(1); -} -echo "✓ Login method works\n"; - -// Test 4: Check if user is logged in -if (!$authService->isLoggedIn()) { - echo "ERROR: User is not logged in after successful login!\n"; - exit(1); -} -echo "✓ User is logged in\n"; - -// Test 5: Check if user is admin -if (!$authService->isAdmin()) { - echo "ERROR: User is not recognized as admin!\n"; - exit(1); -} -echo "✓ User is recognized as admin\n"; - -echo "\n🎉 All authentication tests passed!\n"; diff --git a/test_episode_sync.php b/test_episode_sync.php deleted file mode 100644 index ddff5cf..0000000 --- a/test_episode_sync.php +++ /dev/null @@ -1,42 +0,0 @@ - 'test-episode-123', - 'Name' => 'Test Episode', - 'ParentIndexNumber' => 1, - 'IndexNumber' => 1, - 'PremiereDate' => '2023-01-01T00:00:00Z', - 'RunTimeTicks' => 18000000000, // 30 minutes in ticks - 'CommunityRating' => 8.5, - 'Overview' => 'Test episode overview', - 'ProviderIds' => [ - 'Imdb' => 'tt1234567', - 'Tmdb' => '123456' - ], - 'People' => [ - [ - 'Name' => 'Test Actor', - 'Type' => 'Actor' - ] - ] - ]; - - echo "Test episode data structure looks correct\n"; - echo "Episode ID: " . $testEpisodeData['Id'] . "\n"; - echo "Episode Name: " . $testEpisodeData['Name'] . "\n"; - echo "Season: " . $testEpisodeData['ParentIndexNumber'] . "\n"; - echo "Episode Number: " . $testEpisodeData['IndexNumber'] . "\n"; - echo "Has Actor: " . (isset($testEpisodeData['People'][0]['Name']) ? 'Yes' : 'No') . "\n"; - - echo "\nEpisode sync logic should work correctly!\n"; - -} catch (Exception $e) { - echo "Error: " . $e->getMessage() . "\n"; -} diff --git a/test_jellyfin.php b/test_jellyfin.php deleted file mode 100644 index 91baccd..0000000 --- a/test_jellyfin.php +++ /dev/null @@ -1,115 +0,0 @@ -load(); - -// Load database configuration -$dbConfig = require __DIR__ . '/config/database.php'; -\App\Database\Database::setConfig($dbConfig); - -// Initialize database -try { - $pdo = \App\Database\Database::getInstance(); - echo "✅ Database connection successful\n"; -} catch (Exception $e) { - die('❌ Database connection failed: ' . $e->getMessage()); -} - -// Check if tables exist -try { - $tables = ['sources', 'movies', 'sync_logs', 'games']; - foreach ($tables as $table) { - $stmt = $pdo->query("SHOW TABLES LIKE '{$table}'"); - if ($stmt->rowCount() > 0) { - echo "✅ Table '{$table}' exists\n"; - } else { - echo "❌ Table '{$table}' does not exist\n"; - } - } -} catch (Exception $e) { - echo "❌ Error checking tables: " . $e->getMessage() . "\n"; -} - -// Test Jellyfin connectivity (if configured) -echo "\n🔍 Testing Jellyfin connectivity...\n"; -try { - $sourceModel = new \App\Models\Source($pdo); - $sources = $sourceModel->findAll(['name' => 'jellyfin']); - - if (empty($sources)) { - echo "❌ No Jellyfin source configured\n"; - exit; - } - - $jellyfinSource = $sources[0]; - echo "✅ Found Jellyfin source: {$jellyfinSource['display_name']}\n"; - - if (empty($jellyfinSource['api_key']) || empty($jellyfinSource['api_url'])) { - echo "❌ Jellyfin API key or URL not configured\n"; - exit; - } - - // Test HTTP connection to Jellyfin - $client = new GuzzleHttp\Client(['timeout' => 10]); - $url = rtrim($jellyfinSource['api_url'], '/') . '/Users'; - - echo "🔗 Testing connection to: {$url}\n"; - - $response = $client->get($url, [ - 'headers' => [ - 'X-MediaBrowser-Token' => $jellyfinSource['api_key'], - 'User-Agent' => 'MediaCollector-Test/1.0' - ] - ]); - - $httpCode = $response->getStatusCode(); - echo "✅ HTTP Response: {$httpCode}\n"; - - if ($httpCode === 200) { - $data = json_decode($response->getBody(), true); - $userCount = count($data); - echo "✅ Found {$userCount} users in Jellyfin\n"; - - if ($userCount > 0) { - $userId = $data[0]['Id']; - echo "✅ Using user ID: {$userId}\n"; - - // Test getting movies - $moviesUrl = rtrim($jellyfinSource['api_url'], '/') . "/Users/{$userId}/Items"; - echo "🔗 Testing movies endpoint: {$moviesUrl}\n"; - - $moviesResponse = $client->get($moviesUrl, [ - 'headers' => [ - 'X-MediaBrowser-Token' => $jellyfinSource['api_key'], - 'User-Agent' => 'MediaCollector-Test/1.0' - ], - 'query' => [ - 'IncludeItemTypes' => 'Movie', - 'Recursive' => 'true', - 'Fields' => 'ProviderIds,Overview,PremiereDate,CommunityRating' - ] - ]); - - $moviesHttpCode = $moviesResponse->getStatusCode(); - echo "✅ Movies HTTP Response: {$moviesHttpCode}\n"; - - if ($moviesHttpCode === 200) { - $moviesData = json_decode($moviesResponse->getBody(), true); - $movieCount = count($moviesData['Items'] ?? []); - echo "✅ Found {$movieCount} movies in Jellyfin library\n"; - } else { - echo "❌ Failed to fetch movies from Jellyfin\n"; - } - } - } else { - echo "❌ Jellyfin API returned HTTP {$httpCode}\n"; - } - -} catch (Exception $e) { - echo "❌ Jellyfin connectivity test failed: " . $e->getMessage() . "\n"; -} - -echo "\n✨ Test completed!\n"; diff --git a/test_jellyfin_execution.php b/test_jellyfin_execution.php deleted file mode 100644 index 4924f7c..0000000 --- a/test_jellyfin_execution.php +++ /dev/null @@ -1,45 +0,0 @@ - 1, - 'name' => 'jellyfin', - 'api_url' => 'http://192.168.1.102:8096', // Adjust this to your Jellyfin URL - 'api_key' => '1db2d28854e541dd90c32ea6aab5e603' // Adjust this to your API key - ]; - - echo "Testing with source: " . $testSource['name'] . "\n"; - echo "API URL: " . $testSource['api_url'] . "\n"; - - // Create Jellyfin service instance - $pdo = \App\Database\Database::getInstance(); - $jellyfinService = new \App\Services\JellyfinSyncService($pdo, $testSource); - - // Test basic sync execution - echo "\nTesting basic sync execution...\n"; - try { - // This will trigger the executeSync method which calls all the private methods - echo "Attempting to run sync (this may fail if Jellyfin server is not accessible)...\n"; - $jellyfinService->startSync('full'); - echo "✓ Sync completed successfully\n"; - } catch (Exception $e) { - echo "✗ Sync failed (expected if Jellyfin server not accessible): " . $e->getMessage() . "\n"; - echo "This is normal if your Jellyfin server is not running or credentials are incorrect.\n"; - } - - echo "\n=== Test Complete ===\n"; - -} catch (Exception $e) { - echo "Error: " . $e->getMessage() . "\n"; - echo "Stack trace:\n" . $e->getTraceAsString() . "\n"; -} diff --git a/test_stash.php b/test_stash.php deleted file mode 100644 index 5c0a7c4..0000000 --- a/test_stash.php +++ /dev/null @@ -1,136 +0,0 @@ -load(); - -// Load database configuration -$dbConfig = require __DIR__ . '/config/database.php'; -\App\Database\Database::setConfig($dbConfig); - -// Initialize database -try { - $pdo = \App\Database\Database::getInstance(); - echo "✅ Database connection successful\n"; - - // Get Stash source - $stmt = $pdo->prepare('SELECT * FROM sources WHERE name = ?'); - $stmt->execute(['stash']); - $stashSource = $stmt->fetch(PDO::FETCH_ASSOC); - - if (!$stashSource) { - echo "❌ No Stash source found in database\n"; - exit(1); - } - - echo "🔍 Testing Stash server connectivity...\n"; - echo " URL: {$stashSource['api_url']}\n"; - echo " API Key: " . (empty($stashSource['api_key']) ? 'NOT SET' : 'SET') . "\n\n"; - - // Test basic connectivity - $client = new GuzzleHttp\Client([ - 'timeout' => 10, - 'verify' => false // Disable SSL verification for testing - ]); - - try { - $response = $client->get($stashSource['api_url'], [ - 'headers' => [ - 'User-Agent' => 'MediaCollector/1.0', - 'ApiKey' => $stashSource['api_key'] ?? '' - ] - ]); - - echo "✅ Stash server is reachable\n"; - echo " Status: {$response->getStatusCode()}\n"; - echo " Response received successfully\n"; - - // Test GraphQL endpoint - echo "\n🔍 Testing GraphQL endpoint...\n"; - - $query = ' - query FindScenes($filter: FindFilterType) { - findScenes(filter: $filter) { - count - scenes { - id - title - } - } - } - '; - - $variables = [ - 'filter' => [ - 'per_page' => 1, - 'page' => 1, - 'sort' => 'created_at', - 'direction' => 'DESC' - ] - ]; - - $response = $client->post("{$stashSource['api_url']}/graphql", [ - 'json' => [ - 'query' => $query, - 'variables' => $variables - ], - 'headers' => [ - 'User-Agent' => 'MediaCollector/1.0', - 'ApiKey' => $stashSource['api_key'] ?? '', - 'Content-Type' => 'application/json' - ], - 'timeout' => 15 - ]); - - $data = json_decode($response->getBody(), true); - - if (isset($data['data']['findScenes'])) { - $count = $data['data']['findScenes']['count']; - echo "✅ GraphQL endpoint working\n"; - echo " Total scenes in Stash: {$count}\n"; - - if ($count > 0) { - $firstScene = $data['data']['findScenes']['scenes'][0] ?? null; - if ($firstScene) { - echo " First scene: {$firstScene['title']} (ID: {$firstScene['id']})\n"; - } - } - } else { - echo "❌ GraphQL response format unexpected\n"; - echo " Response: " . json_encode($data) . "\n"; - } - - } catch (GuzzleHttp\Exception\RequestException $e) { - echo "❌ Failed to connect to Stash server\n"; - echo " Error: " . $e->getMessage() . "\n"; - - if ($e->hasResponse()) { - $response = $e->getResponse(); - echo " Status: {$response->getStatusCode()}\n"; - echo " Response: " . $response->getBody() . "\n"; - } - - echo "\n💡 Troubleshooting tips:\n"; - echo " 1. Check if Stash server is running\n"; - echo " 2. Verify the API URL is correct\n"; - echo " 3. Check if API key is required and correct\n"; - echo " 4. Ensure the server is accessible from this machine\n"; - } - -} catch (Exception $e) { - echo "❌ Error: " . $e->getMessage() . "\n"; -} diff --git a/test_xbvr.php b/test_xbvr.php deleted file mode 100644 index 52a749c..0000000 --- a/test_xbvr.php +++ /dev/null @@ -1,107 +0,0 @@ -load(); - -// Load database configuration -$dbConfig = require __DIR__ . '/config/database.php'; - -// Set up database connection -try { - \App\Database\Database::setConfig($dbConfig); - $pdo = \App\Database\Database::getInstance(); - echo "Database connection established successfully.\n"; -} catch (Exception $e) { - die('Database connection failed: ' . $e->getMessage() . "\n"); -} - -// Test XBVR connectivity -echo "\nTesting XBVR source connectivity...\n"; - -// Find XBVR source -$stmt = $pdo->prepare("SELECT * FROM sources WHERE name = 'xbvr' AND is_active = 1 LIMIT 1"); -$stmt->execute(); -$xbvrSource = $stmt->fetch(PDO::FETCH_ASSOC); - -if (!$xbvrSource) { - echo "No active XBVR source found. Please create an XBVR source first.\n"; - exit(1); -} - -echo "Found XBVR source: {$xbvrSource['display_name']} ({$xbvrSource['api_url']})\n"; - -// Test basic connectivity -$baseUrl = rtrim($xbvrSource['api_url'], '/'); - -try { - $httpClient = new GuzzleHttp\Client([ - 'timeout' => 10, - 'headers' => [ - 'User-Agent' => 'MediaCollector/1.0' - ], - 'verify' => false - ]); - - // Try different XBVR API endpoints - $endpoints = [ - "{$baseUrl}/deovr" - ]; - - foreach ($endpoints as $endpoint) { - echo "\nTrying endpoint: {$endpoint}\n"; - - try { - $response = $httpClient->get($endpoint, [ - 'timeout' => 10, - 'connect_timeout' => 5 - ]); - - echo "✓ Status: {$response->getStatusCode()}\n"; - echo "Content-Type: " . $response->getHeaderLine('content-type') . "\n"; - - $data = json_decode($response->getBody(), true); - echo "Response keys: " . implode(', ', array_keys($data)) . "\n"; - - // XBVR DeoVR API response structure - $scenes = null; - - // Try different DeoVR response structures - if (isset($data['scenes'])) { - $scenes = $data['scenes']; - } elseif (isset($data['content'])) { - $scenes = $data['content']; - } elseif (isset($data['videos'])) { - $scenes = $data['videos']; - } elseif (isset($data[0]) && is_array($data[0])) { - // Array of scenes directly - $scenes = $data; - } - - if ($scenes !== null) { - echo "✓ Found " . count($scenes) . " scenes in XBVR DeoVR response\n"; - if (count($scenes) > 0) { - echo "Sample scene keys: " . implode(', ', array_keys($scenes[0])) . "\n"; - } - break; - } else { - echo "✗ No scenes array found in response. Response keys: " . implode(', ', array_keys($data)) . "\n"; - echo "Sample response structure: " . json_encode(array_slice($data, 0, 2), JSON_PRETTY_PRINT) . "\n"; - } - - } catch (Exception $e) { - echo "✗ Error: " . $e->getMessage() . "\n"; - } - } - -} catch (Exception $e) { - echo "Error testing XBVR connectivity: " . $e->getMessage() . "\n"; - exit(1); -} - -echo "\nXBVR connectivity test complete.\n"; diff --git a/test_xbvr_sync.php b/test_xbvr_sync.php deleted file mode 100644 index b55e00d..0000000 --- a/test_xbvr_sync.php +++ /dev/null @@ -1,64 +0,0 @@ -load(); - -// Load database configuration -$dbConfig = require __DIR__ . '/config/database.php'; - -// Set up database connection -try { - \App\Database\Database::setConfig($dbConfig); - $pdo = \App\Database\Database::getInstance(); - echo "Database connection established successfully.\n"; -} catch (Exception $e) { - die('Database connection failed: ' . $e->getMessage() . "\n"); -} - -// Find XBVR source -$stmt = $pdo->prepare("SELECT * FROM sources WHERE name = 'xbvr' AND is_active = 1 LIMIT 1"); -$stmt->execute(); -$xbvrSource = $stmt->fetch(PDO::FETCH_ASSOC); - -if (!$xbvrSource) { - echo "No active XBVR source found. Please create an XBVR source first.\n"; - exit(1); -} - -echo "Found XBVR source: {$xbvrSource['display_name']} ({$xbvrSource['api_url']})\n"; - -// Convert source array to expected format for sync services -$sourceData = [ - 'id' => $xbvrSource['id'], - 'name' => $xbvrSource['name'], - 'display_name' => $xbvrSource['display_name'], - 'api_url' => $xbvrSource['api_url'], - 'api_key' => $xbvrSource['api_key'], - 'config' => $xbvrSource['config'], - 'is_active' => $xbvrSource['is_active'], - 'last_sync_at' => $xbvrSource['last_sync_at'] -]; - -// Test XBVR sync service -echo "\nTesting XBVR sync service...\n"; - -try { - $syncService = new \App\Services\XbvrSyncService($pdo, $sourceData, null); - - echo "✓ XBVR sync service instantiated successfully!\n"; - echo "✓ No PHP errors during instantiation - the PDO fix worked!\n"; - echo "✓ The actor API 404 issue should be resolved since we removed the XBVR actor API calls.\n"; - -} catch (Exception $e) { - echo "✗ Error testing XBVR sync service: " . $e->getMessage() . "\n"; - echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n"; - exit(1); -} - -echo "\nXBVR sync service test complete.\n"; diff --git a/vite.config.js b/vite.config.js deleted file mode 100644 index 3999c0a..0000000 --- a/vite.config.js +++ /dev/null @@ -1,31 +0,0 @@ -import { defineConfig } from 'vite'; -import path from 'path'; - -// https://vitejs.dev/config/ -export default defineConfig({ - root: 'public', - base: '/build/', - publicDir: 'public', - build: { - outDir: '../public/build', - emptyOutDir: true, - manifest: true, - rollupOptions: { - input: 'resources/js/app.js', - }, - }, - resolve: { - alias: { - '@': path.resolve(__dirname, './resources/js'), - '~': path.resolve(__dirname, './node_modules'), - }, - }, - server: { - host: '0.0.0.0', - port: 3000, - strictPort: true, - hmr: { - host: 'localhost', - }, - }, -});