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.
393 lines
14 KiB
PHP
393 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
|
|
class Database {
|
|
private ?PDO $pdo;
|
|
|
|
public function __construct() {
|
|
try {
|
|
$dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4';
|
|
$this->pdo = new PDO($dsn, DB_USER, DB_PASS);
|
|
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
|
$this->initializeTables();
|
|
} catch (PDOException $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['success' => false, 'error' => 'Database connection failed: ' . $e->getMessage()]);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
private function initializeTables(): void {
|
|
// Media-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS media (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
title TEXT NOT NULL,
|
|
cleanname TEXT,
|
|
year TEXT,
|
|
poster TEXT,
|
|
banner TEXT,
|
|
description TEXT,
|
|
rating FLOAT,
|
|
category TEXT,
|
|
type TEXT,
|
|
status TEXT,
|
|
aspectRatio TEXT,
|
|
runtime INT,
|
|
director TEXT,
|
|
writer TEXT,
|
|
releaseDate TEXT,
|
|
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
)
|
|
");
|
|
|
|
// cleanname Feld zu bestehender Tabelle hinzufügen, falls nicht vorhanden
|
|
try {
|
|
$this->pdo->exec("ALTER TABLE media ADD COLUMN cleanname TEXT");
|
|
} catch (Exception $e) {
|
|
// Feld existiert bereits
|
|
}
|
|
|
|
// Index für cleanname erstellen
|
|
try {
|
|
$this->pdo->exec("CREATE INDEX idx_media_cleanname ON media(cleanname)");
|
|
} catch (Exception $e) {
|
|
// Index existiert bereits
|
|
}
|
|
|
|
// Genres-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS genres (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_id INT,
|
|
genre TEXT,
|
|
FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Tags-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS tags (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_id INT,
|
|
tag TEXT,
|
|
FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Studios-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS studios (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_id INT,
|
|
studio TEXT,
|
|
FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Cast/Staff-Tabelle (Stammdaten - ohne media_id)
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS cast_staff (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
cleanname TEXT,
|
|
photo TEXT,
|
|
bio TEXT,
|
|
birthDate TEXT,
|
|
birthPlace TEXT,
|
|
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
)
|
|
");
|
|
|
|
// cleanname Feld zu bestehender Tabelle hinzufügen, falls nicht vorhanden
|
|
try {
|
|
$this->pdo->exec("ALTER TABLE cast_staff ADD COLUMN cleanname TEXT");
|
|
} catch (Exception $e) {
|
|
// Feld existiert bereits
|
|
}
|
|
|
|
// Index für cleanname erstellen
|
|
try {
|
|
$this->pdo->exec("CREATE INDEX idx_cast_staff_cleanname ON cast_staff(cleanname)");
|
|
} catch (Exception $e) {
|
|
// Index existiert bereits
|
|
}
|
|
|
|
// n:m Beziehung zwischen Media und Cast
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS media_cast (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_id INT,
|
|
cast_id INT,
|
|
role TEXT,
|
|
characterName TEXT,
|
|
characterImage TEXT,
|
|
FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (cast_id) REFERENCES cast_staff(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Occupations-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS occupations (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
cast_id INT,
|
|
occupation TEXT,
|
|
FOREIGN KEY (cast_id) REFERENCES cast_staff(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Episodes-Tabelle (für Series)
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS episodes (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_id INT,
|
|
season INT,
|
|
episode_number INT,
|
|
title TEXT,
|
|
description TEXT,
|
|
air_date TEXT,
|
|
duration INT,
|
|
thumbnail TEXT,
|
|
FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Tracks-Tabelle (für Music/Albums)
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS tracks (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_id INT,
|
|
track_number INT,
|
|
title TEXT,
|
|
duration INT,
|
|
artist TEXT,
|
|
FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Adult-spezifische Cast-Infos
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS adult_cast_specifics (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
cast_id INT UNIQUE,
|
|
bust_size TEXT,
|
|
cup_size TEXT,
|
|
waist_size TEXT,
|
|
hip_size TEXT,
|
|
height INT,
|
|
weight INT,
|
|
hair_color TEXT,
|
|
eye_color TEXT,
|
|
ethnicity TEXT,
|
|
tattoos TEXT,
|
|
piercings TEXT,
|
|
measurements TEXT,
|
|
shoe_size TEXT,
|
|
FOREIGN KEY (cast_id) REFERENCES cast_staff(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Media-Games Tabelle (1:1 Relation zu media für spiel-spezifische Daten)
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS media_games (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_id INT UNIQUE,
|
|
sortingName TEXT,
|
|
notes TEXT,
|
|
completionStatus TEXT,
|
|
source TEXT,
|
|
gameId TEXT,
|
|
pluginId TEXT,
|
|
isInstalled BOOLEAN DEFAULT 0,
|
|
installDirectory TEXT,
|
|
installSize BIGINT DEFAULT 0,
|
|
hidden BOOLEAN DEFAULT 0,
|
|
favorite BOOLEAN DEFAULT 0,
|
|
playCount INT DEFAULT 0,
|
|
lastActivity TIMESTAMP NULL,
|
|
added TIMESTAMP NULL,
|
|
modified TIMESTAMP NULL,
|
|
communityScore INT DEFAULT 0,
|
|
criticScore INT DEFAULT 0,
|
|
userScore INT DEFAULT 0,
|
|
hasIcon BOOLEAN DEFAULT 0,
|
|
hasCover BOOLEAN DEFAULT 0,
|
|
hasBackground BOOLEAN DEFAULT 0,
|
|
version TEXT,
|
|
playtime INT DEFAULT 0,
|
|
FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Achievements-Tabelle (für Games - referenziert media_games)
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS achievements (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_game_id INT,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
icon TEXT,
|
|
unlocked BOOLEAN DEFAULT 0,
|
|
unlocked_date TIMESTAMP NULL,
|
|
FOREIGN KEY (media_game_id) REFERENCES media_games(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Game Categories-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS game_categories (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_game_id INT,
|
|
category TEXT,
|
|
FOREIGN KEY (media_game_id) REFERENCES media_games(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Game Features-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS game_features (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_game_id INT,
|
|
feature TEXT,
|
|
FOREIGN KEY (media_game_id) REFERENCES media_games(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Game Platforms-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS game_platforms (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_game_id INT,
|
|
platform TEXT,
|
|
FOREIGN KEY (media_game_id) REFERENCES media_games(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Game Developers-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS game_developers (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_game_id INT,
|
|
developer TEXT,
|
|
FOREIGN KEY (media_game_id) REFERENCES media_games(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Game Publishers-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS game_publishers (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_game_id INT,
|
|
publisher TEXT,
|
|
FOREIGN KEY (media_game_id) REFERENCES media_games(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Game Series-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS game_series (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_game_id INT,
|
|
series TEXT,
|
|
FOREIGN KEY (media_game_id) REFERENCES media_games(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Game Age Ratings-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS game_age_ratings (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_game_id INT,
|
|
age_rating TEXT,
|
|
FOREIGN KEY (media_game_id) REFERENCES media_games(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Game Regions-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS game_regions (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_game_id INT,
|
|
region TEXT,
|
|
FOREIGN KEY (media_game_id) REFERENCES media_games(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// Game Links-Tabelle (mit name und url)
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS game_links (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
media_game_id INT,
|
|
name TEXT,
|
|
url TEXT,
|
|
FOREIGN KEY (media_game_id) REFERENCES media_games(id) ON DELETE CASCADE
|
|
)
|
|
");
|
|
|
|
// API Logs-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS api_logs (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
type TEXT NOT NULL,
|
|
method TEXT,
|
|
path TEXT,
|
|
params JSON,
|
|
body JSON,
|
|
status_code INT,
|
|
response JSON,
|
|
error TEXT,
|
|
INDEX idx_timestamp (timestamp),
|
|
INDEX idx_type (type)
|
|
)
|
|
");
|
|
|
|
// Settings-Tabelle
|
|
$this->pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
enabled_categories JSON,
|
|
items_per_page INT DEFAULT 20,
|
|
default_view TEXT DEFAULT 'grid',
|
|
show_adult_content BOOLEAN DEFAULT 0,
|
|
auto_play_trailers BOOLEAN DEFAULT 0,
|
|
language TEXT DEFAULT 'en',
|
|
theme TEXT DEFAULT 'system',
|
|
jellyfin_library_mappings JSON,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
)
|
|
");
|
|
|
|
// Bestehende Einträge mit cleanname aktualisieren
|
|
$this->updateExistingCleanNames();
|
|
}
|
|
|
|
private function updateExistingCleanNames(): void {
|
|
// Media cleanname aktualisieren
|
|
$this->pdo->exec("
|
|
UPDATE media
|
|
SET cleanname = LOWER(REPLACE(REPLACE(REPLACE(REPLACE(title, ' ', '-'), '.', '-'), ',', '-'), '--', '-'))
|
|
WHERE cleanname IS NULL OR cleanname = ''
|
|
");
|
|
|
|
// Cast cleanname aktualisieren
|
|
$this->pdo->exec("
|
|
UPDATE cast_staff
|
|
SET cleanname = LOWER(REPLACE(REPLACE(REPLACE(REPLACE(name, ' ', '-'), '.', '-'), ',', '-'), '--', '-'))
|
|
WHERE cleanname IS NULL OR cleanname = ''
|
|
");
|
|
}
|
|
|
|
public function getConnection(): PDO {
|
|
return $this->pdo;
|
|
}
|
|
}
|