Files
mystuff_backend/api/models/Media.php
Lars Behrends e38a6e1f7b Add strict types and type hints across API
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.
2026-04-16 16:40:31 +02:00

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
]);
}
}
}
}