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) { $q=$pdo->prepare('SELECT id FROM episodes WHERE tmdb_id=?'); $q->execute([$tmdbId]); $id=$q->fetchColumn(); } 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 '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"; // Prefer extracting show status directly from JSON and include cliffhanger column if present $selectWithYearJson = "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.first_air_year AS first_air_year, 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; $selectNoYearJson = "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, NULL AS first_air_year, 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 selects without JSON_EXTRACT (older MySQL) — frontend will parse from show_json $selectWithYear = "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.first_air_year AS first_air_year, sh.cliffhanger AS show_cliffhanger, sh.json AS show_json, se.season_number, sh.name AS show_name ".$base; $selectNoYear = "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, NULL AS first_air_year, 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($selectWithYearJson); resp(['ok'=>true,'items'=>$rows]); } catch (Throwable $e) { $msg = $e->getMessage(); if (strpos($msg, 'Unknown column') !== false) { // Maybe first_air_year missing — try JSON version without year try { $rows = $run($selectNoYearJson); resp(['ok'=>true,'items'=>$rows]); } catch (Throwable $e2) { $msg2 = $e2->getMessage(); if (stripos($msg2, 'JSON_EXTRACT') !== false || stripos($msg2, 'Unknown function') !== false) { // Fallback to non-JSON_EXTRACT selects try { $rows = $run($selectWithYear); resp(['ok'=>true,'items'=>$rows]); } catch (Throwable $e3) { if (strpos($e3->getMessage(), 'Unknown column') !== false) { $rows = $run($selectNoYear); resp(['ok'=>true,'items'=>$rows]); } else { throw $e3; } } } else { throw $e2; } } } elseif (stripos($msg, 'JSON_EXTRACT') !== false || stripos($msg, 'Unknown function') !== false) { // JSON functions not available try { $rows = $run($selectWithYear); resp(['ok'=>true,'items'=>$rows]); } catch (Throwable $e4) { if (strpos($e4->getMessage(), 'Unknown column') !== false) { $rows = $run($selectNoYear); resp(['ok'=>true,'items'=>$rows]); } else { throw $e4; } } } else { throw $e; } } } 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_show_by_tmdb': { $tmdbId = (int)($in['tmdb_id'] ?? 0); if (!$tmdbId) fail('bad params'); $stmt = $pdo->prepare('SELECT id FROM shows WHERE tmdb_id = ?'); $stmt->execute([$tmdbId]); $id = $stmt->fetchColumn(); resp(['ok' => true, 'id' => $id ? (int)$id : null]); } 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); }