You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1408 lines
57 KiB
PHP
1408 lines
57 KiB
PHP
<?php
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
require_once __DIR__ . '/multimedia-settings.php';
|
|
|
|
// --- CORS ---
|
|
function mm_origin_allowed(string $origin): bool {
|
|
foreach (MM_ALLOWED_ORIGINS as $pat) {
|
|
$rx = preg_quote($pat, '/');
|
|
$rx = str_replace(['\\*', '\\:\\*'], ['.*', '(?::\\d+)?'], $rx);
|
|
if (preg_match('/^'.$rx.'$/i', $origin)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
if ($origin && mm_origin_allowed($origin)) {
|
|
header('Access-Control-Allow-Origin: ' . $origin);
|
|
header('Vary: Origin');
|
|
} else {
|
|
header('Access-Control-Allow-Origin: https://windesign.at'); // Fallback
|
|
}
|
|
|
|
// WICHTIG: weit gefasste Allow-Headers (Browser senden oft zusätzliche)
|
|
$reqHeaders = $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] ?? '';
|
|
$allowHeaders = 'Content-Type, Accept, X-Requested-With, Authorization';
|
|
if ($reqHeaders) { $allowHeaders .= ', ' . $reqHeaders; }
|
|
|
|
header('Access-Control-Allow-Headers: ' . $allowHeaders);
|
|
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
|
header('Access-Control-Max-Age: 86400'); // Preflights cachen
|
|
header('Access-Control-Allow-Credentials: false');
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
|
|
|
function jsonBody(): array {
|
|
$raw = file_get_contents('php://input');
|
|
if (!$raw) return [];
|
|
$data = json_decode($raw, true);
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
|
|
// Accept both JSON and form-urlencoded. Also decode JSON-like strings in keys we expect.
|
|
function inputBody(): array {
|
|
$in = jsonBody();
|
|
if (!$in) {
|
|
// Fallback to form POST
|
|
$in = $_POST ?: [];
|
|
}
|
|
// Normalize: decode JSON strings for nested payloads (e.g., tmdb)
|
|
foreach (['tmdb','episodes','seasons'] as $k) {
|
|
if (isset($in[$k]) && is_string($in[$k])) {
|
|
$d = json_decode($in[$k], true);
|
|
if (is_array($d)) $in[$k] = $d;
|
|
}
|
|
}
|
|
// Cast common numeric fields when present
|
|
foreach (['show_id','season_id','tmdb_id','ref_id','limit','offset'] as $k) {
|
|
if (isset($in[$k])) $in[$k] = (int)$in[$k];
|
|
}
|
|
return $in;
|
|
}
|
|
function resp($data, int $code = 200) { http_response_code($code); echo json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); exit; }
|
|
function fail($msg, int $code = 400) { resp(['ok'=>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);
|
|
}
|