From 975b704553b7bf94e41c0afadb7a7ca34be794bd Mon Sep 17 00:00:00 2001 From: Herwig Birke Date: Tue, 2 Dec 2025 09:36:02 +0100 Subject: [PATCH] merge multimedia.php with igdb.php --- lib/php/igdb-settings.php | 38 ++ lib/php/igdb.php | 891 ++++++++++++++++++++++++++++++++ lib/php/multimedia-settings.php | 19 + lib/php/multimedia.php | 520 ++++++++++++++++++- 4 files changed, 1467 insertions(+), 1 deletion(-) create mode 100644 lib/php/igdb-settings.php create mode 100644 lib/php/igdb.php diff --git a/lib/php/igdb-settings.php b/lib/php/igdb-settings.php new file mode 100644 index 0000000..d193a61 --- /dev/null +++ b/lib/php/igdb-settings.php @@ -0,0 +1,38 @@ + $msg]; + } + respond($msg, $status); +} + +// ===================================================== +// DATABASE +// ===================================================== + +function getDb(): PDO +{ + static $pdo = null; + if ($pdo !== null) { + return $pdo; + } + + $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 (PDOException $e) { + error_response('DB connection failed: ' . $e->getMessage(), 500); + } + + return $pdo; +} + +// ===================================================== +// LOCALIZATION SUPPORT (Tabelle igdb_localizations) +// ===================================================== + +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 +): array { + $pdo = getDb(); + + $sql = ' + INSERT INTO igdb_localizations (igdb_id, lang, title, summary, storyline, user_id) + VALUES (:id, :lang, :title, :summary, :storyline, :user_id) + ON DUPLICATE KEY UPDATE + title = VALUES(title), + summary = VALUES(summary), + storyline = VALUES(storyline), + user_id = VALUES(user_id), + updated_at = CURRENT_TIMESTAMP + '; + + $stmt = $pdo->prepare($sql); + $stmt->execute([ + ':id' => $igdbId, + ':lang' => $lang, + ':title' => $title, + ':summary' => $summary, + ':storyline' => $storyline, + ':user_id' => $userId, + ]); + + return getLocalization($igdbId, $lang); +} + +// ===================================================== +// GAMES IN MULTIMEDIAFLUTTER (Tabelle games) +// ===================================================== + +function saveGameStatus( + int $igdbId, + string $name, + ?string $originalName, + int $status, + ?string $note = null +): array { + $pdo = getDb(); + + $sql = ' + INSERT INTO games (igdb_id, name, original_name, status, note) + VALUES (:igdb_id, :name, :original_name, :status, :note) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + original_name = VALUES(original_name), + status = VALUES(status), + note = VALUES(note), + updated_at = CURRENT_TIMESTAMP + '; + + $stmt = $pdo->prepare($sql); + $stmt->execute([ + ':igdb_id' => $igdbId, + ':name' => $name, + ':original_name'=> $originalName, + ':status' => $status, + ':note' => $note, + ]); + + $stmt = $pdo->prepare('SELECT * FROM games 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 games WHERE igdb_id = :id LIMIT 1'); + $stmt->execute([':id' => $igdbId]); + return $stmt->fetch() ?: null; +} + +// ===================================================== +// IMAGE URL BUILDER +// ===================================================== + +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 + ); +} + +// ===================================================== +// TWITCH TOKEN + IGDB REQUEST +// ===================================================== + +function getCachedToken(): ?array +{ + if (!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 +{ + $expiresIn = (int)($tokenData['expires_in'] ?? 0); + $tokenData['expires_at'] = time() + $expiresIn - 60; // 60s Puffer + + 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; +} + +/** + * Interne Hilfsfunktion: macht einen Wikipedia-API-Call mit einem Suchstring + * und gibt das erste Page-Objekt (inkl. extract) zurück. + */ +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; + } + + // debug_url zum leichteren Debuggen + $page['debug_url'] = $url; + + return $page; +} + +/** + * Hochwertige DE-Zusammenfassung für einen Spieltitel holen. + * + * - Probiert mehrere Suchvarianten (Videospiel, Computerspiel, etc.) + * - Schneidet den Text auf ~600 Zeichen + * - Liefert Titel, Extract, URL + debug_url + */ +function wikiFetchGermanSummaryForTitle(string $gameTitle): ?array +{ + $gameTitleTrimmed = trim($gameTitle); + + // Verschiedene Suchmuster, um Spiele sauber zu treffen + $patterns = [ + $gameTitleTrimmed . ' (Computerspiel)', + $gameTitleTrimmed . ' (Videospiel)', + $gameTitleTrimmed . ' Videospiel', + $gameTitleTrimmed, // Fallback: purer Titel + ]; + + foreach ($patterns as $pattern) { + $page = wikiApiSearchWithExtract($pattern); + if ($page === null || empty($page['extract'])) { + continue; + } + + $pageTitle = $page['title'] ?? $gameTitleTrimmed; + + // einfache Heuristik: Title ohne Klammern vergleichen + $canonicalPageTitle = mb_strtolower(preg_replace('/\s*\(.*?\)\s*/u', '', $pageTitle), 'UTF-8'); + $canonicalGameTitle = mb_strtolower($gameTitleTrimmed, 'UTF-8'); + + // Wenn der Titel gar nicht passt, nächsten Versuch → verbessert Qualität + if (mb_strpos($canonicalPageTitle, $canonicalGameTitle) === false && + mb_strpos($canonicalGameTitle, $canonicalPageTitle) === false) { + // könnte z.B. „Zelda (Band)“ sein → skip + 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, + ]; + } + + // Keine passende Seite gefunden + return null; +} + +// ===================================================== +// ROUTING +// ===================================================== + +$action = $_GET['action'] ?? 'ping'; + +switch ($action) { + + // --------------------------------------- + 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; + + $body = sprintf( + 'fields id,name,summary,first_release_date,cover.image_id; ' . + 'limit %d; offset %d; search "%s";', + $limit, + $offset, + addslashes($query) + ); + + $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 games + $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,' . + 'websites.url,' . + 'websites.category,' . + 'age_ratings.rating,' . + 'age_ratings.category,' . + 'language_supports.language;' . + ' where id = %d;', + $id + ); + + $results = igdbRequest('/games', $body); + + if (empty($results)) { + error_response('Game not found', 404); + } + + $game = $results[0]; + + // Cover-URL + $game['cover_url'] = isset($game['cover']['image_id']) + ? igdbImageUrl($game['cover']['image_id'], 't_cover_big') + : null; + + // Screenshot-URLs + $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'); + } + } + } + + // eigener Status / Notiz + $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; + } + + // eigene Lokalisierung (DB) + $hasLoc = false; + if ($lang !== 'en') { + $loc = getLocalization($id, $lang); + if ($loc !== null) { + $hasLoc = true; + + 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'], + ]; + } + } + $game['has_localization'] = $hasLoc; + + // Wikipedia-Fallback (DE), wenn keine eigene Lokalisierung existiert + $game['has_external_de'] = false; + $game['external_de_source'] = null; + $game['external_de_summary'] = null; + $game['external_de_url'] = null; + + if ($checkExternal && $lang === 'de' && !$hasLoc) { + $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']; + + // Summary-Fallback auf DE-Text + $game['summary'] = $wiki['extract']; + + // OPTIONAL: Name anpassen + // $game['name'] = $wiki['title']; + } + } + + 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']) : ''; + + 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 + ); + + respond([ + 'success' => true, + 'game' => $game, + ]); + 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 'wiki_test': + $title = $_GET['title'] ?? ''; + if ($title === '') { + error_response('Missing "title"', 400); + } + + // Wir bauen die URL sichtbar zusammen + $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, '', '&'); + + // Wikipedia anfragen + $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); + + // Parse versuchen + $json = json_decode($response, true); + + respond([ + 'title' => $title, + 'used_url' => $url, // <- DIE WICHTIGE URL + 'http_status' => $status, + 'curl_error' => $curlErr, + 'raw_response' => $response, // <- Rohdaten + 'parsed_json' => $json, // <- Versuch zu decodieren + ]); + break; + + // --------------------------------------- + default: + error_response('Unknown action: ' . $action, 404); +} diff --git a/lib/php/multimedia-settings.php b/lib/php/multimedia-settings.php index 374049f..31976b7 100644 --- a/lib/php/multimedia-settings.php +++ b/lib/php/multimedia-settings.php @@ -24,4 +24,23 @@ define('UPLOAD_BASE_URL', 'https://api.windesign.at/multimedia/uploads'); define('MAX_UPLOAD_SIZE', 512 * 1024 * 1024); define('MM_DEBUG', true); + +// Twitch / IGDB OAuth +define('TWITCH_CLIENT_ID', 'jnmt5hzqcrt7iitrvxckli0qfqa8bp'); +define('TWITCH_CLIENT_SECRET', 'iop63242aj4atxjruwbw2p2r11ho4g'); + +// Basis-URLs +define('TWITCH_TOKEN_URL', 'https://id.twitch.tv/oauth2/token'); +define('IGDB_BASE_URL', 'https://api.igdb.com/v4'); + +// Token-Cache (kannst du beliebig verschieben) +define('IGDB_TOKEN_CACHE_FILE', __DIR__ . '/igdb_token_cache.json'); + +// Debug-Flag für Logging +define('IGDB_DEBUG', true); + +// Wikipedia +define('WIKI_DE_API_URL', 'https://de.wikipedia.org/w/api.php'); +define('WIKI_USER_AGENT', 'MultimediaFlutter/1.0 (https://windesign.at; Herwig.Birke@windesign.at)'); + ?> diff --git a/lib/php/multimedia.php b/lib/php/multimedia.php index 43d74ad..f2a6f71 100644 --- a/lib/php/multimedia.php +++ b/lib/php/multimedia.php @@ -26,7 +26,7 @@ $allowHeaders = 'Content-Type, Accept, X-Requested-With, Authorization'; if ($reqHeaders) { $allowHeaders .= ', ' . $reqHeaders; } header('Access-Control-Allow-Headers: ' . $allowHeaders); -header('Access-Control-Allow-Methods: POST, OPTIONS'); +header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Max-Age: 86400'); // Preflights cachen header('Access-Control-Allow-Credentials: false'); @@ -73,6 +73,524 @@ try { fail('DB connection failed', 500); } +// Shared getter so IGDB helpers can reuse the PDO instance +function getDb(): PDO { + global $pdo; + return $pdo; +} + +// ===================================================== +// 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 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): array { + $pdo = getDb(); + $sql = ' + INSERT INTO igdb_localizations (igdb_id, lang, title, summary, storyline, user_id) + VALUES (:id, :lang, :title, :summary, :storyline, :user_id) + ON DUPLICATE KEY UPDATE + title = VALUES(title), + summary = VALUES(summary), + storyline = VALUES(storyline), + user_id = VALUES(user_id), + updated_at = CURRENT_TIMESTAMP + '; + $stmt = $pdo->prepare($sql); + $stmt->execute([ + ':id' => $igdbId, + ':lang' => $lang, + ':title' => $title, + ':summary' => $summary, + ':storyline' => $storyline, + ':user_id' => $userId, + ]); + return getLocalization($igdbId, $lang); + } + + function saveGameStatus(int $igdbId, string $name, ?string $originalName, int $status, ?string $note = null): array { + $pdo = getDb(); + $sql = ' + INSERT INTO games (igdb_id, name, original_name, status, note) + VALUES (:igdb_id, :name, :original_name, :status, :note) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + original_name = VALUES(original_name), + status = VALUES(status), + note = VALUES(note), + updated_at = CURRENT_TIMESTAMP + '; + $stmt = $pdo->prepare($sql); + $stmt->execute([ + ':igdb_id' => $igdbId, + ':name' => $name, + ':original_name' => $originalName, + ':status' => $status, + ':note' => $note, + ]); + $stmt = $pdo->prepare('SELECT * FROM games 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 games WHERE igdb_id = :id LIMIT 1'); + $stmt->execute([':id' => $igdbId]); + return $stmt->fetch() ?: null; + } + + 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; + + $body = sprintf( + 'fields id,name,summary,first_release_date,cover.image_id; ' . + 'limit %d; offset %d; search "%s";', + $limit, + $offset, + addslashes($query) + ); + + $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 games + $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,websites.url,websites.category,age_ratings.rating,age_ratings.category,language_supports.language; 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; + if ($lang !== 'en') { + $loc = getLocalization($id, $lang); + if ($loc !== null) { + $hasLoc = true; + 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'], + ]; + } + } + $game['has_localization'] = $hasLoc; + $game['has_external_de'] = false; + $game['external_de_source']= null; + $game['external_de_summary']= null; + $game['external_de_url'] = null; + if ($checkExternal && $lang === 'de' && !$hasLoc) { + $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']) : ''; + 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); + respond(['success' => true, 'game' => $game]); + 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 '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;