PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ]); $pdo->exec("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, username VARCHAR(50) NOT NULL, password_hash VARCHAR(255) NOT NULL)"); $pdo->exec("CREATE TABLE IF NOT EXISTS config (key_name VARCHAR(50) PRIMARY KEY, key_value TEXT NOT NULL)"); $pdo->exec("CREATE TABLE IF NOT EXISTS critiques (id BIGINT PRIMARY KEY, title VARCHAR(255) NOT NULL, year VARCHAR(10), director VARCHAR(255), poster TEXT, rating DECIMAL(3,1) DEFAULT 3.0, review TEXT, streaming VARCHAR(255))"); try { $pdo->exec("ALTER TABLE critiques MODIFY COLUMN rating DECIMAL(3,1) DEFAULT 3.0"); } catch (\Exception $e) {} $pdo->exec("CREATE TABLE IF NOT EXISTS videotheque (id BIGINT PRIMARY KEY, title VARCHAR(255) NOT NULL, year VARCHAR(10), director VARCHAR(255), poster TEXT, format VARCHAR(50), length VARCHAR(50), publisher VARCHAR(255), ean_isbn13 VARCHAR(50), number_of_discs INT DEFAULT 1, aspect_ratio VARCHAR(50), description TEXT, actors TEXT)"); try { $pdo->exec("ALTER TABLE videotheque ADD COLUMN actors TEXT AFTER description"); } catch (\Exception $e) {} } catch (\PDOException $e) { echo json_encode(["error" => "Erreur BDD : " . $e->getMessage()]); exit; } // ── FONCTIONS UTILITAIRES ── function makeStableId($type, $title, $year) { return (abs(crc32(strtolower(trim($type ?? '')) . '|' . strtolower(trim($title ?? '')) . '|' . trim($year ?? ''))) % 2000000000) + 100000000; } function checkAuth($pdo) { if ($pdo->query("SELECT COUNT(*) FROM users")->fetchColumn() == 0) return true; $token = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; if (empty($token) && function_exists('apache_request_headers')) { $headers = apache_request_headers(); $token = $headers['Authorization'] ?? $headers['authorization'] ?? ''; } if ($token !== md5(ENCRYPTION_KEY . 'session')) { http_response_code(403); echo json_encode(["error" => "Accès interdit."]); exit; } } function encryptData($data) { $iv = openssl_random_pseudo_bytes(16); $key = hash('sha256', ENCRYPTION_KEY, true); return base64_encode(openssl_encrypt($data, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv) . '::' . $iv); } function decryptData($str) { $decoded = base64_decode($str); if ($decoded === false || strpos($decoded, '::') === false) return null; list($enc, $iv) = explode('::', $decoded, 2); return openssl_decrypt($enc, 'AES-256-CBC', hash('sha256', ENCRYPTION_KEY, true), OPENSSL_RAW_DATA, substr($iv, 0, 16)); } function getTmdbApiKey($pdo) { $stmt = $pdo->prepare("SELECT key_value FROM config WHERE key_name = 'tmdb_api_key'"); $stmt->execute(); $row = $stmt->fetch(); return $row ? decryptData($row['key_value']) : null; } function getUpcmdbApiKey($pdo) { $stmt = $pdo->prepare("SELECT key_value FROM config WHERE key_name = 'upcmdb_api_key'"); $stmt->execute(); $row = $stmt->fetch(); return $row ? decryptData($row['key_value']) : null; } function httpGet($url, $timeout = 5, $ua = null, $extraHeaders = []) { if (!$ua) $ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'; $headers = array_merge( ['Accept: application/json, text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language: fr-FR,fr;q=0.8'], $extraHeaders ); if (!function_exists('curl_init')) { $headerLines = implode("\r\n", $headers); $ctx = stream_context_create(['http' => ['timeout' => $timeout, 'user_agent' => $ua, 'header' => $headerLines]]); $res = @file_get_contents($url, false, $ctx); return $res ?: null; } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_USERAGENT => $ua, CURLOPT_FOLLOWLOCATION => true, CURLOPT_REFERER => 'https://www.cinemapassion.com/', CURLOPT_HTTPHEADER => $headers, ]); $res = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return ($code === 200 && is_string($res) && strlen($res) > 0) ? $res : null; } function cleanTitle($title) { $clean = preg_replace('/\s*[\[\(].*?[\]\)]\s*/', '', $title); $clean = preg_replace('/\s*-\s*(Édition|Edition|Collector|Simple|Spéciale|Digibook|Ultimate|Intégrale|Combo|SteelBook|Boîtier).*$/i', '', $clean); $clean = preg_replace('/(blu-ray|bluray|dvd|4k|ultra hd|combo|vhs|bdrip).*$/i', '', $clean); return trim(preg_replace('/\s{2,}/', ' ', $clean)); } function detectFormat($title, $desc = '') { $t = strtoupper($title . ' ' . $desc); if (strpos($t, '4K') !== false || strpos($t, 'UHD') !== false) return '4K Ultra HD'; if (strpos($t, 'BLU-RAY') !== false || strpos($t, 'BLURAY') !== false) return 'Blu-ray'; if (strpos($t, 'DVD') !== false) return 'DVD'; if (strpos($t, 'VHS') !== false) return 'VHS'; if (strpos($t, 'COFFRET') !== false || strpos($t, 'TRILOGIE') !== false) return 'Coffret'; return 'Blu-ray'; } function parseDiscCountFromTitle($title) { if (preg_match('/(\d+)\s*(?:dvd|blu-?ray|bluray|bd|disc|disque)/i', $title, $m)) { return max(1, (int)$m[1]); } if (preg_match('/(?:coffret|pack|collection|trilogie|anthologie).*?(\d+)/i', $title, $m)) { return max(1, (int)$m[1]); } if (preg_match('/\btrilogie\b/i', $title)) return 3; return 1; } function cleanUpcTitle($title) { $clean = trim($title); $clean = preg_replace('/^[\s"]*(DVD|Blu-ray|Blu Ray|4K|UHD)[\s"]*/i', '', $clean); $clean = preg_replace('/(blu-ray|bluray|dvd|4k|ultra hd|combo|vhs|blister pack|new blister).*$/i', '', $clean); $clean = preg_replace('/[\s"]+New[\s"]*$/i', '', $clean); return cleanTitle($clean); } function emptyPhysicalResult() { return [ 'title' => '', 'publisher' => '', 'format' => '', 'length' => '', 'number_of_discs' => 1, 'aspect_ratio' => '', 'year' => '' ]; } // ── FONCTIONS API PHYSIQUE (UPCitemdb → UPCMDB fallback) ── function throttleUpcLookup() { static $last = 0; $elapsed = microtime(true) - $last; if ($last > 0 && $elapsed < 2) usleep((int)((2 - $elapsed) * 1000000)); $last = microtime(true); } function fetchPhysicalFromUpcitemdb($ean) { $empty = ['title'=>'','publisher'=>'','format'=>'','length'=>'','number_of_discs'=>1,'aspect_ratio'=>'','year'=>'']; $ean = preg_replace('/[^0-9]/', '', (string)$ean); if (strlen($ean) < 8) return $empty; throttleUpcLookup(); $res = httpGet("https://api.upcitemdb.com/prod/trial/lookup?upc=" . urlencode($ean), 10); if (!$res) return $empty; $data = json_decode($res, true); if (empty($data['items'][0])) return $empty; $item = $data['items'][0]; $raw = $item['title'] ?? ''; $clean = cleanUpcTitle($raw) ?: $raw; return [ 'title' => $clean, 'publisher' => trim($item['brand'] ?? $item['manufacturer'] ?? ''), 'format' => detectFormat($raw), 'number_of_discs' => parseDiscCountFromTitle($raw), 'aspect_ratio' => '', 'year' => '', 'length' => '' ]; } function fetchPhysicalFromUpcmdb($ean, $pdo) { $empty = ['title'=>'','publisher'=>'','format'=>'','length'=>'','number_of_discs'=>1,'aspect_ratio'=>'','year'=>'']; $apiKey = getUpcmdbApiKey($pdo); if (!$apiKey) return $empty; $ean = preg_replace('/[^0-9]/', '', (string)$ean); if (strlen($ean) < 8) return $empty; $url = "https://upcmdb.com/api/v1/lookup/" . urlencode($ean); $res = httpGet($url, 10, 'MonPetitCinema/1.0', ['Accept: application/json', 'X-API-Key: ' . $apiKey]); if (!$res || $res[0] === '<') return $empty; $data = json_decode($res, true); if (($data['status'] ?? '') !== 'success' || empty($data['data'])) return $empty; $item = $data['data']; $raw = trim($item['title'] ?? ''); if (!$raw) return $empty; return [ 'title' => cleanUpcTitle($raw) ?: $raw, 'publisher' => trim($item['studio'] ?? $item['publisher'] ?? ''), 'format' => trim($item['format'] ?: detectFormat($raw)), 'length' => trim($item['runtime'] ?? ''), 'number_of_discs' => (int)($item['discs'] ?? parseDiscCountFromTitle($raw)) ?: 1, 'aspect_ratio' => trim($item['aspect_ratio'] ?? ''), 'year' => trim($item['year'] ?? '') ]; } function fetchPhysicalByEan($ean, $pdo = null) { $res = fetchPhysicalFromUpcitemdb($ean); if (!empty($res['title'])) return $res; if ($pdo) { $fb = fetchPhysicalFromUpcmdb($ean, $pdo); if (!empty($fb['title'])) return $fb; } return $res; } // ── FONCTION POUR RÉCUPÉRER LES AFFICHES DEPUIS TMDB ── function fetchPosterTMDB($title, $year = '', $pdo = null) { $defaultPoster = 'assets/img/default_physical_media.jpg'; $cleanTitle = cleanTitle($title); if (empty($cleanTitle)) { return ['poster' => $defaultPoster, 'title' => $cleanTitle, 'format' => 'Blu-ray']; } $tmdbKey = getTmdbApiKey($pdo); if (!$tmdbKey) { error_log("TMDB: ❌ Clé API non configurée"); return ['poster' => $defaultPoster, 'title' => $cleanTitle, 'format' => 'Blu-ray']; } // ÉTAPE 1 : Recherche du film $searchUrl = "https://api.themoviedb.org/3/search/movie?api_key={$tmdbKey}&query=" . urlencode($cleanTitle); if (!empty($year)) { $searchUrl .= "&year={$year}"; } $searchUrl .= "&language=fr-FR"; $searchRes = httpGet($searchUrl, 5); $searchData = $searchRes ? json_decode($searchRes, true) : []; // Si pas de résultat avec l'année, on réessaie sans if (empty($searchData['results']) && !empty($year)) { $searchUrl = "https://api.themoviedb.org/3/search/movie?api_key={$tmdbKey}&query=" . urlencode($cleanTitle) . "&language=fr-FR"; $searchRes = httpGet($searchUrl, 5); $searchData = $searchRes ? json_decode($searchRes, true) : []; } if (empty($searchData['results'])) { error_log("TMDB: ❌ Film non trouvé pour '{$cleanTitle}'"); return ['poster' => $defaultPoster, 'title' => $cleanTitle, 'format' => 'Blu-ray']; } // ÉTAPE 2 : Récupérer le poster du premier résultat $posterPath = $searchData['results'][0]['poster_path'] ?? ''; if (!empty($posterPath)) { $posterUrl = "https://image.tmdb.org/t/p/w500" . $posterPath; error_log("TMDB: ✅ Affiche trouvée pour '{$cleanTitle}' → {$posterUrl}"); return [ 'poster' => $posterUrl, 'title' => $cleanTitle, 'format' => 'Blu-ray' ]; } error_log("TMDB: ❌ Film trouvé mais pas d'affiche pour '{$cleanTitle}'"); return ['poster' => $defaultPoster, 'title' => $cleanTitle, 'format' => 'Blu-ray']; } // ── FONCTION POUR RÉCUPÉRER LE SYNOPSIS DEPUIS TMDB ── function fetchTmdbSynopsis($title, $year = '', $pdo = null) { $tmdbKey = getTmdbApiKey($pdo); if (!$tmdbKey || empty($title)) return ''; $cleanTitle = cleanTitle($title); $searchUrl = "https://api.themoviedb.org/3/search/movie?api_key={$tmdbKey}&query=" . urlencode($cleanTitle); if (!empty($year)) $searchUrl .= "&year={$year}"; $searchUrl .= "&language=fr-FR"; $searchRes = httpGet($searchUrl, 5); $searchData = $searchRes ? json_decode($searchRes, true) : []; if (empty($searchData['results'])) { // Retry sans l'année $searchUrl = "https://api.themoviedb.org/3/search/movie?api_key={$tmdbKey}&query=" . urlencode($cleanTitle) . "&language=fr-FR"; $searchRes = httpGet($searchUrl, 5); $searchData = $searchRes ? json_decode($searchRes, true) : []; } if (!empty($searchData['results'][0]['overview'])) { return $searchData['results'][0]['overview']; } return ''; } // ── FONCTION TMDB COMPLÈTE (Affiche + Métadonnées) ── function fetchTmdbPosterAndSynopsis($title, $year = '', $pdo = null) { $default = ['poster'=>'assets/img/default_physical_media.jpg','title'=>'','description'=>'','director'=>'','actors'=>'','length'=>'','year'=>'']; if (empty($title)) return $default; $tmdbKey = getTmdbApiKey($pdo); if (!$tmdbKey) return $default; $clean = cleanTitle($title); $searchUrl = "https://api.themoviedb.org/3/search/movie?api_key={$tmdbKey}&query=" . urlencode($clean) . "&language=fr-FR"; if ($year) $searchUrl .= "&year={$year}"; $res = httpGet($searchUrl, 5); $data = $res ? json_decode($res, true) : []; // Retry sans année si échec if (empty($data['results']) && $year) { $res = httpGet("https://api.themoviedb.org/3/search/movie?api_key={$tmdbKey}&query=" . urlencode($clean) . "&language=fr-FR", 5); $data = $res ? json_decode($res, true) : []; } if (empty($data['results'])) return $default; $movie = $data['results'][0]; $movieId = $movie['id']; $default['poster'] = !empty($movie['poster_path']) ? "https://image.tmdb.org/t/p/w500{$movie['poster_path']}" : $default['poster']; $default['year'] = !empty($movie['release_date']) ? substr($movie['release_date'], 0, 4) : $year; $default['description'] = $movie['overview'] ?? ''; $default['title'] = $clean; // Détails supplémentaires (réalisateur, acteurs, durée) $detailsUrl = "https://api.themoviedb.org/3/movie/{$movieId}?api_key={$tmdbKey}&append_to_response=credits&language=fr-FR"; $detRes = httpGet($detailsUrl, 5); if ($detRes) { $det = json_decode($detRes, true); $default['length'] = !empty($det['runtime']) ? "{$det['runtime']} min" : ''; if (!empty($det['credits']['crew'])) { $dirs = array_filter($det['credits']['crew'], fn($c) => $c['job'] === 'Director'); $default['director'] = $dirs ? implode(', ', array_map(fn($c) => $c['name'], array_slice($dirs, 0, 2))) : ''; } if (!empty($det['credits']['cast'])) { $default['actors'] = implode(', ', array_map(fn($c) => $c['name'], array_slice($det['credits']['cast'], 0, 5))); } } return $default; } // ── FONCTION POUR RÉCUPÉRER LES DONNÉES DEPUIS BLU-RAY.COM ── function fetchFromBlurayCom($ean) { static $lastRequest = 0; $empty = [ 'title' => '', 'year' => '', 'director' => '', 'actors' => '', 'poster' => '', 'description' => '', 'length' => '', 'publisher' => '', 'format' => 'Blu-ray', 'number_of_discs' => 1, 'aspect_ratio' => '' ]; $ean = preg_replace('/[^0-9]/', '', (string)$ean); if (strlen($ean) < 8) return $empty; // ✅ THROTTLE : Attendre 3 secondes entre chaque requête pour éviter le blocage $now = microtime(true); if ($lastRequest > 0 && ($now - $lastRequest) < 3) { usleep((int)(3 - ($now - $lastRequest)) * 1000000); } $lastRequest = microtime(true); // ÉTAPE 1 : Recherche par EAN sur Blu-ray.com $searchUrl = "https://www.blu-ray.com/movies/search.php?ean=" . urlencode($ean) . "&action=search"; $ch = curl_init($searchUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_FOLLOWLOCATION => true, CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', CURLOPT_HTTPHEADER => [ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7', 'Referer: https://www.blu-ray.com/', 'Connection: keep-alive' ] ]); $searchHtml = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); // ✅ GESTION D'ERREUR : Si Blu-ray.com bloque (403, 429, 503), on retourne vide if (!$searchHtml || $httpCode !== 200) { error_log("Blu-ray.com: ❌ Échec recherche EAN $ean (HTTP $httpCode)"); return $empty; } // Extraire l'URL du film depuis les résultats if (!preg_match('/href="(https?:\/\/www\.blu-ray\.com\/movies\/[^\/]+\/(\d+)\/)"/i', $searchHtml, $matches)) { error_log("Blu-ray.com: ❌ Film non trouvé pour EAN $ean"); return $empty; } $movieUrl = $matches[1]; $movieId = $matches[2]; // ✅ THROTTLE avant la 2ème requête usleep(1000000); // 1 seconde // ÉTAPE 2 : Récupérer la page du film $ch2 = curl_init($movieUrl); curl_setopt_array($ch2, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_FOLLOWLOCATION => true, CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', CURLOPT_HTTPHEADER => [ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7', 'Referer: https://www.blu-ray.com/', 'Connection: keep-alive' ] ]); $movieHtml = curl_exec($ch2); $httpCode2 = curl_getinfo($ch2, CURLINFO_HTTP_CODE); curl_close($ch2); if (!$movieHtml || $httpCode2 !== 200) { error_log("Blu-ray.com: ❌ Impossible de charger la page du film $movieUrl (HTTP $httpCode2)"); return $empty; } // === EXTRACTION DES DONNÉES === // Titre (depuis

) if (preg_match('/]*>([^<]+)<\/h1>/i', $movieHtml, $m)) { $rawTitle = trim(strip_tags($m[1])); $empty['title'] = trim(preg_replace('/\s*(Blu-ray|4K|3D|DVD|UHD).*$/i', '', $rawTitle)); } // Année (depuis 2015) if (preg_match('/href="[^"]*year=(\d{4})[^"]*"[^>]*>(\d{4})<\/a>/i', $movieHtml, $m)) { $empty['year'] = $m[1]; } // Studio/Éditeur (depuis Warner Bros.) if (preg_match('/href="[^"]*studioid=\d+[^"]*"[^>]*>([^<]+)<\/a>/i', $movieHtml, $m)) { $empty['publisher'] = trim($m[1]); } // Durée (depuis 120 min) if (preg_match('/]*id="runtime"[^>]*>(\d+)\s*min<\/span>/i', $movieHtml, $m)) { $empty['length'] = $m[1] . ' min'; } // Aspect ratio (depuis "Aspect ratio: 2.40:1
") if (preg_match('/Aspect[\s-]*ratio:\s*([\d\.]+:[\d\.]+)/i', $movieHtml, $m)) { $empty['aspect_ratio'] = trim($m[1]); } // Nombre de disques (depuis "Two-disc set" ou "Single disc set") if (preg_match('/(\w+)-disc\s+set/i', $movieHtml, $m)) { $wordToNum = [ 'single' => 1, 'two' => 2, 'three' => 3, 'four' => 4, 'five' => 5, 'six' => 6, 'seven' => 7, 'eight' => 8 ]; $word = strtolower($m[1]); $empty['number_of_discs'] = $wordToNum[$word] ?? 1; } elseif (preg_match('/(\d+)-disc\s+set/i', $movieHtml, $m)) { $empty['number_of_discs'] = (int)$m[1]; } // Format (déterminé depuis l'URL ou le contenu) if (strpos($movieUrl, '/4k/') !== false || stripos($movieHtml, '4K Ultra HD') !== false) { $empty['format'] = '4K Ultra HD'; } elseif (strpos($movieUrl, '/3d/') !== false || stripos($movieHtml, '3D Blu-ray') !== false) { $empty['format'] = '3D Blu-ray'; } else { $empty['format'] = 'Blu-ray'; } // Affiche HD (depuis ) if (preg_match('/src="(https:\/\/images\.static-bluray\.com\/movies\/covers\/\d+_front\.jpg[^"]*)"/i', $movieHtml, $m)) { $empty['poster'] = $m[1]; } elseif (preg_match('/]*class="coverfront"[^>]*src="([^"]+)"/i', $movieHtml, $m)) { $posterUrl = $m[1]; $posterUrl = preg_replace('/_large\.jpg/', '_front.jpg', $posterUrl); $empty['poster'] = $posterUrl; } // Synopsis, Réalisateur, Acteurs (depuis
) if (preg_match('/]*id="movie_info"[^>]*>(.*?)]*id="movie_review_intro"/is', $movieHtml, $infoBlock)) { $infoHtml = $infoBlock[1]; // Synopsis (texte après
et avant


Director:) if (preg_match('/<\/center>
\s*(.*?)


Director:/is', $infoHtml, $m)) { $synopsis = trim(strip_tags($m[1])); $synopsis = preg_replace('/\s+/', ' ', $synopsis); $empty['description'] = $synopsis; } // Réalisateur (depuis "Director: George Miller") if (preg_match('/Director:\s*]*>([^<]+)<\/a>/i', $infoHtml, $m)) { $empty['director'] = trim($m[1]); } // Acteurs (depuis "Starring: Tom Hardy, Charlize Theron, ...") if (preg_match('/Starring:\s*(.*?)(?:
|<\/div>)/is', $infoHtml, $m)) { $actorsHtml = $m[1]; preg_match_all('/]*>([^<]+)<\/a>/i', $actorsHtml, $actorMatches); if (!empty($actorMatches[1])) { $empty['actors'] = implode(', ', array_map('trim', array_slice($actorMatches[1], 0, 6))); } } } error_log("Blu-ray.com: ✅ Données récupérées pour EAN $ean → " . $empty['title']); return $empty; } // ── ROUTEUR PRINCIPAL ── $action = $_GET['action'] ?? ''; $data = json_decode(file_get_contents('php://input'), true) ?? []; switch ($action) { case 'check_security_status': echo json_encode(["is_blank" => ($pdo->query("SELECT COUNT(*) FROM users")->fetchColumn() == 0)]); break; case 'login': if ($pdo->query("SELECT COUNT(*) FROM users")->fetchColumn() == 0) { echo json_encode(["success" => true, "token" => md5(ENCRYPTION_KEY . 'session'), "blank" => true]); } else { $stmt = $pdo->prepare("SELECT password_hash FROM users WHERE username = 'admin'"); $stmt->execute(); $user = $stmt->fetch(); if ($user && password_verify($data['password'] ?? '', $user['password_hash'])) { echo json_encode(["success" => true, "token" => md5(ENCRYPTION_KEY . 'session'), "blank" => false]); } else { http_response_code(401); echo json_encode(["error" => "Mot de passe incorrect."]); } } break; case 'setup_admin': case 'update_password': checkAuth($pdo); $pwd = $data['password'] ?? $data['new_password'] ?? ''; $stmt = $pdo->prepare("REPLACE INTO users (id, username, password_hash) VALUES (1, 'admin', :pass)"); $stmt->execute([':pass' => password_hash($pwd, PASSWORD_BCRYPT)]); echo json_encode(["success" => true]); break; // ── CONFIGURATION ── case 'get_config_keys': $keys = ['tmdb_api_key', 'upcmdb_api_key']; $config = []; foreach ($keys as $k) { $stmt = $pdo->prepare("SELECT key_value FROM config WHERE key_name = ?"); $stmt->execute([$k]); $row = $stmt->fetch(); $config[$k] = $row ? decryptData($row['key_value']) : ''; } echo json_encode($config); break; case 'save_config': checkAuth($pdo); $name = $data['key_name'] ?? ''; $val = trim($data['key_value'] ?? ''); if (!in_array($name, ['tmdb_api_key', 'upcmdb_api_key'])) { http_response_code(400); echo json_encode(["error" => "Clé invalide."]); break; } if (empty($val)) break; // Ne rien écraser si vide $stmt = $pdo->prepare("REPLACE INTO config (key_name, key_value) VALUES (?, ?)"); $stmt->execute([$name, encryptData($val)]); echo json_encode(["success" => true]); break; case 'get_films': $sql = " SELECT id, title, year, director, poster, rating, review, NULL AS description, streaming, 'critique' AS type, NULL AS format FROM critiques UNION ALL SELECT id, title, year, director, poster, NULL AS rating, NULL AS review, description, NULL AS streaming, 'videotheque' AS type, format FROM videotheque ORDER BY id DESC "; $result = $pdo->query($sql)->fetchAll(); // IMPORTANT : Utilisation du pointeur `&` pour que la modification soit effective dans $result foreach ($result as $row) { if ($row['rating'] !== null) { $ratingVal = (float)$row['rating']; $row['rating'] = ($ratingVal == floor($ratingVal)) ? (int)$ratingVal : $ratingVal; } } unset($row); echo json_encode($result); break; case 'save_film': checkAuth($pdo); $type = $data['type'] ?? 'critique'; $id = !empty($data['id']) ? $data['id'] : makeStableId($type, $data['title'] ?? '', $data['year'] ?? '0000'); if ($type === 'critique' && (empty($data['director']) || empty($data['poster']))) { $tmdbData = fetchTmdbPosterAndSynopsis($data['title'] ?? '', $data['year'] ?? '', $pdo); if ($tmdbData) { if (empty($data['director'])) $data['director'] = $tmdbData['director']; if (empty($data['poster'])) $data['poster'] = $tmdbData['poster']; } } if ($type === 'critique') { $streaming = $data['streaming'] ?? ''; if (empty($streaming)) $streaming = 'Support physique / Cinéma'; $sql = "INSERT INTO critiques (id, title, year, director, poster, rating, review, streaming) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE title=VALUES(title), year=VALUES(year), director=VALUES(director), poster=VALUES(poster), rating=VALUES(rating), review=VALUES(review), streaming=VALUES(streaming)"; $stmt = $pdo->prepare($sql); $stmt->execute([$id, $data['title'] ?? '', $data['year'] ?? '', $data['director'] ?? '', $data['poster'] ?? '', $data['rating'] ?? 3.0, $data['review'] ?? '', $streaming]); } else { // ── CORRECTION : Suppression de fetchBluRayPhysicalInfo qui n'existe pas ── if (empty($data['poster']) && !empty($data['title'])) { $tmdbData = fetchTmdbPosterAndSynopsis($data['title'], $data['year'] ?? '', $pdo); if ($tmdbData['poster'] !== 'assets/img/default_physical_media.jpg') { $data['poster'] = $tmdbData['poster']; } if (empty($data['description'])) $data['description'] = $tmdbData['description']; if (empty($data['director'])) $data['director'] = $tmdbData['director']; if (empty($data['actors'])) $data['actors'] = $tmdbData['actors']; if (empty($data['length'])) $data['length'] = $tmdbData['length']; if (empty($data['format'])) $data['format'] = detectFormat($data['title'], $data['description'] ?? ''); } $sql = "INSERT INTO videotheque (id, title, year, director, poster, format, length, publisher, ean_isbn13, number_of_discs, aspect_ratio, description, actors) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE title=VALUES(title), year=VALUES(year), director=IF(VALUES(director)!='', VALUES(director), director), poster=IF(VALUES(poster)!='', VALUES(poster), poster), format=IF(VALUES(format)!='', VALUES(format), format), length=IF(VALUES(length)!='', VALUES(length), length), publisher=IF(VALUES(publisher)!='', VALUES(publisher), publisher), ean_isbn13=IF(VALUES(ean_isbn13)!='', VALUES(ean_isbn13), ean_isbn13), number_of_discs=IF(VALUES(number_of_discs)!=1, VALUES(number_of_discs), number_of_discs), aspect_ratio=IF(VALUES(aspect_ratio)!='', VALUES(aspect_ratio), aspect_ratio), description=IF(VALUES(description)!='', VALUES(description), description), actors=IF(VALUES(actors)!='', VALUES(actors), actors)"; $stmt = $pdo->prepare($sql); $stmt->execute([$id, $data['title'] ?? '', $data['year'] ?? '', $data['director'] ?? '', $data['poster'] ?? '', $data['format'] ?? '', $data['length'] ?? '', $data['publisher'] ?? '', $data['ean_isbn13'] ?? '', $data['number_of_discs'] ?? 1, $data['aspect_ratio'] ?? '', $data['description'] ?? '', $data['actors'] ?? '']); } echo json_encode(["success" => true]); break; case 'delete_film': checkAuth($pdo); $type = $_GET['type'] ?? 'critique'; $table = ($type === 'videotheque') ? 'videotheque' : 'critiques'; $id = $_GET['id'] ?? null; if (!$id) { http_response_code(400); echo json_encode(["error" => "ID manquant."]); break; } $stmt = $pdo->prepare("DELETE FROM $table WHERE id = ?"); $stmt->execute([$id]); echo json_encode(["success" => true]); break; case 'bulk_delete': checkAuth($pdo); $ids = $data['ids'] ?? []; $type = $data['type'] ?? 'critique'; $table = ($type === 'videotheque') ? 'videotheque' : 'critiques'; if (!empty($ids)) { $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("DELETE FROM $table WHERE id IN ($placeholders)"); $stmt->execute($ids); echo json_encode(["success" => true]); } else { http_response_code(400); echo json_encode(["success" => false, "error" => "Aucun élément sélectionné."]); } break; // ── IMPORT BATCH (VIDÉOTHÈQUE) ── case 'import_batch': checkAuth($pdo); $data = json_decode(file_get_contents("php://input"), true); $type = $data['type'] ?? ''; $items = $data['items'] ?? []; $pdo->beginTransaction(); $imported = 0; $skipped = 0; try { if ($type === 'videotheque') { $stmt = $pdo->prepare("INSERT INTO videotheque (id, title, year, format, poster, ean_isbn13, description, length, number_of_discs, aspect_ratio, actors, publisher, director) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE title=VALUES(title), year=VALUES(year), format=VALUES(format), poster=VALUES(poster), ean_isbn13=VALUES(ean_isbn13), description=VALUES(description), length=VALUES(length), number_of_discs=VALUES(number_of_discs), aspect_ratio=VALUES(aspect_ratio), actors=VALUES(actors), publisher=VALUES(publisher), director=VALUES(director)"); foreach ($items as $item) { $ean = preg_replace('/[^0-9]/', '', (string)($item['ean'] ?? '')); $csvTitle = trim($item['title'] ?? ''); // Titre issu du CSV (fallback) if (strlen($ean) < 8 && empty($csvTitle)) { $skipped++; continue; } // 1. Données physiques via BLU-RAY.COM (prioritaire) $blurayData = !empty($ean) ? fetchFromBlurayCom($ean) : []; // Si Blu-ray.com trouve le film, on prend ses données. Sinon, on utilise le titre du CSV. $title = !empty($blurayData['title']) ? $blurayData['title'] : $csvTitle; if (empty($title)) { $skipped++; continue; } $year = $blurayData['year'] ?? ''; $format = $blurayData['format'] ?: detectFormat($title); $publisher = $blurayData['publisher'] ?? ''; $discs = $blurayData['number_of_discs'] ?: 1; $aspect = $blurayData['aspect_ratio'] ?? ''; $length = $blurayData['length'] ?? ''; $poster = $blurayData['poster'] ?? ''; $desc = $blurayData['description'] ?? ''; $director = $blurayData['director'] ?? ''; $actors = $blurayData['actors'] ?? ''; // 2. Fallback TMDB si Blu-ray.com n'a pas trouvé certaines données if (empty($poster) || empty($director) || empty($actors) || empty($desc)) { $tmdb = fetchTmdbPosterAndSynopsis($title, $year, $pdo); if (empty($poster) || $poster === 'assets/img/default_physical_media.jpg') { $poster = $tmdb['poster']; } if (empty($director)) $director = $tmdb['director'] ?? ''; if (empty($actors)) $actors = $tmdb['actors'] ?? ''; if (empty($desc)) $desc = $tmdb['description'] ?? ''; if (empty($length) && !empty($tmdb['length'])) $length = $tmdb['length']; if (empty($year) && !empty($tmdb['year'])) $year = $tmdb['year']; } $id = makeStableId('videotheque', $title, $year); $stmt->execute([$id, $title, $year, $format, $poster, $ean, $desc, $length, $discs, $aspect, $actors, $publisher, $director]); $imported++; } } else { // ── IMPORTATION CRITIQUES ── $stmtCritiques = $pdo->prepare("INSERT INTO critiques (id, title, year, director, poster, rating, review, streaming) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE title=VALUES(title), year=VALUES(year), director=VALUES(director), poster=VALUES(poster), rating=VALUES(rating), review=VALUES(review), streaming=VALUES(streaming)"); foreach ($items as $rowData) { $title = $rowData['Title'] ?? $rowData['title'] ?? ''; if (empty($title)) continue; $year = $rowData['Year'] ?? $rowData['year'] ?? ''; $id = makeStableId('critique', $title, $year); $ratingRaw = $rowData['Rating'] ?? $rowData['rating'] ?? ''; $rating = ($ratingRaw !== '' && $ratingRaw !== null) ? (float)$ratingRaw : null; $review = $rowData['Review'] ?? $rowData['review'] ?? ''; $director = ''; $poster = 'assets/img/default_physical_media.jpg'; $streaming = 'Support physique / Cinéma'; // ── CORRECTION ICI : Utilisation de la bonne fonction TMDB ── $tmdbData = fetchTmdbPosterAndSynopsis($title, $year, $pdo); if ($tmdbData) { if (!empty($tmdbData['director'])) $director = $tmdbData['director']; if ($tmdbData['poster'] !== 'assets/img/default_physical_media.jpg') $poster = $tmdbData['poster']; if (empty($year) && !empty($tmdbData['year'])) $year = $tmdbData['year']; } $stmtCritiques->execute([$id, $title, $year, $director, $poster, $rating, $review, $streaming]); $imported++; } } $pdo->commit(); echo json_encode(["success" => true, "imported" => $imported, "skipped" => $skipped]); } catch (Throwable $e) { // ── CORRECTION ICI : Throwable capture les Crash Fatals PHP ── $pdo->rollBack(); error_log("import_batch error: " . $e->getMessage()); http_response_code(500); echo json_encode(["success" => false, "error" => $e->getMessage(), "line" => $e->getLine()]); } break; }