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
This commit is contained in:
Lars Behrends
2026-01-18 01:42:03 +01:00
parent b728b0c72d
commit eb1ec1153d
29 changed files with 2685 additions and 2454 deletions

View File

@@ -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];

View File

@@ -0,0 +1,206 @@
<?php
namespace App\Controllers\Api;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use App\Controllers\Api\ApiController;
class DashboardController extends ApiController
{
private \PDO $pdo;
public function __construct(\PDO $pdo)
{
$this->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);
}
}
}

View File

@@ -0,0 +1,364 @@
<?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\Controllers\Api\BaseApiController;
class GameController extends BaseApiController
{
/**
* Get all games grouped by completion status
*/
public function index(Request $request, Response $response, array $args): Response
{
$queryParams = $request->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;
}
}

File diff suppressed because it is too large Load Diff