mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
- 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
1271 lines
46 KiB
PHP
1271 lines
46 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\Api;
|
|
|
|
use Psr\Http\Message\ResponseInterface as Response;
|
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
use App\Models\Game;
|
|
use App\Models\Movie;
|
|
use App\Models\TvShow;
|
|
use App\Models\MusicArtist;
|
|
use App\Models\AdultVideo;
|
|
use App\Models\Actor;
|
|
use App\Controllers\Api\ApiController;
|
|
|
|
class MediaController extends ApiController
|
|
{
|
|
private $gameModel;
|
|
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
|
|
public function listGames(Request $request, Response $response): Response
|
|
{
|
|
try {
|
|
$pagination = $this->getPaginationParams($request);
|
|
$filters = $this->getFiltersFromRequest($request);
|
|
|
|
// 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,
|
|
'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 games: ' . $e->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
// Get grouped games (merged across platforms)
|
|
public function getGamesGroupedByPlatform(Request $request, Response $response): Response
|
|
{
|
|
try {
|
|
$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;
|
|
}
|
|
|
|
// 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, [
|
|
'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 grouped games: ' . $e->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
// Search across all media
|
|
public function search(Request $request, Response $response): Response
|
|
{
|
|
try {
|
|
$query = $request->getQueryParams()['q'] ?? '';
|
|
$type = $request->getQueryParams()['type'] ?? 'all';
|
|
$pagination = $this->getPaginationParams($request);
|
|
|
|
$results = [];
|
|
|
|
if ($type === 'all' || $type === 'game') {
|
|
$results['games'] = $this->searchGames($query, $pagination);
|
|
}
|
|
|
|
if ($type === 'all' || $type === 'movie') {
|
|
$results['movies'] = $this->searchMovies($query, $pagination);
|
|
}
|
|
|
|
if ($type === 'all' || $type === 'tvshow') {
|
|
$results['tvshows'] = $this->searchTvShows($query, $pagination);
|
|
}
|
|
|
|
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) {
|
|
return $this->error($response, 'Search failed: ' . $e->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
// Helper methods for searching different media types
|
|
private function searchGames(string $query, array $pagination): array
|
|
{
|
|
try {
|
|
// 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
|
|
else {
|
|
$allGames = $this->gameModel->findAll();
|
|
$filtered = array_filter($allGames, function($game) use ($query) {
|
|
return stripos($game['title'] ?? '', $query) !== false;
|
|
});
|
|
|
|
// Apply pagination
|
|
$games = array_slice($filtered, $pagination['offset'], $pagination['per_page']);
|
|
$total = count($filtered);
|
|
}
|
|
|
|
return [
|
|
'items' => $games,
|
|
'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 searchMovies(string $query, array $pagination): array
|
|
{
|
|
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
|
|
{
|
|
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
|
|
{
|
|
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()
|
|
];
|
|
}
|
|
}
|
|
|
|
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 = [];
|
|
$queryParams = $request->getQueryParams();
|
|
|
|
// Add common filters
|
|
if (!empty($queryParams['genre'])) {
|
|
$filters['genre'] = $queryParams['genre'];
|
|
}
|
|
|
|
if (!empty($queryParams['year'])) {
|
|
$filters['year'] = (int)$queryParams['year'];
|
|
}
|
|
|
|
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)
|
|
]
|
|
];
|
|
}
|
|
}
|