Apply strict_types and extensive type declarations throughout the API and models, improving type safety and error handling. Key changes: add declare(strict_types=1) to many files; convert properties, method parameters and return values to typed signatures (PDO, arrays, ints, strings, bools, nullables); switch exception handling to Throwable in index and Router; improve Router, controllers and model method signatures and nullability handling; refine file/image serving security checks and headers in ImageController; strengthen Database typing and initialization methods; return explicit types from BaseModel CRUD helpers and counting; update Media/Cast/Adult/Game/Console/Settings controllers and models to use typed methods, better validation, and clearer update/create return types. Also add AGENTS.md (agent skills index), update README with Swagger/OpenAPI usage instructions, and add /.windsurf to .gitignore. These changes aim to harden runtime correctness, make intended contracts explicit, and prepare the codebase for easier maintenance and static analysis.
396 lines
14 KiB
PHP
396 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../models/Media.php';
|
|
require_once __DIR__ . '/../models/Series.php';
|
|
require_once __DIR__ . '/../models/Music.php';
|
|
require_once __DIR__ . '/../models/Game.php';
|
|
require_once __DIR__ . '/../services/ApiLogger.php';
|
|
|
|
class MediaController {
|
|
private Media $media;
|
|
private Series $series;
|
|
private Music $music;
|
|
private Game $game;
|
|
private ApiLogger $logger;
|
|
|
|
public function __construct(PDO $pdo) {
|
|
$this->media = new Media($pdo);
|
|
$this->series = new Series($pdo);
|
|
$this->music = new Music($pdo);
|
|
$this->game = new Game($pdo);
|
|
$this->logger = ApiLogger::getInstance();
|
|
}
|
|
|
|
public function handleRequest(string $method, array $segments): array {
|
|
$id = isset($segments[1]) ? (int)$segments[1] : null;
|
|
$subResource = isset($segments[2]) ? $segments[2] : null;
|
|
|
|
// Sub-Endpunkte für Episoden und Tracks
|
|
if ($id && $subResource) {
|
|
if ($subResource === 'episodes') {
|
|
return $this->handleEpisodes($method, $id, $segments);
|
|
}
|
|
if ($subResource === 'tracks') {
|
|
return $this->handleTracks($method, $id, $segments);
|
|
}
|
|
}
|
|
|
|
switch ($method) {
|
|
case 'GET':
|
|
return $id ? $this->getOne($id) : $this->getAll();
|
|
case 'POST':
|
|
return $this->create();
|
|
case 'PUT':
|
|
return $this->update($id);
|
|
case 'DELETE':
|
|
return $this->delete($id);
|
|
default:
|
|
http_response_code(405);
|
|
return ['success' => false, 'error' => 'Method not allowed'];
|
|
}
|
|
}
|
|
|
|
private function handleEpisodes(string $method, ?int $mediaId, array $segments): array {
|
|
$episodeId = isset($segments[3]) ? (int)$segments[3] : null;
|
|
|
|
switch ($method) {
|
|
case 'GET':
|
|
if ($episodeId) {
|
|
return $this->getEpisode($episodeId);
|
|
}
|
|
return $this->getEpisodes($mediaId);
|
|
case 'POST':
|
|
return $this->addEpisode($mediaId);
|
|
case 'PUT':
|
|
return $this->updateEpisode($episodeId);
|
|
case 'DELETE':
|
|
return $this->deleteEpisode($episodeId);
|
|
default:
|
|
http_response_code(405);
|
|
return ['success' => false, 'error' => 'Method not allowed'];
|
|
}
|
|
}
|
|
|
|
private function handleTracks(string $method, ?int $mediaId, array $segments): array {
|
|
$trackId = isset($segments[3]) ? (int)$segments[3] : null;
|
|
|
|
switch ($method) {
|
|
case 'GET':
|
|
if ($trackId) {
|
|
return $this->getTrack($trackId);
|
|
}
|
|
return $this->getTracks($mediaId);
|
|
case 'POST':
|
|
return $this->addTrack($mediaId);
|
|
case 'PUT':
|
|
return $this->updateTrack($trackId);
|
|
case 'DELETE':
|
|
return $this->deleteTrack($trackId);
|
|
default:
|
|
http_response_code(405);
|
|
return ['success' => false, 'error' => 'Method not allowed'];
|
|
}
|
|
}
|
|
|
|
private function getEpisodes(?int $mediaId): array {
|
|
$season = isset($_GET['season']) ? (int)$_GET['season'] : null;
|
|
$episodes = $this->series->getEpisodes($mediaId, $season);
|
|
return ['success' => true, 'data' => ['items' => $episodes]];
|
|
}
|
|
|
|
/**
|
|
* Add a new episode to a series
|
|
* @param int $mediaId Media ID
|
|
* @return array Created episode ID
|
|
*/
|
|
private function addEpisode(?int $mediaId): array {
|
|
$data = json_decode(file_get_contents('php://input'), true);
|
|
if (!$data) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'Invalid JSON'];
|
|
}
|
|
|
|
$episodeId = $this->series->addEpisode($mediaId, $data);
|
|
http_response_code(201);
|
|
return ['success' => true, 'data' => ['id' => $episodeId]];
|
|
}
|
|
|
|
/**
|
|
* Update an existing episode
|
|
* @param int $episodeId Episode ID
|
|
* @return array Updated episode ID
|
|
*/
|
|
private function updateEpisode(?int $episodeId): array {
|
|
if (!$episodeId) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'Episode ID required'];
|
|
}
|
|
|
|
$data = json_decode(file_get_contents('php://input'), true);
|
|
if (!$data) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'Invalid JSON'];
|
|
}
|
|
|
|
$this->series->updateEpisode($episodeId, $data);
|
|
return ['success' => true, 'data' => ['id' => $episodeId]];
|
|
}
|
|
|
|
/**
|
|
* Delete an episode
|
|
* @param int $episodeId Episode ID
|
|
* @return array Success message
|
|
*/
|
|
private function deleteEpisode(?int $episodeId): array {
|
|
if (!$episodeId) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'Episode ID required'];
|
|
}
|
|
|
|
$deleted = $this->series->deleteEpisode($episodeId);
|
|
if (!$deleted) {
|
|
http_response_code(404);
|
|
return ['success' => false, 'error' => 'Episode not found'];
|
|
}
|
|
return ['success' => true, 'message' => 'Episode deleted successfully'];
|
|
}
|
|
|
|
/**
|
|
* Get a single episode by ID
|
|
* @param int $episodeId Episode ID
|
|
* @return array Episode data
|
|
*/
|
|
private function getEpisode(?int $episodeId): array {
|
|
// Episode direkt aus Datenbank abrufen
|
|
$stmt = $this->series->getConnection()->prepare("SELECT * FROM episodes WHERE id = ?");
|
|
$stmt->execute([$episodeId]);
|
|
$episode = $stmt->fetch();
|
|
|
|
if (!$episode) {
|
|
http_response_code(404);
|
|
return ['success' => false, 'error' => 'Episode not found'];
|
|
}
|
|
return ['success' => true, 'data' => $episode];
|
|
}
|
|
|
|
private function getTracks(?int $mediaId): array {
|
|
$tracks = $this->music->getTracks($mediaId);
|
|
return ['success' => true, 'data' => ['items' => $tracks]];
|
|
}
|
|
|
|
private function addTrack(?int $mediaId): array {
|
|
$data = json_decode(file_get_contents('php://input'), true);
|
|
if (!$data) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'Invalid JSON'];
|
|
}
|
|
|
|
$trackId = $this->music->addTrack($mediaId, $data);
|
|
http_response_code(201);
|
|
return ['success' => true, 'data' => ['id' => $trackId]];
|
|
}
|
|
|
|
private function updateTrack(?int $trackId): array {
|
|
if (!$trackId) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'Track ID required'];
|
|
}
|
|
|
|
$data = json_decode(file_get_contents('php://input'), true);
|
|
if (!$data) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'Invalid JSON'];
|
|
}
|
|
|
|
$this->music->updateTrack($trackId, $data);
|
|
return ['success' => true, 'data' => ['id' => $trackId]];
|
|
}
|
|
|
|
private function deleteTrack(?int $trackId): array {
|
|
if (!$trackId) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'Track ID required'];
|
|
}
|
|
|
|
$deleted = $this->music->deleteTrack($trackId);
|
|
if (!$deleted) {
|
|
http_response_code(404);
|
|
return ['success' => false, 'error' => 'Track not found'];
|
|
}
|
|
return ['success' => true, 'message' => 'Track deleted successfully'];
|
|
}
|
|
|
|
private function getTrack(?int $trackId): array {
|
|
// Track direkt aus Datenbank abrufen
|
|
$stmt = $this->music->getConnection()->prepare("SELECT * FROM tracks WHERE id = ?");
|
|
$stmt->execute([$trackId]);
|
|
$track = $stmt->fetch();
|
|
|
|
if (!$track) {
|
|
http_response_code(404);
|
|
return ['success' => false, 'error' => 'Track not found'];
|
|
}
|
|
return ['success' => true, 'data' => $track];
|
|
}
|
|
|
|
/**
|
|
* Get a single media item by ID
|
|
* @param int $id Media ID
|
|
* @return array Media object with relations
|
|
*/
|
|
private function getOne(?int $id): array {
|
|
// Zuerst Basis-Media abrufen um Typ zu bestimmen
|
|
$baseMedia = $this->media->getBase($id);
|
|
if (!$baseMedia) {
|
|
http_response_code(404);
|
|
return ['success' => false, 'error' => 'Media not found'];
|
|
}
|
|
|
|
// Typ-spezifisches Abrufen
|
|
switch ($baseMedia['type']) {
|
|
case 'TV':
|
|
$media = $this->series->getWithEpisodes($id);
|
|
break;
|
|
case 'Album':
|
|
$media = $this->music->getWithTracks($id);
|
|
break;
|
|
case 'Game':
|
|
$media = $this->game->getWithGameInfo($id);
|
|
break;
|
|
default:
|
|
$media = $this->media->getWithRelations($id);
|
|
}
|
|
|
|
return ['success' => true, 'data' => $media];
|
|
}
|
|
|
|
/**
|
|
* Get all media items with filtering and pagination
|
|
* @return array Paginated media list
|
|
*/
|
|
private function getAll(): array {
|
|
$filters = [];
|
|
if (isset($_GET['category'])) $filters['category'] = $_GET['category'];
|
|
if (isset($_GET['type'])) $filters['type'] = $_GET['type'];
|
|
if (isset($_GET['search'])) $filters['search'] = $_GET['search'];
|
|
|
|
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
|
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
|
|
|
|
$result = $this->media->search($filters, $page, $limit);
|
|
|
|
return ['success' => true, 'data' => $result];
|
|
}
|
|
|
|
/**
|
|
* Create a new media item
|
|
* @return array Created media ID
|
|
*/
|
|
private function create(): array {
|
|
$data = json_decode(file_get_contents('php://input'), true);
|
|
if (!$data) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'Invalid JSON'];
|
|
}
|
|
|
|
error_log("MediaController::create - Data received, poster field exists: " . (isset($data['poster']) ? 'yes' : 'no'));
|
|
if (isset($data['poster'])) {
|
|
error_log("MediaController::create - Poster length: " . strlen($data['poster']));
|
|
error_log("MediaController::create - Poster starts with: " . substr($data['poster'], 0, 50));
|
|
}
|
|
|
|
$title = $data['title'] ?? null;
|
|
if (!$title) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'Title is required'];
|
|
}
|
|
|
|
// Prüfen ob bereits Eintrag mit diesem cleanname existiert
|
|
$cleanname = generateCleanName($title);
|
|
$existing = $this->media->findByCleanName($cleanname);
|
|
|
|
if ($existing) {
|
|
http_response_code(200);
|
|
$this->logger->logRequest('POST', '/api/media', [], $data);
|
|
$this->logger->logResponse('POST', '/api/media', 200, ['id' => $existing['id'], 'message' => 'Media already exists']);
|
|
return ['success' => true, 'data' => ['id' => $existing['id'], 'message' => 'Media already exists']];
|
|
}
|
|
|
|
|
|
|
|
// Typ-spezifisches Erstellen
|
|
$type = $data['type'] ?? null;
|
|
if ($type === 'Game') {
|
|
$mediaId = $this->game->createWithRelations($data);
|
|
} elseif ($type === 'TV') {
|
|
$mediaId = $this->series->createWithRelations($data);
|
|
} elseif ($type === 'Album') {
|
|
$mediaId = $this->music->createWithRelations($data);
|
|
} else {
|
|
$mediaId = $this->media->createWithRelations($data);
|
|
}
|
|
|
|
http_response_code(201);
|
|
$this->logger->logRequest('POST', '/api/media', [], $data);
|
|
$this->logger->logResponse('POST', '/api/media', 201, ['id' => $mediaId]);
|
|
return ['success' => true, 'data' => ['id' => $mediaId]];
|
|
}
|
|
|
|
/**
|
|
* Update an existing media item
|
|
* @param int $id Media ID
|
|
* @return array Updated media ID
|
|
*/
|
|
private function update(?int $id): array {
|
|
if (!$id) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'ID required'];
|
|
}
|
|
|
|
$data = json_decode(file_get_contents('php://input'), true);
|
|
if (!$data) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'Invalid JSON'];
|
|
}
|
|
|
|
// Typ-spezifisches Aktualisieren
|
|
$type = $data['type'] ?? null;
|
|
if ($type === 'Game') {
|
|
$this->game->updateWithRelations($id, $data);
|
|
} elseif ($type === 'TV') {
|
|
$this->series->updateWithRelations($id, $data);
|
|
} elseif ($type === 'Album') {
|
|
$this->music->updateWithRelations($id, $data);
|
|
} else {
|
|
$this->media->updateWithRelations($id, $data);
|
|
}
|
|
|
|
$this->logger->logRequest('PUT', "/api/media/$id", [], $data);
|
|
$this->logger->logResponse('PUT', "/api/media/$id", 200, ['id' => $id]);
|
|
return ['success' => true, 'data' => ['id' => $id]];
|
|
}
|
|
|
|
/**
|
|
* Delete a media item
|
|
* @param int $id Media ID
|
|
* @return array Success message
|
|
*/
|
|
private function delete(?int $id): array {
|
|
if (!$id) {
|
|
http_response_code(400);
|
|
return ['success' => false, 'error' => 'ID required'];
|
|
}
|
|
|
|
$deleted = $this->media->delete($id);
|
|
if (!$deleted) {
|
|
http_response_code(404);
|
|
return ['success' => false, 'error' => 'Media not found'];
|
|
}
|
|
$this->logger->logRequest('DELETE', "/api/media/$id", [], null);
|
|
$this->logger->logResponse('DELETE', "/api/media/$id", 200, ['message' => 'Media deleted successfully']);
|
|
return ['success' => true, 'message' => 'Media deleted successfully'];
|
|
}
|
|
}
|