$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 game) // ===================================================== function saveGameStatus( int $igdbId, string $name, ?string $originalName, int $status, ?string $note = null ): array { $pdo = getDb(); $sql = ' INSERT INTO game (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 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; } // ===================================================== // 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; $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,' . '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); }