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.

893 lines
25 KiB
PHP

<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
// igdb.php
require_once __DIR__ . '/igdb-settings.php';
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)');
}
header('Content-Type: application/json; charset=utf-8');
// =====================================================
// CORS HANDLING
// =====================================================
function origin_allowed(string $origin): bool
{
if (!defined('IGDB_ALLOWED_ORIGINS')) {
return false;
}
foreach (IGDB_ALLOWED_ORIGINS as $allowed) {
// Wildcards verarbeiten: *.domain.tld oder localhost:*
if (strpos($allowed, '*') !== false) {
$pattern = '#^' . str_replace(
['*', '.', ':'],
['.*', '\.', '\:'],
$allowed
) . '$#i';
if (preg_match($pattern, $origin)) {
return true;
}
} else {
if (strcasecmp($allowed, $origin) === 0) {
return true;
}
}
}
return false;
}
// Set CORS headers if origin matches
$origin = $_SERVER['HTTP_ORIGIN'] ?? null;
if ($origin && origin_allowed($origin)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Vary: Origin');
}
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
// Preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
// =====================================================
// HELPERS
// =====================================================
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);
}
// =====================================================
// 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);
}