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.
284 lines
11 KiB
PHP
284 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/BaseModel.php';
|
|
|
|
class Media extends BaseModel {
|
|
protected string $table = 'media';
|
|
|
|
public function getBase(?int $id): array|false {
|
|
return $this->findById($id);
|
|
}
|
|
|
|
public function getWithRelations(?int $id): ?array {
|
|
$media = $this->findById($id);
|
|
if (!$media) {
|
|
return null;
|
|
}
|
|
|
|
$media['genres'] = $this->getRelatedItems('genres', $id);
|
|
$media['tags'] = $this->getRelatedItems('tags', $id);
|
|
$media['studios'] = $this->getRelatedItems('studios', $id);
|
|
$media['staff'] = $this->getCastForMedia($id);
|
|
|
|
return $media;
|
|
}
|
|
|
|
public function search(array $filters = [], int $page = 1, int $limit = 20): array {
|
|
$conditions = [];
|
|
|
|
if (isset($filters['category'])) {
|
|
$conditions['category'] = $filters['category'];
|
|
}
|
|
if (isset($filters['type'])) {
|
|
$conditions['type'] = $filters['type'];
|
|
}
|
|
if (isset($filters['search'])) {
|
|
$searchTerm = "%" . $filters['search'] . "%";
|
|
$conditions['title'] = [$searchTerm];
|
|
// OR Bedingung für description wird separat behandelt
|
|
}
|
|
|
|
$offset = ($page - 1) * $limit;
|
|
|
|
if (isset($filters['search'])) {
|
|
// Komplexe Suche mit OR
|
|
$query = "SELECT * FROM {$this->table} WHERE 1=1";
|
|
$params = [];
|
|
|
|
if (isset($filters['category'])) {
|
|
$query .= " AND category = ?";
|
|
$params[] = $filters['category'];
|
|
}
|
|
if (isset($filters['type'])) {
|
|
$query .= " AND type = ?";
|
|
$params[] = $filters['type'];
|
|
}
|
|
|
|
$query .= " AND (title LIKE ? OR description LIKE ?)";
|
|
$searchTerm = "%" . $filters['search'] . "%";
|
|
$params[] = $searchTerm;
|
|
$params[] = $searchTerm;
|
|
|
|
$query .= " ORDER BY createdAt DESC LIMIT " . (int)$limit . " OFFSET " . (int)$offset;
|
|
|
|
$stmt = $this->pdo->prepare($query);
|
|
$stmt->execute($params);
|
|
$items = $stmt->fetchAll();
|
|
|
|
// Count Query
|
|
$countQuery = "SELECT COUNT(*) FROM {$this->table} WHERE 1=1";
|
|
$countParams = [];
|
|
|
|
if (isset($filters['category'])) {
|
|
$countQuery .= " AND category = ?";
|
|
$countParams[] = $filters['category'];
|
|
}
|
|
if (isset($filters['type'])) {
|
|
$countQuery .= " AND type = ?";
|
|
$countParams[] = $filters['type'];
|
|
}
|
|
|
|
$countQuery .= " AND (title LIKE ? OR description LIKE ?)";
|
|
$countParams[] = $searchTerm;
|
|
$countParams[] = $searchTerm;
|
|
|
|
$countStmt = $this->pdo->prepare($countQuery);
|
|
$countStmt->execute($countParams);
|
|
$total = $countStmt->fetchColumn();
|
|
} else {
|
|
$items = $this->findAll($conditions, 'createdAt DESC', $limit, $offset);
|
|
$total = $this->count($conditions);
|
|
}
|
|
|
|
return [
|
|
'items' => $items,
|
|
'total' => $total,
|
|
'page' => $page,
|
|
'limit' => $limit,
|
|
'totalPages' => ceil($total / $limit)
|
|
];
|
|
}
|
|
|
|
public function createWithRelations(array $data): int {
|
|
$title = $data['title'] ?? null;
|
|
if (!$title) {
|
|
throw new Exception('Title is required');
|
|
}
|
|
|
|
// cleanname generieren
|
|
$cleanname = generateCleanName($title);
|
|
|
|
// Basis-Media-Daten extrahieren
|
|
$mediaData = [
|
|
'title' => $title,
|
|
'cleanname' => $cleanname,
|
|
'year' => $data['year'] ?? null,
|
|
'poster' => $data['poster'] ?? null,
|
|
'banner' => $data['banner'] ?? null,
|
|
'description' => $data['description'] ?? null,
|
|
'rating' => $data['rating'] ?? null,
|
|
'category' => $data['category'] ?? null,
|
|
'type' => $data['type'] ?? null,
|
|
'status' => $data['status'] ?? null,
|
|
'aspectRatio' => $data['aspectRatio'] ?? null,
|
|
'runtime' => $data['runtime'] ?? null,
|
|
'director' => $data['director'] ?? null,
|
|
'writer' => $data['writer'] ?? null,
|
|
'source' => $data['source'] ?? null,
|
|
'releaseDate' => $data['releaseDate'] ?? null
|
|
];
|
|
|
|
$mediaId = $this->create($mediaData);
|
|
|
|
// Relationen speichern
|
|
if (isset($data['genres']) && is_array($data['genres'])) {
|
|
$this->saveRelatedItems('genres', $mediaId, $data['genres']);
|
|
}
|
|
if (isset($data['tags']) && is_array($data['tags'])) {
|
|
$this->saveRelatedItems('tags', $mediaId, $data['tags']);
|
|
}
|
|
if (isset($data['studios']) && is_array($data['studios'])) {
|
|
$this->saveRelatedItems('studios', $mediaId, $data['studios']);
|
|
}
|
|
if (isset($data['staff']) && is_array($data['staff'])) {
|
|
$this->saveCastAssignments($mediaId, $data['staff']);
|
|
}
|
|
|
|
return $mediaId;
|
|
}
|
|
|
|
public function updateWithRelations(int $id, array $data): bool {
|
|
$mediaData = [];
|
|
|
|
foreach (['title', 'year', 'poster', 'banner', 'description', 'rating', 'category', 'type', 'status', 'aspectRatio', 'runtime', 'director', 'writer', 'releaseDate', 'source'] as $field) {
|
|
if (array_key_exists($field, $data)) {
|
|
$mediaData[$field] = $data[$field];
|
|
}
|
|
}
|
|
|
|
// Wenn title geändert wurde, cleanname aktualisieren
|
|
if (isset($data['title'])) {
|
|
$mediaData['cleanname'] = generateCleanName($data['title']);
|
|
}
|
|
|
|
if (!empty($mediaData)) {
|
|
$this->update($id, $mediaData);
|
|
}
|
|
|
|
// Relationen aktualisieren
|
|
if (isset($data['genres']) && is_array($data['genres'])) {
|
|
$this->pdo->prepare("DELETE FROM genres WHERE media_id = ?")->execute([$id]);
|
|
$this->saveRelatedItems('genres', $id, $data['genres']);
|
|
}
|
|
if (isset($data['tags']) && is_array($data['tags'])) {
|
|
$this->pdo->prepare("DELETE FROM tags WHERE media_id = ?")->execute([$id]);
|
|
$this->saveRelatedItems('tags', $id, $data['tags']);
|
|
}
|
|
if (isset($data['studios']) && is_array($data['studios'])) {
|
|
$this->pdo->prepare("DELETE FROM studios WHERE media_id = ?")->execute([$id]);
|
|
$this->saveRelatedItems('studios', $id, $data['studios']);
|
|
}
|
|
if (isset($data['staff']) && is_array($data['staff'])) {
|
|
$this->pdo->prepare("DELETE FROM media_cast WHERE media_id = ?")->execute([$id]);
|
|
$this->saveCastAssignments($id, $data['staff']);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function findByCleanName(string $cleanname): array|false {
|
|
$stmt = $this->pdo->prepare("SELECT * FROM {$this->table} WHERE cleanname = ?");
|
|
$stmt->execute([$cleanname]);
|
|
return $stmt->fetch();
|
|
}
|
|
|
|
protected function saveRelatedItems(string $table, int $id, array $items, string $fkColumn = 'media_id'): void {
|
|
$valueColumn = $table === 'genres' ? 'genre' : ($table === 'tags' ? 'tag' : ($table === 'occupations' ? 'occupation' : 'studio'));
|
|
$stmt = $this->pdo->prepare("INSERT INTO $table ($fkColumn, $valueColumn) VALUES (?, ?)");
|
|
foreach ($items as $item) {
|
|
$stmt->execute([$id, $item]);
|
|
}
|
|
}
|
|
|
|
protected function getCastForMedia(?int $mediaId): array {
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT cs.*, mc.role, mc.characterName, mc.characterImage
|
|
FROM cast_staff cs
|
|
INNER JOIN media_cast mc ON cs.id = mc.cast_id
|
|
WHERE mc.media_id = ?
|
|
");
|
|
$stmt->execute([$mediaId]);
|
|
$cast = $stmt->fetchAll();
|
|
foreach ($cast as &$member) {
|
|
$member['occupations'] = $this->getRelatedItems('occupations', $member['id'], 'cast_id');
|
|
}
|
|
return $cast;
|
|
}
|
|
|
|
protected function getRelatedItems(string $table, int $id, string $fkColumn = 'media_id'): array {
|
|
$stmt = $this->pdo->prepare("SELECT * FROM $table WHERE $fkColumn = ?");
|
|
$stmt->execute([$id]);
|
|
$items = $stmt->fetchAll();
|
|
$result = [];
|
|
foreach ($items as $item) {
|
|
if ($fkColumn === 'cast_id') {
|
|
$result[] = $item['occupation'];
|
|
} else {
|
|
$result[] = $table === 'genres' ? $item['genre'] : ($table === 'tags' ? $item['tag'] : $item['studio']);
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
protected function saveCastAssignments(int $mediaId, array $castData): void {
|
|
foreach ($castData as $member) {
|
|
$castId = null;
|
|
if (isset($member['id']) && $member['id']) {
|
|
$castId = $member['id'];
|
|
} elseif (isset($member['name'])) {
|
|
$stmt = $this->pdo->prepare("SELECT id FROM cast_staff WHERE name = ? LIMIT 1");
|
|
$stmt->execute([$member['name']]);
|
|
$existing = $stmt->fetch();
|
|
if ($existing) {
|
|
$castId = $existing['id'];
|
|
} else {
|
|
$cleanname = generateCleanName($member['name']);
|
|
$stmt = $this->pdo->prepare("
|
|
INSERT INTO cast_staff (name, cleanname, photo, bio, birthDate, birthPlace)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
");
|
|
$stmt->execute([
|
|
$member['name'] ?? null,
|
|
$cleanname,
|
|
$member['photo'] ?? null,
|
|
$member['bio'] ?? null,
|
|
$member['birthDate'] ?? null,
|
|
$member['birthPlace'] ?? null
|
|
]);
|
|
$castId = $this->pdo->lastInsertId();
|
|
|
|
if (isset($member['occupations']) && is_array($member['occupations'])) {
|
|
$this->saveRelatedItems('occupations', $castId, $member['occupations'], 'cast_id');
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($castId) {
|
|
$stmt = $this->pdo->prepare("
|
|
INSERT INTO media_cast (media_id, cast_id, role, characterName, characterImage)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
");
|
|
$stmt->execute([
|
|
$mediaId,
|
|
$castId,
|
|
$member['role'] ?? null,
|
|
$member['characterName'] ?? null,
|
|
$member['characterImage'] ?? null
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|