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); } $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_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; $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()]); } 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 '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, ]); } default: fail('unknown action'); } } catch (Throwable $e) { if (MM_DEBUG) resp(['ok'=>false,'error'=>$e->getMessage()], 500); resp(['ok'=>false,'error'=>'server error'], 500); }