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.
212 lines
7.6 KiB
PHP
212 lines
7.6 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/BaseModel.php';
|
|
require_once __DIR__ . '/../services/ImageHandler.php';
|
|
|
|
class Cast extends BaseModel {
|
|
protected $table = 'cast_staff';
|
|
private $imageHandler;
|
|
protected $isUpdate = false;
|
|
protected $castId = null;
|
|
|
|
public function __construct($pdo) {
|
|
parent::__construct($pdo);
|
|
$this->imageHandler = new ImageHandler();
|
|
}
|
|
|
|
public function getWithFilmography($id) {
|
|
$cast = $this->findById($id);
|
|
if (!$cast) {
|
|
return null;
|
|
}
|
|
|
|
$cast['occupations'] = $this->getRelatedItems('occupations', $id, 'cast_id');
|
|
$cast['filmography'] = $this->getMediaForCast($id);
|
|
|
|
return $cast;
|
|
}
|
|
|
|
public function getMediaForCast($castId) {
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT m.id, m.title, m.year, m.poster, m.category, m.type, mc.role, mc.characterName
|
|
FROM media m
|
|
INNER JOIN media_cast mc ON m.id = mc.media_id
|
|
WHERE mc.cast_id = ?
|
|
ORDER BY m.year DESC
|
|
");
|
|
$stmt->execute([$castId]);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
public function search($filters = [], $page = 1, $limit = 20) {
|
|
$conditions = [];
|
|
|
|
if (isset($filters['search'])) {
|
|
$conditions['name'] = ["%" . $filters['search'] . "%"];
|
|
}
|
|
|
|
$offset = ($page - 1) * $limit;
|
|
$items = $this->findAll($conditions, 'createdAt DESC', $limit, $offset);
|
|
$total = $this->count($conditions);
|
|
|
|
// Load filmography for all cast members in a single query
|
|
if (!empty($items)) {
|
|
$castIds = array_column($items, 'id');
|
|
$placeholders = str_repeat('?,', count($castIds) - 1) . '?';
|
|
|
|
$stmt = $this->pdo->prepare("
|
|
SELECT mc.cast_id, m.id, m.title, m.year, m.poster, m.category, m.type, mc.role, mc.characterName
|
|
FROM media m
|
|
INNER JOIN media_cast mc ON m.id = mc.media_id
|
|
WHERE mc.cast_id IN ($placeholders)
|
|
ORDER BY m.year DESC
|
|
");
|
|
$stmt->execute($castIds);
|
|
$allFilmography = $stmt->fetchAll();
|
|
|
|
// Group filmography by cast_id
|
|
$filmographyByCast = [];
|
|
foreach ($allFilmography as $film) {
|
|
$castId = $film['cast_id'];
|
|
unset($film['cast_id']);
|
|
$filmographyByCast[$castId][] = $film;
|
|
}
|
|
|
|
// Attach filmography to each cast member
|
|
foreach ($items as &$item) {
|
|
$item['filmography'] = $filmographyByCast[$item['id']] ?? [];
|
|
$mediaTypes = array_unique(array_column($item['filmography'], 'category'));
|
|
$item['media_types'] = array_values($mediaTypes);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'items' => $items,
|
|
'total' => $total,
|
|
'page' => $page,
|
|
'limit' => $limit
|
|
];
|
|
}
|
|
|
|
protected function processPhotoField($data) {
|
|
error_log("Cast::processPhotoField - Checking for photo field, isUpdate: " . ($this->isUpdate ? 'yes' : 'no'));
|
|
|
|
if ($this->isUpdate && $this->castId && isset($data['photo']) && !empty($data['photo'])) {
|
|
$currentCast = $this->findById($this->castId);
|
|
if ($currentCast && !empty($currentCast['photo'])) {
|
|
$oldPhoto = $currentCast['photo'];
|
|
if (strpos($oldPhoto, '/images/') === 0) {
|
|
error_log("Cast::processPhotoField - Deleting old photo: " . $oldPhoto);
|
|
$this->imageHandler->deleteImage($oldPhoto);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($data['photo']) && !empty($data['photo'])) {
|
|
error_log("Cast::processPhotoField - Photo found, length: " . strlen($data['photo']));
|
|
|
|
if (strpos($data['photo'], '/images/') === 0 || filter_var($data['photo'], FILTER_VALIDATE_URL)) {
|
|
error_log("Cast::processPhotoField - Photo is already a path or URL, skipping processing");
|
|
return $data;
|
|
}
|
|
|
|
$photoPath = $this->imageHandler->saveBase64Image($data['photo'], 'cast/photo');
|
|
error_log("Cast::processPhotoField - ImageHandler returned: " . ($photoPath ?: 'null'));
|
|
if ($photoPath) {
|
|
$data['photo'] = $photoPath;
|
|
error_log("Cast::processPhotoField - Photo path set to: " . $photoPath);
|
|
} else {
|
|
error_log("Cast::processPhotoField - Failed to process photo, keeping original data");
|
|
}
|
|
} else {
|
|
error_log("Cast::processPhotoField - No photo field found or empty");
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
public function createWithOccupations($data) {
|
|
$name = $data['name'] ?? null;
|
|
if (!$name) {
|
|
throw new Exception('Name is required');
|
|
}
|
|
|
|
// cleanname generieren
|
|
$cleanname = generateCleanName($name);
|
|
|
|
$data = $this->processPhotoField($data);
|
|
|
|
$castData = [
|
|
'name' => $name,
|
|
'cleanname' => $cleanname,
|
|
'photo' => $data['photo'] ?? null,
|
|
'bio' => $data['bio'] ?? null,
|
|
'birthDate' => $data['birthDate'] ?? null,
|
|
'birthPlace' => $data['birthPlace'] ?? null
|
|
];
|
|
|
|
$castId = $this->create($castData);
|
|
|
|
if (isset($data['occupations']) && is_array($data['occupations'])) {
|
|
$this->saveRelatedItems('occupations', $castId, $data['occupations'], 'cast_id');
|
|
}
|
|
|
|
return $castId;
|
|
}
|
|
|
|
public function updateWithOccupations($id, $data) {
|
|
$this->isUpdate = true;
|
|
$this->castId = $id;
|
|
|
|
$data = $this->processPhotoField($data);
|
|
|
|
$castData = [];
|
|
|
|
foreach (['name', 'photo', 'bio', 'birthDate', 'birthPlace'] as $field) {
|
|
if (array_key_exists($field, $data)) {
|
|
$castData[$field] = $data[$field];
|
|
}
|
|
}
|
|
|
|
// Wenn name geändert wurde, cleanname aktualisieren
|
|
if (isset($data['name'])) {
|
|
$castData['cleanname'] = generateCleanName($data['name']);
|
|
}
|
|
|
|
if (!empty($castData)) {
|
|
$this->update($id, $castData);
|
|
}
|
|
|
|
if (isset($data['occupations']) && is_array($data['occupations'])) {
|
|
$this->pdo->prepare("DELETE FROM occupations WHERE cast_id = ?")->execute([$id]);
|
|
$this->saveRelatedItems('occupations', $id, $data['occupations'], 'cast_id');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function findByCleanName($cleanname) {
|
|
$stmt = $this->pdo->prepare("SELECT * FROM {$this->table} WHERE cleanname = ?");
|
|
$stmt->execute([$cleanname]);
|
|
return $stmt->fetch();
|
|
}
|
|
|
|
protected function getRelatedItems($table, $id, $fkColumn = 'media_id') {
|
|
$stmt = $this->pdo->prepare("SELECT * FROM $table WHERE $fkColumn = ?");
|
|
$stmt->execute([$id]);
|
|
$items = $stmt->fetchAll();
|
|
$result = [];
|
|
foreach ($items as $item) {
|
|
$result[] = $fkColumn === 'cast_id' ? $item['occupation'] : $item['genre'];
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
protected function saveRelatedItems($table, $id, $items, $fkColumn = 'media_id') {
|
|
$valueColumn = $fkColumn === 'cast_id' ? 'occupation' : 'genre';
|
|
$stmt = $this->pdo->prepare("INSERT INTO $table ($fkColumn, $valueColumn) VALUES (?, ?)");
|
|
foreach ($items as $item) {
|
|
$stmt->execute([$id, $item]);
|
|
}
|
|
}
|
|
}
|