false,'error'=>$msg], $code); } $dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', DATABASE_HOST, DATABASE_NAME); try { $pdo = new PDO($dsn, DATABASE_USER, DATABASE_PASSWORD, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); } catch (Throwable $e) { if (MM_DEBUG) fail('DB connection failed: '.$e->getMessage(), 500); fail('DB connection failed', 500); } // Shared getter so IGDB helpers can reuse the PDO instance function getDb(): PDO { global $pdo; return $pdo; } // Shared game helpers (usable for both GET and POST actions) function saveGameStatus(int $igdbId, string $name, ?string $originalName, int $status, ?string $note = null, ?string $coverUrl = null): array { $pdo = getDb(); $hasCover = gameHasColumn('cover_url'); $hasReleaseYear = gameHasColumn('release_year'); $hasJson = gameHasColumn('json'); $releaseYear = $GLOBALS['__release_year'] ?? null; $gameJson = $GLOBALS['__game_json'] ?? null; $fields = ['igdb_id', 'name', 'original_name', 'status', 'note']; if ($hasCover) $fields[] = 'cover_url'; if ($hasReleaseYear) $fields[] = 'release_year'; if ($hasJson) $fields[] = 'json'; $placeholders = array_map(fn($f) => ':'.$f, $fields); $updates = []; foreach ($fields as $f) { if ($f === 'igdb_id') continue; $updates[] = "$f = VALUES($f)"; } $sql = sprintf( 'INSERT INTO game (%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s', implode(', ', $fields), implode(', ', $placeholders), implode(', ', $updates) . ', updated_at = CURRENT_TIMESTAMP' ); $stmt = $pdo->prepare($sql); $params = []; foreach ($fields as $f) { switch ($f) { case 'igdb_id': $params[':igdb_id'] = $igdbId; break; case 'name': $params[':name'] = $name; break; case 'original_name': $params[':original_name'] = $originalName; break; case 'status': $params[':status'] = $status; break; case 'note': $params[':note'] = $note; break; case 'cover_url': $params[':cover_url'] = $coverUrl; break; case 'release_year': $params[':release_year'] = $releaseYear; break; case 'json': $params[':json'] = $gameJson; break; } } $stmt->execute($params); $stmt = $pdo->prepare('SELECT * FROM game WHERE igdb_id = :id LIMIT 1'); $stmt->execute([':id' => $igdbId]); return $stmt->fetch() ?: []; } function getGameByIgdbId(int $igdbId): ?array { $pdo = getDb(); $stmt = $pdo->prepare('SELECT * FROM game WHERE igdb_id = :id LIMIT 1'); $stmt->execute([':id' => $igdbId]); return $stmt->fetch() ?: null; } function gameColumns(): array { static $cols = null; if ($cols !== null) return $cols; $cols = []; try { $pdo = getDb(); $stmt = $pdo->query('DESCRIBE game'); foreach ($stmt as $row) { if (isset($row['Field'])) $cols[] = $row['Field']; } } catch (Throwable $e) { $cols = []; } return $cols; } function gameHasColumn(string $col): bool { return in_array($col, gameColumns(), true); } function getLocalization(int $igdbId, string $lang = 'de'): ?array { $pdo = getDb(); $stmt = $pdo->prepare('SELECT * FROM igdb_localizations WHERE igdb_id = :id AND lang = :lang LIMIT 1'); $stmt->execute([ ':id' => $igdbId, ':lang' => $lang, ]); $row = $stmt->fetch(); return $row ?: null; } function saveLocalization(int $igdbId, string $lang, ?string $title, ?string $summary, ?string $storyline, ?int $userId, ?int $locked = null): array { $pdo = getDb(); $existing = getLocalization($igdbId, $lang); $lockedVal = $locked === null ? (int)($existing['locked'] ?? 0) : (int)!!$locked; $titleToSave = $title !== null ? $title : ($existing['title'] ?? null); $summaryToSave = $summary !== null ? $summary : ($existing['summary'] ?? null); $storyToSave = $storyline !== null ? $storyline : ($existing['storyline'] ?? null); $sql = ' INSERT INTO igdb_localizations (igdb_id, lang, title, summary, storyline, user_id, locked) VALUES (:id, :lang, :title, :summary, :storyline, :user_id, :locked) ON DUPLICATE KEY UPDATE title = VALUES(title), summary = VALUES(summary), storyline = VALUES(storyline), user_id = VALUES(user_id), locked = VALUES(locked), updated_at = CURRENT_TIMESTAMP '; $stmt = $pdo->prepare($sql); $stmt->execute([ ':id' => $igdbId, ':lang' => $lang, ':title' => $titleToSave, ':summary' => $summaryToSave, ':storyline' => $storyToSave, ':user_id' => $userId, ':locked' => $lockedVal, ]); return getLocalization($igdbId, $lang); } // ===================================================== // IGDB + Wikipedia (GET-basiert) // ===================================================== if (isset($_GET['action'])) { // Helper nur für diesen Block, damit die bestehenden resp/fail unangetastet bleiben function respond($data, int $status = 200): void { http_response_code($status); echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); exit; } function error_response($msg, int $status = 400): void { if (is_string($msg)) { $msg = ['error' => $msg]; } respond($msg, $status); } if (!defined('WIKI_DE_API_URL')) { define('WIKI_DE_API_URL', 'https://de.wikipedia.org/w/api.php'); } if (!defined('WIKI_USER_AGENT')) { define('WIKI_USER_AGENT', 'MultimediaFlutter/1.0 (https://windesign.at; Herwig.Birke@windesign.at)'); } function igdbImageUrl(?string $imageId, string $sizeTag = 't_cover_big'): ?string { if (!$imageId) return null; return sprintf('https://images.igdb.com/igdb/image/upload/%s/%s.jpg', $sizeTag, $imageId); } function getCachedToken(): ?array { if (!defined('IGDB_TOKEN_CACHE_FILE') || !file_exists(IGDB_TOKEN_CACHE_FILE)) return null; $json = file_get_contents(IGDB_TOKEN_CACHE_FILE); $data = json_decode($json, true); if (!$data || time() >= ($data['expires_at'] ?? 0)) return null; return $data; } function saveToken(array $tokenData): void { if (!defined('IGDB_TOKEN_CACHE_FILE')) return; $expiresIn = (int)($tokenData['expires_in'] ?? 0); $tokenData['expires_at'] = time() + $expiresIn - 60; file_put_contents(IGDB_TOKEN_CACHE_FILE, json_encode($tokenData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } function fetchNewToken(): array { $postFields = [ 'client_id' => TWITCH_CLIENT_ID, 'client_secret' => TWITCH_CLIENT_SECRET, 'grant_type' => 'client_credentials', ]; $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => TWITCH_TOKEN_URL, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($postFields), CURLOPT_RETURNTRANSFER => true, ]); $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $data = json_decode($response, true); if ($status !== 200) { error_response(['message' => 'Failed to fetch token from Twitch', 'status' => $status, 'body' => $data], 500); } saveToken($data); return $data; } function getAccessToken(): string { $cached = getCachedToken(); if ($cached) return $cached['access_token']; $new = fetchNewToken(); return $new['access_token']; } function igdbRequest(string $endpoint, string $body) { $accessToken = getAccessToken(); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => IGDB_BASE_URL . $endpoint, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_HTTPHEADER => [ 'Client-ID: ' . TWITCH_CLIENT_ID, 'Authorization: Bearer ' . $accessToken, 'Accept: application/json', ], CURLOPT_RETURNTRANSFER => true, ]); $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $data = json_decode($response, true); if ($status !== 200) { error_response(['message' => 'IGDB returned error', 'status' => $status, 'body' => $data], 500); } return $data; } function wikiApiSearchWithExtract(string $searchQuery): ?array { $params = [ 'action' => 'query', 'generator' => 'search', 'gsrsearch' => $searchQuery, 'gsrlimit' => 1, 'prop' => 'extracts', 'exintro' => 1, 'explaintext' => 1, 'format' => 'json', 'utf8' => 1, ]; $url = WIKI_DE_API_URL . '?' . http_build_query($params, '', '&'); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_HTTPHEADER => [ 'User-Agent: ' . WIKI_USER_AGENT, ], ]); $response = curl_exec($ch); if ($response === false) { curl_close($ch); return null; } $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) return null; $data = json_decode($response, true); if (!is_array($data) || empty($data['query']['pages'])) return null; $pages = $data['query']['pages']; $page = reset($pages); if (empty($page['extract'])) return null; $page['debug_url'] = $url; return $page; } function wikiFetchGermanSummaryForTitle(string $gameTitle): ?array { $gameTitleTrimmed = trim($gameTitle); $patterns = [ $gameTitleTrimmed . ' (Computerspiel)', $gameTitleTrimmed . ' (Videospiel)', $gameTitleTrimmed . ' Videospiel', $gameTitleTrimmed, ]; foreach ($patterns as $pattern) { $page = wikiApiSearchWithExtract($pattern); if ($page === null || empty($page['extract'])) continue; $pageTitle = $page['title'] ?? $gameTitleTrimmed; $canonicalPageTitle = mb_strtolower(preg_replace('/\\s*\\(.*?\\)\\s*/u', '', $pageTitle), 'UTF-8'); $canonicalGameTitle = mb_strtolower($gameTitleTrimmed, 'UTF-8'); if (mb_strpos($canonicalPageTitle, $canonicalGameTitle) === false && mb_strpos($canonicalGameTitle, $canonicalPageTitle) === false) { continue; } $extract = $page['extract']; $short = mb_substr($extract, 0, 600, 'UTF-8'); return [ 'pageid' => (int)($page['pageid'] ?? 0), 'title' => $pageTitle, 'extract' => $short, 'url' => 'https://de.wikipedia.org/?curid=' . (int)($page['pageid'] ?? 0), 'debug_url' => $page['debug_url'] ?? null, ]; } return null; } $actionGet = $_GET['action'] ?? 'ping'; switch ($actionGet) { case 'ping': respond(['status' => 'ok']); break; case 'search': $query = $_GET['query'] ?? ''; $limit = (int)($_GET['limit'] ?? 10); $page = (int)($_GET['page'] ?? 1); $lang = $_GET['lang'] ?? 'en'; $checkExternal = isset($_GET['check_external']) ? ((int)$_GET['check_external'] === 1) : false; $debug = isset($_GET['debug']) ? ((int)$_GET['debug'] === 1) : false; if ($query === '') { error_response('Missing "query"', 400); } if ($limit < 1 || $limit > 50) { $limit = 10; } if ($page < 1) { $page = 1; } $offset = ($page - 1) * $limit; $safeQuery = str_replace('"', '\\"', $query); $body = sprintf( 'fields id,name,summary,first_release_date,cover.image_id; ' . 'limit %d; offset %d; search "%s";', $limit, $offset, $safeQuery ); $results = igdbRequest('/games', $body); foreach ($results as &$g) { // Cover-URL $g['cover_url'] = isset($g['cover']['image_id']) ? igdbImageUrl($g['cover']['image_id'], 't_cover_big') : null; // eigener Status aus game-Tabelle $localGame = getGameByIgdbId($g['id']); $g['my_status'] = $localGame['status'] ?? null; // eig. Lokalisierung aus DB (z.B. de) $hasLoc = false; if ($lang !== 'en') { $loc = getLocalization($g['id'], $lang); if ($loc !== null) { $hasLoc = true; if (!empty($loc['title'])) { $g['name'] = $loc['title']; } if (!empty($loc['summary'])) { $g['summary'] = $loc['summary']; } } } $g['has_localization'] = $hasLoc; // Wikipedia-Felder vorbereiten $g['has_external_de'] = false; $g['external_de_source'] = null; $g['external_de_summary'] = null; $g['external_de_url'] = null; if ($debug) { $g['wiki_debug'] = []; } // Wikipedia nur bei lang=de, wenn keine eigene Lokalisierung da ist if ($checkExternal && $lang === 'de' && !$hasLoc) { $wiki = wikiFetchGermanSummaryForTitle($g['name']); if ($debug) { $g['wiki_debug']['used_title'] = $g['name']; $g['wiki_debug']['wiki_result'] = $wiki; } if ($wiki !== null && !empty($wiki['extract'])) { $g['has_external_de'] = true; $g['external_de_source'] = 'wikipedia'; $g['external_de_summary'] = $wiki['extract']; $g['external_de_url'] = $wiki['url']; // Direkt als Summary verwenden (Qualitäts-Boost) $g['summary'] = $wiki['extract']; // OPTIONAL: den Titel auf Wikipedia-Titel setzen: // $g['name'] = $wiki['title']; } } } unset($g); respond([ 'source' => 'igdb', 'mode' => 'search', 'lang' => $lang, 'query' => $query, 'page' => $page, 'limit' => $limit, 'offset' => $offset, 'results_count' => count($results), 'results' => $results, ]); break; case 'details': $idParam = $_GET['id'] ?? ''; $lang = $_GET['lang'] ?? 'en'; $checkExternal = isset($_GET['check_external']) ? ((int)$_GET['check_external'] === 1) : false; if ($idParam === '') error_response('Missing \"id\"', 400); $id = (int)$idParam; if ($id <= 0) error_response('Invalid \"id\"', 400); $body = sprintf( 'fields id,name,summary,storyline,first_release_date,genres.name,platforms.name,cover.image_id,screenshots.image_id,' .'involved_companies.company.name,involved_companies.developer,involved_companies.publisher,' .'websites.url,websites.category,age_ratings.rating,age_ratings.category,language_supports.language,' .'game_modes.name,themes.name,player_perspectives.name,game_engines.name; where id = %d;', $id ); $results = igdbRequest('/games', $body); if (empty($results)) error_response('Game not found', 404); $game = $results[0]; $game['cover_url'] = isset($game['cover']['image_id']) ? igdbImageUrl($game['cover']['image_id'], 't_cover_big') : null; $game['screenshot_urls'] = []; if (!empty($game['screenshots']) && is_array($game['screenshots'])) { foreach ($game['screenshots'] as $s) { if (!empty($s['image_id'])) $game['screenshot_urls'][] = igdbImageUrl($s['image_id'], 't_screenshot_big'); } } $localGame = getGameByIgdbId($id); if ($localGame !== null) { $game['my_status'] = (int)$localGame['status']; $game['my_note'] = $localGame['note']; } else { $game['my_status'] = null; $game['my_note'] = null; } $hasLoc = false; $locLocked = 0; if ($lang !== 'en') { $loc = getLocalization($id, $lang); if ($loc !== null) { $locLocked = (int)($loc['locked'] ?? 0); if (!empty($loc['title'])) $game['name'] = $loc['title']; if (!empty($loc['summary'])) $game['summary'] = $loc['summary']; if (!empty($loc['storyline'])) $game['storyline'] = $loc['storyline']; $game['localization'] = [ 'lang' => $lang, 'source' => 'custom', 'id' => $loc['id'], 'locked' => $locLocked, ]; if ($locLocked === 1) { $hasLoc = true; // locked -> treat as authoritative, skip external fallback } } } $game['has_localization'] = $hasLoc; $game['loc_locked'] = $locLocked; $game['has_external_de'] = false; $game['external_de_source']= null; $game['external_de_summary']= null; $game['external_de_url'] = null; if ($checkExternal && $lang === 'de' && $locLocked === 0) { $wiki = wikiFetchGermanSummaryForTitle($game['name']); if ($wiki !== null && !empty($wiki['extract'])) { $game['has_external_de'] = true; $game['external_de_source'] = 'wikipedia'; $game['external_de_summary'] = $wiki['extract']; $game['external_de_url'] = $wiki['url']; $game['summary'] = $wiki['extract']; } } respond([ 'source' => 'igdb', 'mode' => 'details', 'lang' => $lang, 'id' => $id, 'game' => $game, ]); break; case 'get_localization': $idParam = $_GET['id'] ?? ''; $lang = $_GET['lang'] ?? 'de'; if ($idParam === '') error_response('Missing \"id\"', 400); $igdbId = (int)$idParam; if ($igdbId <= 0) error_response('Invalid \"id\"', 400); $loc = getLocalization($igdbId, $lang); respond(['found' => (bool)$loc, 'localization' => $loc]); break; case 'submit_localization': if ($_SERVER['REQUEST_METHOD'] !== 'POST') error_response('Use POST', 405); $raw = file_get_contents('php://input'); $data = json_decode($raw, true); if (!is_array($data)) error_response('Invalid JSON body', 400); $igdbId = (int)($data['igdb_id'] ?? 0); $lang = trim($data['lang'] ?? 'de'); $title = isset($data['title']) ? trim($data['title']) : ''; $summary= isset($data['summary']) ? trim($data['summary']) : ''; $story = isset($data['storyline']) ? trim($data['storyline']) : ''; $userId = isset($data['user_id']) ? (int)$data['user_id'] : null; if ($igdbId <= 0) error_response('Invalid \"igdb_id\"', 400); if ($lang === '') { $lang = 'de'; } $loc = saveLocalization($igdbId, $lang, $title ?: null, $summary ?: null, $story ?: null, $userId); respond(['success' => true, 'localization' => $loc]); break; case 'set_game_status': if ($_SERVER['REQUEST_METHOD'] !== 'POST') error_response('Use POST', 405); $raw = file_get_contents('php://input'); $data = json_decode($raw, true); if (!is_array($data)) error_response('Invalid JSON body', 400); $igdbId = (int)($data['igdb_id'] ?? 0); $name = isset($data['name']) ? trim($data['name']) : ''; $orig = isset($data['original_name']) ? trim($data['original_name']) : ''; $status = (int)($data['status'] ?? 0); $note = isset($data['note']) ? trim($data['note']) : ''; $coverUrl = isset($data['cover_url']) ? trim($data['cover_url']) : null; $releaseYear = isset($data['release_year']) ? (int)$data['release_year'] : null; $GLOBALS['__release_year'] = $releaseYear; if ($igdbId <= 0) error_response('Invalid \"igdb_id\"', 400); if ($name === '') error_response('Missing \"name\"', 400); if ($status < 0 || $status > 2) error_response('Invalid \"status\" (0, 1, 2 allowed)', 400); $game = saveGameStatus($igdbId, $name, $orig !== '' ? $orig : null, $status, $note !== '' ? $note : null, $coverUrl ?: null); respond(['success' => true, 'game' => $game]); break; case 'delete_game': if ($_SERVER['REQUEST_METHOD'] !== 'POST') error_response('Use POST', 405); $igdbId = (int)($in['igdb_id'] ?? 0); if ($igdbId <= 0) fail('Invalid \"igdb_id\"'); $pdo->beginTransaction(); try { // remove localizations first $stmt = $pdo->prepare('DELETE FROM igdb_localizations WHERE igdb_id = ?'); $stmt->execute([$igdbId]); // remove game entry $stmt = $pdo->prepare('DELETE FROM game WHERE igdb_id = ?'); $stmt->execute([$igdbId]); $pdo->commit(); resp(['ok' => true]); } catch (Throwable $e) { $pdo->rollBack(); if (MM_DEBUG) fail('delete failed: '.$e->getMessage(), 500); fail('delete failed', 500); } break; case 'get_game': $idParam = $_GET['igdb_id'] ?? ''; if ($idParam === '') error_response('Missing \"igdb_id\"', 400); $igdbId = (int)$idParam; if ($igdbId <= 0) error_response('Invalid \"igdb_id\"', 400); $game = getGameByIgdbId($igdbId); respond(['found' => (bool)$game, 'game' => $game]); break; case 'delete_game': $idParam = $_GET['igdb_id'] ?? ''; $igdbId = (int)$idParam; if ($igdbId <= 0) error_response('Invalid \"igdb_id\"', 400); $pdo->beginTransaction(); try { $stmt = $pdo->prepare('DELETE FROM igdb_localizations WHERE igdb_id = ?'); $stmt->execute([$igdbId]); $stmt = $pdo->prepare('DELETE FROM game WHERE igdb_id = ?'); $stmt->execute([$igdbId]); $pdo->commit(); respond(['ok' => true]); } catch (Throwable $e) { $pdo->rollBack(); error_response('delete failed', 500); } break; case 'wiki_test': $title = $_GET['title'] ?? ''; if ($title === '') error_response('Missing \"title\"', 400); $params = [ 'action' => 'query', 'generator' => 'search', 'gsrsearch' => $title . ' Videospiel', 'gsrlimit' => 1, 'prop' => 'extracts', 'exintro' => 1, 'explaintext' => 1, 'format' => 'json', 'utf8' => 1, ]; $url = WIKI_DE_API_URL . '?' . http_build_query($params, '', '&'); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_HTTPHEADER => [ 'User-Agent: ' . WIKI_USER_AGENT, ], ]); $response = curl_exec($ch); $curlErr = curl_error($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $json = json_decode($response, true); respond([ 'title' => $title, 'used_url' => $url, 'http_status' => $status, 'curl_error' => $curlErr, 'raw_response' => $response, 'parsed_json' => $json, ]); break; default: error_response('Unknown action: ' . $actionGet, 404); } exit; } $in = inputBody(); $action = $in['action'] ?? null; try { switch ($action) { case 'upsert_show': { $tmdb = $in['tmdb'] ?? null; if (!$tmdb || !isset($tmdb['id'])) fail('missing tmdb payload'); try { $stmt = $pdo->prepare("INSERT INTO shows (tmdb_id, name, original_name, first_air_year, poster_path, backdrop_path, json) VALUES (?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name), original_name=VALUES(original_name), first_air_year=VALUES(first_air_year), poster_path=VALUES(poster_path), backdrop_path=VALUES(backdrop_path), json=VALUES(json)"); $stmt->execute([ $tmdb['id'], $tmdb['name'] ?? '', $tmdb['original_name'] ?? null, isset($tmdb['first_air_date']) ? intval(substr($tmdb['first_air_date'],0,4)) : null, $tmdb['poster_path'] ?? null, $tmdb['backdrop_path'] ?? null, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), ]); } catch (Throwable $e) { // Fallback for schemas without original_name/first_air_year if (strpos($e->getMessage(), 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) { $stmt = $pdo->prepare("INSERT INTO shows (tmdb_id, name, poster_path, backdrop_path, json) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name), poster_path=VALUES(poster_path), backdrop_path=VALUES(backdrop_path), json=VALUES(json)"); $stmt->execute([ $tmdb['id'], $tmdb['name'] ?? '', $tmdb['poster_path'] ?? null, $tmdb['backdrop_path'] ?? null, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), ]); } else { throw $e; } } $id = $pdo->lastInsertId(); if (!$id) { $q=$pdo->prepare('SELECT id FROM shows WHERE tmdb_id=?'); $q->execute([$tmdb['id']]); $id=$q->fetchColumn(); } resp(['ok'=>true,'id'=>(int)$id]); } case 'upsert_season': { $showId = (int)($in['show_id'] ?? 0); $tmdb = $in['tmdb'] ?? null; if (!$showId || !$tmdb) fail('bad params'); $seasonNo = isset($tmdb['season_number']) ? (int)$tmdb['season_number'] : null; if ($seasonNo===null) fail('missing season_number'); $name = $tmdb['name'] ?? (isset($seasonNo) ? ('Season '.$seasonNo) : ''); $airDate = $tmdb['air_date'] ?? null; try { $stmt = $pdo->prepare("INSERT INTO seasons (show_id, season_number, name, air_date, json) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name), air_date=VALUES(air_date), json=VALUES(json)"); $stmt->execute([$showId, $seasonNo, $name, $airDate, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)]); } catch (Throwable $e) { if (strpos($e->getMessage(), 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) { $stmt = $pdo->prepare("INSERT INTO seasons (show_id, season_number, name, json) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name), json=VALUES(json)"); $stmt->execute([$showId, $seasonNo, $name, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)]); } else { throw $e; } } $id = $pdo->lastInsertId(); if (!$id) { $q=$pdo->prepare('SELECT id FROM seasons WHERE show_id=? AND season_number=?'); $q->execute([$showId,$seasonNo]); $id=$q->fetchColumn(); } resp(['ok'=>true,'id'=>(int)$id]); } case 'upsert_episode': { $seasonId = (int)($in['season_id'] ?? 0); $tmdb = $in['tmdb'] ?? null; if (!$seasonId || !$tmdb) fail('bad params'); $epNo = isset($tmdb['episode_number']) ? (int)$tmdb['episode_number'] : null; if ($epNo===null) fail('missing episode_number'); $name = $tmdb['name'] ?? ('Episode '.$epNo); $runtime = isset($tmdb['runtime']) ? (int)$tmdb['runtime'] : null; $tmdbId = $tmdb['id'] ?? null; try { if ($tmdbId) { $stmt = $pdo->prepare("INSERT INTO episodes (season_id, tmdb_id, episode_number, name, runtime, json) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE episode_number=VALUES(episode_number), name=VALUES(name), runtime=VALUES(runtime), json=VALUES(json)"); $stmt->execute([$seasonId, $tmdbId, $epNo, $name, $runtime, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)]); } else { // No tmdb_id $stmt = $pdo->prepare("INSERT INTO episodes (season_id, episode_number, name, runtime, json) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name), runtime=VALUES(runtime), json=VALUES(json)"); $stmt->execute([$seasonId, $epNo, $name, $runtime, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)]); } } catch (Throwable $e) { // Fallback if tmdb_id column doesn't exist: always use (season_id, episode_number) if (strpos($e->getMessage(), 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) { $stmt = $pdo->prepare("INSERT INTO episodes (season_id, episode_number, name, runtime, json) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name), runtime=VALUES(runtime), json=VALUES(json)"); $stmt->execute([$seasonId, $epNo, $name, $runtime, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)]); } else { throw $e; } } $id = $pdo->lastInsertId(); if (!$id) { if ($tmdbId) { try { $q=$pdo->prepare('SELECT id FROM episodes WHERE tmdb_id=?'); $q->execute([$tmdbId]); $id=$q->fetchColumn(); } catch (Throwable $e) { if (strpos($e->getMessage(), 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) { $q=$pdo->prepare('SELECT id FROM episodes WHERE season_id=? AND episode_number=?'); $q->execute([$seasonId,$epNo]); $id=$q->fetchColumn(); } else { throw $e; } } } if (!$id) { $q=$pdo->prepare('SELECT id FROM episodes WHERE season_id=? AND episode_number=?'); $q->execute([$seasonId,$epNo]); $id=$q->fetchColumn(); } } resp(['ok'=>true,'id'=>(int)$id]); } case 'upsert_episodes_bulk': { $seasonId = (int)($in['season_id'] ?? 0); $episodes = $in['episodes'] ?? null; if (!$seasonId || !is_array($episodes)) fail('bad params'); // Detect if episodes table has tmdb_id column $hasTmdbId = false; try { $pdo->query('SELECT tmdb_id FROM episodes LIMIT 0'); $hasTmdbId = true; } catch (Throwable $e) { $hasTmdbId = false; } // Prepare statements $stmtWith = null; $stmtWithout = null; try { if ($hasTmdbId) { $stmtWith = $pdo->prepare("INSERT INTO episodes (season_id, tmdb_id, episode_number, name, runtime, json) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE episode_number=VALUES(episode_number), name=VALUES(name), runtime=VALUES(runtime), json=VALUES(json)"); } } catch (Throwable $_) { $hasTmdbId = false; $stmtWith = null; } try { $stmtWithout = $pdo->prepare("INSERT INTO episodes (season_id, episode_number, name, runtime, json) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name), runtime=VALUES(runtime), json=VALUES(json)"); } catch (Throwable $e) { fail('episodes table schema not supported: '.$e->getMessage(), 500); } $count = 0; $pdo->beginTransaction(); try { foreach ($episodes as $tmdb) { if (!is_array($tmdb)) continue; $epNo = isset($tmdb['episode_number']) ? (int)$tmdb['episode_number'] : null; if ($epNo === null) continue; $name = $tmdb['name'] ?? ('Episode '.$epNo); $runtime = isset($tmdb['runtime']) ? (int)$tmdb['runtime'] : null; $tmdbId = $tmdb['id'] ?? null; $json = json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); if ($hasTmdbId && $tmdbId) { try { $stmtWith->execute([$seasonId, $tmdbId, $epNo, $name, $runtime, $json]); $count++; continue; } catch (Throwable $_) { // Fall back to without tmdb_id } } $stmtWithout->execute([$seasonId, $epNo, $name, $runtime, $json]); $count++; } $pdo->commit(); } catch (Throwable $e) { $pdo->rollBack(); fail(MM_DEBUG ? ('bulk upsert failed: '.$e->getMessage()) : 'bulk upsert failed', 500); } resp(['ok'=>true, 'count'=>$count]); } case 'upsert_seasons_bulk': { $showId = (int)($in['show_id'] ?? 0); $seasons = $in['seasons'] ?? null; if (!$showId || !is_array($seasons)) fail('bad params'); $map = []; $count = 0; $pdo->beginTransaction(); try { // Prepare both statement variants to handle older schemas $stmtWith = null; $stmtWithout = null; try { $stmtWith = $pdo->prepare("INSERT INTO seasons (show_id, season_number, name, air_date, json) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name), air_date=VALUES(air_date), json=VALUES(json)"); } catch (Throwable $_) {} try { $stmtWithout = $pdo->prepare("INSERT INTO seasons (show_id, season_number, name, json) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name), json=VALUES(json)"); } catch (Throwable $e) { fail('seasons table schema not supported: '.$e->getMessage(), 500); } foreach ($seasons as $tmdb) { if (!is_array($tmdb)) continue; $seasonNo = isset($tmdb['season_number']) ? (int)$tmdb['season_number'] : null; if ($seasonNo === null) continue; if ($seasonNo < 0) continue; $name = $tmdb['name'] ?? ('Season '.$seasonNo); $airDate = $tmdb['air_date'] ?? null; $json = json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); $ok = false; if ($stmtWith) { try { $stmtWith->execute([$showId, $seasonNo, $name, $airDate, $json]); $ok = true; } catch (Throwable $_) { /* fallback below */ } } if (!$ok && $stmtWithout) { $stmtWithout->execute([$showId, $seasonNo, $name, $json]); } $count++; // Resolve ID $id = $pdo->lastInsertId(); if (!$id) { $q=$pdo->prepare('SELECT id FROM seasons WHERE show_id=? AND season_number=?'); $q->execute([$showId,$seasonNo]); $id=$q->fetchColumn(); } if ($id) $map[(int)$seasonNo] = (int)$id; } $pdo->commit(); } catch (Throwable $e) { $pdo->rollBack(); fail(MM_DEBUG ? ('bulk seasons upsert failed: '.$e->getMessage()) : 'bulk seasons upsert failed', 500); } resp(['ok'=>true, 'count'=>$count, 'map'=>$map]); } case 'get_capabilities': { // Advertise supported actions; clients can probe this to choose bulk paths $caps = [ 'upsert_episodes_bulk' => true, 'upsert_seasons_bulk' => true, ]; resp(['ok'=>true, 'capabilities'=>$caps]); } case 'upsert_movie': { $tmdb = $in['tmdb'] ?? null; if (!$tmdb || !isset($tmdb['id'])) fail('missing tmdb payload'); $stmt = $pdo->prepare("INSERT INTO movies (tmdb_id, title, original_title, release_year, poster_path, backdrop_path, runtime, json) VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE title=VALUES(title), original_title=VALUES(original_title), release_year=VALUES(release_year), poster_path=VALUES(poster_path), backdrop_path=VALUES(backdrop_path), runtime=VALUES(runtime), json=VALUES(json)"); $stmt->execute([ $tmdb['id'], $tmdb['title'] ?? $tmdb['name'] ?? '', $tmdb['original_title'] ?? $tmdb['original_name'] ?? null, isset($tmdb['release_date']) ? intval(substr($tmdb['release_date'],0,4)) : null, $tmdb['poster_path'] ?? null, $tmdb['backdrop_path'] ?? null, $tmdb['runtime'] ?? null, json_encode($tmdb, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), ]); $id = $pdo->lastInsertId(); if (!$id) { $q=$pdo->prepare('SELECT id FROM movies WHERE tmdb_id=?'); $q->execute([$tmdb['id']]); $id=$q->fetchColumn(); } resp(['ok'=>true,'id'=>(int)$id]); } case 'set_status': { $type=$in['type'] ?? null; $ref=(int)($in['ref_id'] ?? 0); $st=$in['status'] ?? null; // may be string or int if (!in_array($type,['movie','episode'],true) || !$ref || $st===null) fail('bad params'); // Map string statuses to int codes: 0 Init, 1 Progress, 2 Done $stInt = is_numeric($st) ? (int)$st : (function($s){ $s = strtolower((string)$s); if ($s==='progress') return 1; if ($s==='done') return 2; return 0; })($st); if ($type==='movie') { $stmt=$pdo->prepare("UPDATE movies SET status=? WHERE id=?"); $stmt->execute([$stInt,$ref]); } else { $stmt=$pdo->prepare("UPDATE episodes SET status=? WHERE id=?"); $stmt->execute([$stInt,$ref]); } resp(['ok'=>true]); } case 'set_movie_resolution': { $movieId = (int)($in['movie_id'] ?? 0); $res = $in['resolution'] ?? null; if (!$movieId || !$res) fail('bad params'); $stmt = $pdo->prepare('UPDATE movies SET resolution = ? WHERE id = ?'); $stmt->execute([$res, $movieId]); resp(['ok' => true]); } case 'get_list': { $type=$in['type'] ?? 'movie'; $status=$in['status'] ?? null; $lang=$in['lang'] ?? 'de'; $q=$in['q'] ?? ''; $limit=max(1,(int)($in['limit']??50)); $offset=max(0,(int)($in['offset']??0)); if ($type === 'episode') { $statusVal = null; if ($status) { $s = strtolower($status); if ($s==='init') $statusVal = 0; elseif ($s==='progress') $statusVal = 1; elseif ($s==='done') $statusVal = 2; } $base = "FROM episodes e JOIN seasons se ON se.id = e.season_id JOIN shows sh ON sh.id = se.show_id WHERE 1=1"; // JSON-aware select (keine first_air_year-Spalte voraussetzen) $selectJson = "SELECT e.*, CASE e.status WHEN 1 THEN 'Progress' WHEN 2 THEN 'Done' ELSE 'Init' END AS status, sh.resolution AS resolution, sh.poster_path AS poster_path, sh.id AS show_id, sh.download_path AS download_path, JSON_UNQUOTE(JSON_EXTRACT(sh.json, '$.status')) AS show_status, sh.cliffhanger AS show_cliffhanger, sh.json AS show_json, se.season_number, sh.name AS show_name ".$base; // Fallback ohne JSON_EXTRACT (ältere MySQL-Versionen) $selectNoJson = "SELECT e.*, CASE e.status WHEN 1 THEN 'Progress' WHEN 2 THEN 'Done' ELSE 'Init' END AS status, sh.resolution AS resolution, sh.poster_path AS poster_path, sh.id AS show_id, sh.download_path AS download_path, sh.cliffhanger AS show_cliffhanger, sh.json AS show_json, se.season_number, sh.name AS show_name ".$base; $run = function(string $sql) use ($pdo, $statusVal, $q, $offset, $limit) { $params = []; if ($statusVal !== null) { $sql .= " AND e.status = ?"; $params[] = $statusVal; } if ($q) { $sql .= " AND e.name LIKE ?"; $params[] = "%$q%"; } $sql .= " ORDER BY sh.name ASC, se.season_number ASC, e.episode_number ASC LIMIT ?, ?"; $params[] = $offset; $params[] = $limit; $stmt = $pdo->prepare($sql); $i=1; foreach ($params as $p) { $stmt->bindValue($i++, $p, is_int($p)?PDO::PARAM_INT:PDO::PARAM_STR); } $stmt->execute(); return $stmt->fetchAll(); }; try { $rows = $run($selectJson); resp(['ok'=>true,'items'=>$rows]); } catch (Throwable $e) { $msg = $e->getMessage(); if (stripos($msg, 'JSON_EXTRACT') !== false || stripos($msg, 'Unknown function') !== false) { // JSON-Funktionen nicht verfügbar -> Fallback ohne JSON $rows = $run($selectNoJson); resp(['ok'=>true,'items'=>$rows]); } else { throw $e; } } } // Movie list handling if ($type==='movie') { $statusVal = null; if ($status) { $s = strtolower($status); if ($s==='init') $statusVal = 0; elseif ($s==='progress') $statusVal = 1; elseif ($s==='done') $statusVal = 2; } $sql = "SELECT m.*, CASE m.status WHEN 1 THEN 'Progress' WHEN 2 THEN 'Done' ELSE 'Init' END AS status, m.resolution FROM movies m WHERE 1=1"; $params=[]; if ($statusVal !== null) { $sql.=" AND m.status=?"; $params[]=$statusVal; } if ($q){$sql.=" AND m.title LIKE ?"; $params[]='%'.$q.'%';} $sql.=" ORDER BY m.title ASC LIMIT ?,?"; $params[]=$offset; $params[]=$limit; $stmt=$pdo->prepare($sql); $i=1; foreach($params as $p){$stmt->bindValue($i++,$p,is_int($p)?PDO::PARAM_INT:PDO::PARAM_STR);} $stmt->execute(); resp(['ok'=>true,'items'=>$stmt->fetchAll()]); } if ($type==='game') { $statusVal = null; if ($status) { $s = strtolower($status); if ($s==='init') $statusVal = 0; elseif ($s==='progress') $statusVal = 1; elseif ($s==='done') $statusVal = 2; } $hasRelease = gameHasColumn('release_year'); $hasCover = gameHasColumn('cover_url'); $sql = "SELECT g.*, ".($hasRelease ? 'g.release_year' : 'NULL AS release_year').", ".($hasCover ? 'g.cover_url' : 'NULL AS cover_url').", CASE g.status WHEN 1 THEN 'Progress' WHEN 2 THEN 'Done' ELSE 'Init' END AS status, loc.title AS loc_title, loc.summary AS loc_summary, loc.storyline AS loc_storyline, loc.locked AS loc_locked, g.json AS json FROM game g LEFT JOIN igdb_localizations loc ON loc.igdb_id = g.igdb_id AND loc.lang = ? WHERE 1=1"; $params = [$lang]; if ($statusVal !== null) { $sql .= " AND g.status=?"; $params[] = $statusVal; } if ($q) { $sql .= " AND g.name LIKE ?"; $params[] = '%'.$q.'%'; } $sql .= " ORDER BY g.name ASC LIMIT ?,?"; $params[] = $offset; $params[] = $limit; $stmt = $pdo->prepare($sql); $i = 1; foreach ($params as $p) { $stmt->bindValue($i++, $p, is_int($p)?PDO::PARAM_INT:PDO::PARAM_STR); } $stmt->execute(); resp(['ok'=>true,'items'=>$stmt->fetchAll()]); } fail('unsupported type'); } case 'get_series_summary': { // Optional: increase GROUP_CONCAT limit for very large shows try { $pdo->query('SET SESSION group_concat_max_len = 1048576'); } catch (Throwable $e) {} $sql = " SELECT sh.id AS show_id, sh.name, sh.poster_path, sh.resolution, sh.download_path, sh.cliffhanger, sh.json, sa.season_status, sa.seasons_eps, (af.progress_sum > 0) AS any_progress, (af.init_sum > 0) AS any_init FROM shows sh LEFT JOIN ( SELECT t.show_id, GROUP_CONCAT(CONCAT(t.season_number, ':', t.init_cnt, ',', t.prog_cnt, ',', t.done_cnt, ',', t.total_cnt) ORDER BY t.season_number SEPARATOR '|') AS season_status, GROUP_CONCAT(CONCAT(t.season_number, ':', t.eps_list) ORDER BY t.season_number SEPARATOR ';') AS seasons_eps FROM ( SELECT se.show_id, se.season_number, SUM(CASE WHEN e.status IS NULL OR e.status = 0 THEN 1 ELSE 0 END) AS init_cnt, SUM(CASE WHEN e.status = 1 THEN 1 ELSE 0 END) AS prog_cnt, SUM(CASE WHEN e.status = 2 THEN 1 ELSE 0 END) AS done_cnt, COUNT(e.id) AS total_cnt, GROUP_CONCAT(CONCAT(e.episode_number, '|', COALESCE(e.status,0)) ORDER BY e.episode_number SEPARATOR ',') AS eps_list FROM seasons se LEFT JOIN episodes e ON e.season_id = se.id WHERE se.season_number > 0 GROUP BY se.show_id, se.season_number ) AS t GROUP BY t.show_id ) AS sa ON sa.show_id = sh.id LEFT JOIN ( SELECT se.show_id, SUM(CASE WHEN e.status = 1 THEN 1 ELSE 0 END) AS progress_sum, SUM(CASE WHEN e.status IS NULL OR e.status = 0 THEN 1 ELSE 0 END) AS init_sum FROM seasons se LEFT JOIN episodes e ON e.season_id = se.id WHERE se.season_number > 0 GROUP BY se.show_id ) AS af ON af.show_id = sh.id ORDER BY sh.name ASC"; try { $stmt = $pdo->query($sql); $rows = $stmt->fetchAll(); resp(['ok' => true, 'items' => $rows]); } catch (Throwable $e) { // Fallback for legacy schemas using column name 'state' instead of 'status' $msg = $e->getMessage(); if (strpos($msg, 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) { $sql2 = str_replace(['e.status', 'COALESCE(e.status,0)'], ['e.state', 'COALESCE(e.state,0)'], $sql); $stmt = $pdo->query($sql2); $rows = $stmt->fetchAll(); resp(['ok' => true, 'items' => $rows]); } else { throw $e; } } } case 'get_show_episodes': { $showId = (int)($in['show_id'] ?? 0); if (!$showId) fail('bad params'); $base = "FROM episodes e JOIN seasons se ON se.id = e.season_id JOIN shows sh ON sh.id = se.show_id WHERE se.show_id = ? AND se.season_number > 0"; $selectJson = "SELECT e.*, CASE e.status WHEN 1 THEN 'Progress' WHEN 2 THEN 'Done' ELSE 'Init' END AS status, sh.resolution AS resolution, sh.poster_path AS poster_path, sh.id AS show_id, sh.download_path AS download_path, JSON_UNQUOTE(JSON_EXTRACT(sh.json, '$.status')) AS show_status, sh.cliffhanger AS show_cliffhanger, sh.json AS show_json, se.season_number, sh.name AS show_name ".$base. " ORDER BY se.season_number ASC, e.episode_number ASC"; $selectNoJson = "SELECT e.*, CASE e.status WHEN 1 THEN 'Progress' WHEN 2 THEN 'Done' ELSE 'Init' END AS status, sh.resolution AS resolution, sh.poster_path AS poster_path, sh.id AS show_id, sh.download_path AS download_path, sh.cliffhanger AS show_cliffhanger, sh.json AS show_json, se.season_number, sh.name AS show_name ".$base. " ORDER BY se.season_number ASC, e.episode_number ASC"; try { $stmt = $pdo->prepare($selectJson); $stmt->execute([$showId]); $rows = $stmt->fetchAll(); resp(['ok'=>true,'items'=>$rows]); } catch (Throwable $e) { $msg = $e->getMessage(); if (stripos($msg, 'JSON_EXTRACT') !== false || stripos($msg, 'Unknown function') !== false) { $stmt = $pdo->prepare($selectNoJson); $stmt->execute([$showId]); $rows = $stmt->fetchAll(); resp(['ok'=>true,'items'=>$rows]); } // Fallback for legacy 'state' column instead of 'status' if (strpos($msg, 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) { $selectJson2 = str_replace('e.status', 'e.state', $selectJson); try { $stmt = $pdo->prepare($selectJson2); $stmt->execute([$showId]); $rows = $stmt->fetchAll(); resp(['ok'=>true,'items'=>$rows]); } catch (Throwable $e2) { $selectNoJson2 = str_replace('e.status', 'e.state', $selectNoJson); $stmt = $pdo->prepare($selectNoJson2); $stmt->execute([$showId]); $rows = $stmt->fetchAll(); resp(['ok'=>true,'items'=>$rows]); } } throw $e; } } case 'get_show_by_tmdb': { $tmdbId = (int)($in['tmdb_id'] ?? 0); if (!$tmdbId) fail('bad params'); try { $stmt = $pdo->prepare('SELECT id FROM shows WHERE tmdb_id = ?'); $stmt->execute([$tmdbId]); $id = $stmt->fetchColumn(); resp(['ok' => true, 'id' => $id ? (int)$id : null]); } catch (Throwable $e) { // Fallback if column tmdb_id does not exist (older schema): scan JSON if (strpos($e->getMessage(), 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) { $stmt = $pdo->query('SELECT id, json FROM shows'); $rows = $stmt->fetchAll(); $found = null; foreach ($rows as $r) { $j = json_decode($r['json'] ?? 'null', true); if (is_array($j) && isset($j['id']) && (int)$j['id'] === $tmdbId) { $found = (int)$r['id']; break; } } resp(['ok' => true, 'id' => $found]); } else { throw $e; } } } case 'get_tmdb_by_show_id': { $showId = (int)($in['show_id'] ?? 0); if (!$showId) fail('bad params'); try { $stmt = $pdo->prepare('SELECT tmdb_id FROM shows WHERE id = ?'); $stmt->execute([$showId]); $tm = $stmt->fetchColumn(); resp(['ok' => true, 'tmdb_id' => $tm !== false ? (int)$tm : null]); } catch (Throwable $e) { // Fallback if column tmdb_id does not exist: load from JSON if (strpos($e->getMessage(), 'Unknown column') !== false || ($e instanceof PDOException && $e->getCode()==='42S22')) { $stmt = $pdo->prepare('SELECT json FROM shows WHERE id = ?'); $stmt->execute([$showId]); $json = $stmt->fetchColumn(); $m = json_decode($json ?: 'null', true); $tid = is_array($m) && isset($m['id']) ? (int)$m['id'] : null; resp(['ok' => true, 'tmdb_id' => $tid]); } else { throw $e; } } } case 'list_shows': { $stmt = $pdo->query('SELECT id, tmdb_id, name, json FROM shows'); $rows = $stmt->fetchAll(); resp(['ok' => true, 'items' => $rows]); } case 'delete_show': { $showId = (int)($in['show_id'] ?? 0); if (!$showId) fail('bad params'); $pdo->beginTransaction(); try { // Delete episodes belonging to this show's seasons try { $stmt = $pdo->prepare('DELETE e FROM episodes e JOIN seasons s ON s.id = e.season_id WHERE s.show_id = ?'); $stmt->execute([$showId]); } catch (Throwable $e) { // Fallback for SQL dialects not supporting multi-table delete $stmt = $pdo->prepare('DELETE FROM episodes WHERE season_id IN (SELECT id FROM seasons WHERE show_id = ?)'); $stmt->execute([$showId]); } // Delete seasons $stmt = $pdo->prepare('DELETE FROM seasons WHERE show_id = ?'); $stmt->execute([$showId]); // Delete show $stmt = $pdo->prepare('DELETE FROM shows WHERE id = ?'); $stmt->execute([$showId]); $pdo->commit(); resp(['ok' => true]); } catch (Throwable $e) { $pdo->rollBack(); if (MM_DEBUG) fail('delete failed: '.$e->getMessage(), 500); fail('delete failed', 500); } } case 'set_show_meta': { $showId = (int)($in['show_id'] ?? 0); if (!$showId) fail('bad params'); $fields = []; $params = []; if (isset($in['resolution'])) { $fields[] = 'resolution = ?'; $params[] = $in['resolution']; } if (array_key_exists('download_path', $in)) { $fields[] = 'download_path = ?'; $params[] = $in['download_path']; } if (isset($in['cliffhanger'])) { $fields[] = 'cliffhanger = ?'; $params[] = (int)!!$in['cliffhanger']; } if (empty($fields)) fail('no fields'); $sql = 'UPDATE shows SET '.implode(', ', $fields).' WHERE id = ?'; $params[] = $showId; $stmt = $pdo->prepare($sql); $stmt->execute($params); resp(['ok'=>true]); } case 'ping': { resp([ 'ok' => true, 'time' => date('c'), 'origin' => $origin, 'php' => PHP_VERSION, ]); break; } case 'set_game_status': { $igdbId = (int)($in['igdb_id'] ?? 0); $name = isset($in['name']) ? trim($in['name']) : ''; $orig = isset($in['original_name']) ? trim($in['original_name']) : ''; $status = (int)($in['status'] ?? 0); $note = isset($in['note']) ? trim($in['note']) : ''; $lang = isset($in['lang']) && $in['lang'] !== '' ? $in['lang'] : 'de'; $locTitle = isset($in['title']) ? trim($in['title']) : ''; $locSummary = isset($in['summary']) ? trim($in['summary']) : ''; $locStory = isset($in['storyline']) ? trim($in['storyline']) : ''; $lockFlag = array_key_exists('locked', $in) ? (int)!!$in['locked'] : null; $coverUrl = isset($in['cover_url']) ? trim($in['cover_url']) : null; $releaseYear = isset($in['release_year']) ? (int)$in['release_year'] : null; $gameJson = isset($in['json']) ? $in['json'] : null; $GLOBALS['__release_year'] = $releaseYear; $GLOBALS['__game_json'] = $gameJson !== null ? json_encode($gameJson, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) : null; if ($igdbId <= 0) fail('Invalid "igdb_id"'); if ($name === '') fail('Missing "name"'); if ($status < 0 || $status > 2) fail('Invalid "status" (0,1,2)'); $game = saveGameStatus($igdbId, $name, $orig !== '' ? $orig : null, $status, $note !== '' ? $note : null, $coverUrl ?: null); if ($locTitle !== '' || $locSummary !== '' || $locStory !== '' || $lockFlag !== null) { saveLocalization($igdbId, $lang, $locTitle ?: null, $locSummary ?: null, $locStory ?: null, null, $lockFlag); } resp(['ok' => true, 'game' => $game]); break; } case 'delete_game': { $igdbId = (int)($in['igdb_id'] ?? 0); if ($igdbId <= 0) fail('Invalid "igdb_id"'); $pdo->beginTransaction(); try { $stmt = $pdo->prepare('DELETE FROM igdb_localizations WHERE igdb_id = ?'); $stmt->execute([$igdbId]); $stmt = $pdo->prepare('DELETE FROM game WHERE igdb_id = ?'); $stmt->execute([$igdbId]); $pdo->commit(); resp(['ok' => true]); } catch (Throwable $e) { $pdo->rollBack(); if (MM_DEBUG) fail('delete failed: '.$e->getMessage(), 500); fail('delete failed', 500); } break; } case 'get_game': { $igdbId = (int)($in['igdb_id'] ?? 0); if ($igdbId <= 0) fail('Invalid "igdb_id"'); $game = getGameByIgdbId($igdbId); resp(['ok' => true, 'found' => (bool)$game, 'game' => $game]); break; } default: fail('unknown action'); } } catch (Throwable $e) { if (MM_DEBUG) resp(['ok'=>false,'error'=>$e->getMessage()], 500); resp(['ok'=>false,'error'=>'server error'], 500); }