Performance and settings updates: - Add a new jellyfin_library_mappings column to the settings table and wire it into the Settings model (update handling and default value). This enables storing Jellyfin library mappings in settings. - Optimize Cast::list by loading all cast filmography in a single joined query and grouping results per cast to avoid N+1 queries. - Remove per-item cast/staff loading in Media model to avoid repeated queries during list/search operations. - Remove game-specific enrichment from MediaController search to stop extra game info lookups during search responses. These changes reduce repeated DB calls and centralize Jellyfin mapping storage.
394 lines
14 KiB
PHP
394 lines
14 KiB
PHP
<?php
|
|
|
|
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;
|
|
private $series;
|
|
private $music;
|
|
private $game;
|
|
private $logger;
|
|
|
|
public function __construct($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($method, $segments) {
|
|
$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($method, $mediaId, $segments) {
|
|
$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($method, $mediaId, $segments) {
|
|
$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($mediaId) {
|
|
$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($mediaId) {
|
|
$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($episodeId) {
|
|
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($episodeId) {
|
|
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($episodeId) {
|
|
// 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($mediaId) {
|
|
$tracks = $this->music->getTracks($mediaId);
|
|
return ['success' => true, 'data' => ['items' => $tracks]];
|
|
}
|
|
|
|
private function addTrack($mediaId) {
|
|
$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($trackId) {
|
|
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($trackId) {
|
|
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($trackId) {
|
|
// 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($id) {
|
|
// 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() {
|
|
$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() {
|
|
$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($id) {
|
|
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($id) {
|
|
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'];
|
|
}
|
|
}
|