Files
mon-petit-cinema/api.php
T
2026-06-30 16:54:56 +02:00

896 lines
40 KiB
PHP

PHP
<?php
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/php_errors.log');
ini_set('display_errors', 0);
set_time_limit(600); // Protection timeout pour les gros imports
error_reporting(E_ALL);
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
define('ENCRYPTION_KEY', 'MaCleSecreteSuperRobuste123!');
try {
$pdo = new PDO("mysql:host=localhost;dbname=mon_cinema;charset=utf8mb4", "root", "", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
// Initialisation BDD simplifiée
$pdo->exec("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, username VARCHAR(50), password_hash VARCHAR(255))");
$pdo->exec("CREATE TABLE IF NOT EXISTS config (key_name VARCHAR(50) PRIMARY KEY, key_value TEXT)");
$pdo->exec("CREATE TABLE IF NOT EXISTS critiques (id BIGINT PRIMARY KEY, title VARCHAR(255), year VARCHAR(10), director VARCHAR(255), poster TEXT, rating DECIMAL(3,1), review TEXT, streaming VARCHAR(255))");
$pdo->exec("CREATE TABLE IF NOT EXISTS videotheque (id BIGINT PRIMARY KEY, title VARCHAR(255), 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)");
} catch (PDOException $e) { die(json_encode(["error" => "Connexion BDD échouée"])); }
// --- Fonctions Utilitaires ---
function checkAuth($pdo) {
if ($pdo->query("SELECT COUNT(*) FROM users")->fetchColumn() == 0) return true;
$token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if ($token !== md5(ENCRYPTION_KEY . 'session')) { http_response_code(403); exit; }
}
function encryptData($data) {
$iv = openssl_random_pseudo_bytes(16);
return base64_encode(openssl_encrypt($data, 'AES-256-CBC', hash('sha256', ENCRYPTION_KEY, true), 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 = 10, $headers = []) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0',
CURLOPT_HTTPHEADER => $headers
]);
$res = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ($code === 200) ? $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) {
// 1. Essayer UPCitemdb
$res = fetchPhysicalFromUpcitemdb($ean);
if (!empty($res['title'])) {
// 2. Chercher sur MovieCovers avec le titre trouvé
$mc = fetchFromMovieCovers($res['title'], $res['year']);
if (!empty($mc['poster'])) {
$res['poster'] = $mc['poster'];
}
if (!empty($mc['director'])) {
$res['director'] = $mc['director'];
}
if (!empty($mc['actors'])) {
$res['actors'] = $mc['actors'];
}
if (!empty($mc['description'])) {
$res['description'] = $mc['description'];
}
return $res;
}
// 3. Fallback UPCMDB
if ($pdo) {
$fb = fetchPhysicalFromUpcmdb($ean, $pdo);
if (!empty($fb['title'])) {
$mc = fetchFromMovieCovers($fb['title'], $fb['year']);
if (!empty($mc['poster'])) {
$fb['poster'] = $mc['poster'];
}
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;
}
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) {
error_log("Blu-ray.com: ❌ EAN invalide: $ean");
return $empty;
}
// Throttle: 3 secondes entre chaque requête
$now = microtime(true);
if ($lastRequest > 0 && ($now - $lastRequest) < 3) {
$sleepTime = 3 - ($now - $lastRequest);
error_log("Blu-ray.com: ⏱️ Attente de " . round($sleepTime, 2) . "s");
usleep((int)($sleepTime * 1000000));
}
$lastRequest = microtime(true);
error_log("Blu-ray.com: 🔍 Recherche EAN $ean");
// Recherche par EAN
$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,*/*;q=0.8',
'Accept-Language: fr-FR,fr;q=0.9',
'Referer: https://www.blu-ray.com/'
]
]);
$searchHtml = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if (!$searchHtml || $httpCode !== 200) {
error_log("Blu-ray.com: ❌ Échec recherche EAN $ean (HTTP $httpCode) - Erreur: $curlError");
return $empty;
}
// Extraire l'URL du film - regex améliorée
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];
error_log("Blu-ray.com: ✅ Film trouvé → $movieUrl");
// Délai avant la 2ème requête
sleep(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',
CURLOPT_HTTPHEADER => [
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Referer: https://www.blu-ray.com/'
]
]);
$movieHtml = curl_exec($ch2);
curl_close($ch2);
if (!$movieHtml) {
error_log("Blu-ray.com: ❌ Impossible de charger la page du film");
return $empty;
}
// Extraction des données - regex améliorées
// Titre
if (preg_match('/<h1[^>]*>([^<]+)<\/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
if (preg_match('/href="[^"]*year=(\d{4})[^"]*"[^>]*>(\d{4})<\/a>/i', $movieHtml, $m)) {
$empty['year'] = $m[1];
}
// Studio/Éditeur
if (preg_match('/href="[^"]*studioid=\d+[^"]*"[^>]*>([^<]+)<\/a>/i', $movieHtml, $m)) {
$empty['publisher'] = trim($m[1]);
}
// Durée
if (preg_match('/<span[^>]*id="runtime"[^>]*>(\d+)\s*min<\/span>/i', $movieHtml, $m)) {
$empty['length'] = $m[1] . ' min';
}
// Aspect ratio
if (preg_match('/Aspect[\s-]*ratio:\s*([\d\.]+:[\d\.]+)/i', $movieHtml, $m)) {
$empty['aspect_ratio'] = trim($m[1]);
}
// Nombre de disques - plusieurs patterns
if (preg_match('/(\w+)-disc\s+set/i', $movieHtml, $m)) {
$wordToNum = ['single' => 1, 'two' => 2, 'three' => 3, 'four' => 4, 'five' => 5, 'six' => 6];
$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
if (strpos($movieUrl, '/4k/') !== false || stripos($movieHtml, '4K Ultra HD') !== false) {
$empty['format'] = '4K Ultra HD';
} elseif (strpos($movieUrl, '/3d/') !== false) {
$empty['format'] = '3D Blu-ray';
} else {
$empty['format'] = 'Blu-ray';
}
// Affiche HD - plusieurs patterns
if (preg_match('/src="(https:\/\/images\.static-bluray\.com\/movies\/covers\/\d+_front\.jpg[^"]*)"/i', $movieHtml, $m)) {
$empty['poster'] = $m[1];
} elseif (preg_match('/<img[^>]*class="coverfront"[^>]*src="([^"]+)"/i', $movieHtml, $m)) {
$posterUrl = $m[1];
$posterUrl = preg_replace('/_large\.jpg/', '_front.jpg', $posterUrl);
$empty['poster'] = $posterUrl;
}
// Synopsis, Réalisateur, Acteurs
if (preg_match('/<div[^>]*id="movie_info"[^>]*>(.*?)<div[^>]*id="movie_review_intro"/is', $movieHtml, $infoBlock)) {
$infoHtml = $infoBlock[1];
// Synopsis
if (preg_match('/<\/center><br>\s*(.*?)<br><br><br>Director:/is', $infoHtml, $m)) {
$synopsis = trim(strip_tags($m[1]));
$synopsis = preg_replace('/\s+/', ' ', $synopsis);
$empty['description'] = $synopsis;
}
// Réalisateur
if (preg_match('/Director:\s*<a[^>]*>([^<]+)<\/a>/i', $infoHtml, $m)) {
$empty['director'] = trim($m[1]);
}
// Acteurs
if (preg_match('/Starring:\s*(.*?)(?:<br>|<\/div>)/is', $infoHtml, $m)) {
$actorsHtml = $m[1];
preg_match_all('/<a[^>]*>([^<]+)<\/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;
}
// ── FONCTION POUR RÉCUPÉRER LES DONNÉES DEPUIS MOVIECOVERS.COM ──
function fetchFromMovieCovers($title, $year = '') {
$empty = [
'title' => '', 'year' => '', 'director' => '', 'actors' => '',
'poster' => '', 'description' => '', 'length' => '',
'publisher' => '', 'format' => 'DVD', 'number_of_discs' => 1,
'aspect_ratio' => ''
];
if (empty($title)) return $empty;
// Nettoyer le titre pour la recherche
$searchTitle = trim($title);
$searchTitle = preg_replace('/\s*[\[\(].*?[\]\)]\s*/', '', $searchTitle); // Enlever [Blu-ray], (2015), etc.
$searchTitle = preg_replace('/\s*-\s*(Édition|Edition|Collector|Simple|Spéciale|Digibook|Ultimate|Intégrale|Combo|SteelBook|Boîtier).*$/i', '', $searchTitle);
$searchTitle = preg_replace('/(blu-ray|bluray|dvd|4k|ultra hd|combo|vhs|bdrip).*$/i', '', $searchTitle);
$searchTitle = trim(preg_replace('/\s{2,}/', ' ', $searchTitle));
if (empty($searchTitle)) return $empty;
error_log("MovieCovers: 🔍 Recherche de '$searchTitle'");
// ÉTAPE 1 : Recherche du film via l'URL de recherche
$searchUrl = "https://moviecovers.com/multicrit.html?titre=" . urlencode($searchTitle) . "&slow=1&listes=1";
$html = httpGet($searchUrl, 10);
if (!$html) {
error_log("MovieCovers: ❌ Échec de la recherche");
return $empty;
}
// Extraire le lien vers la page du film
// Format : <a href="/film/titre_SAW.html">SAW</a>
if (!preg_match('/href=["\']?\/film\/titre_([^"\'\s]+)\.html["\']?/i', $html, $matches)) {
error_log("MovieCovers: ❌ Film non trouvé pour '$searchTitle'");
return $empty;
}
$filmId = $matches[1];
$filmUrl = "https://moviecovers.com/film/titre_{$filmId}.html";
error_log("MovieCovers: ✅ Film trouvé → $filmUrl");
// ÉTAPE 2 : Récupérer la page du film
$filmHtml = httpGet($filmUrl, 10);
if (!$filmHtml) {
error_log("MovieCovers: ❌ Impossible de charger la page du film");
return $empty;
}
// ÉTAPE 3 : Extraire les informations
// Titre (via og:title)
if (preg_match('/<meta property="og:title" content="([^"]+)"/i', $filmHtml, $m)) {
$empty['title'] = trim($m[1]);
} else {
$empty['title'] = $searchTitle;
}
// IDMC (via og:image ou PRE)
$idmc = null;
if (preg_match('/<meta property="og:image" content="[^"]*\/getjpg\.html\/([^"\'\/]+)\.jpg"/i', $filmHtml, $m)) {
$idmc = urldecode($m[1]);
} elseif (preg_match('/IDMC.*?<PRE>([^<]+)<\/PRE>/is', $filmHtml, $m)) {
$idmc = trim($m[1]);
}
if (!$idmc) {
error_log("MovieCovers: ❌ IDMC non trouvé");
return $empty;
}
// URL de la cover (haute qualité)
$idmcEncoded = urlencode($idmc);
$empty['poster'] = "https://moviecovers.com/DATA/zipcache/{$idmcEncoded}.jpg";
// Année
if (preg_match('/<TH[^>]*>Ann[^<]*e<\/TH>\s*<TD[^>]*>.*?(\d{4})/is', $filmHtml, $m)) {
$empty['year'] = $m[1];
}
// Durée
if (preg_match('/<TH[^>]*>Dur[^<]*e<\/TH>\s*<TD[^>]*>([^<]+)/is', $filmHtml, $m)) {
$empty['length'] = trim(strip_tags($m[1]));
}
// Réalisateur
if (preg_match('/<TH[^>]*>R[^<]*alisateur<\/TH>\s*<TD[^>]*>.*?<a[^>]*>([^<]+)<\/a>/is', $filmHtml, $m)) {
$empty['director'] = trim($m[1]);
}
// Acteurs (extraire tous les liens dans la section acteurs)
if (preg_match('/<TH[^>]*>Acteurs[^<]*<\/TH>\s*<TD[^>]*>(.*?)<\/TD>/is', $filmHtml, $m)) {
$actorsHtml = $m[1];
preg_match_all('/<a[^>]*>([^<]+)<\/a>/i', $actorsHtml, $actorMatches);
if (!empty($actorMatches[1])) {
$empty['actors'] = implode(', ', array_map('trim', array_slice($actorMatches[1], 0, 5)));
}
}
// Synopsis (via og:description)
if (preg_match('/<meta property="og:description" content="([^"]+)"/i', $filmHtml, $m)) {
$empty['description'] = html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
// Distribution (éditeur)
if (preg_match('/<TH[^>]*>Distribution<\/TH>\s*<TD[^>]*>.*?<a[^>]*>([^<]+)<\/a>/is', $filmHtml, $m)) {
$empty['publisher'] = trim($m[1]);
}
error_log("MovieCovers: ✅ Données récupérées → " . $empty['title'] . " (" . $empty['year'] . ")");
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=IF(VALUES(poster)!='assets/img/default_physical_media.jpg', VALUES(poster), poster),
ean_isbn13=IF(VALUES(ean_isbn13)!='', VALUES(ean_isbn13), ean_isbn13),
description=IF(VALUES(description)!='', VALUES(description), description),
length=IF(VALUES(length)!='', VALUES(length), length),
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),
actors=IF(VALUES(actors)!='', VALUES(actors), actors),
publisher=IF(VALUES(publisher)!='', VALUES(publisher), publisher),
director=IF(VALUES(director)!='', VALUES(director), director)");
foreach ($items as $item) {
$ean = preg_replace('/[^0-9]/', '', (string)($item['ean'] ?? ''));
$csvTitle = trim($item['title'] ?? '');
if (strlen($ean) < 8 && empty($csvTitle)) {
$skipped++;
continue;
}
error_log("Import: 🎬 Traitement de '$csvTitle' (EAN: $ean)");
// 1. Essayer Blu-ray.com (prioritaire pour les données physiques)
$blurayData = !empty($ean) ? fetchFromBlurayCom($ean) : [];
// 2. Si Blu-ray.com échoue, essayer MovieCovers avec le titre CSV
$movieCoversData = [];
if ((empty($blurayData['title']) || empty($blurayData['poster'])) && !empty($csvTitle)) {
error_log("Import: 🔄 Blu-ray.com incomplet, essai MovieCovers pour '$csvTitle'");
$movieCoversData = fetchFromMovieCovers($csvTitle);
}
// 3. Fusionner les données (Blu-ray.com prioritaire, MovieCovers en complément)
$title = !empty($blurayData['title']) ? $blurayData['title'] : (!empty($movieCoversData['title']) ? $movieCoversData['title'] : $csvTitle);
if (empty($title)) {
error_log("Import: ❌ Pas de titre pour EAN $ean - ignoré");
$skipped++;
continue;
}
$year = !empty($blurayData['year']) ? $blurayData['year'] : (!empty($movieCoversData['year']) ? $movieCoversData['year'] : '');
$format = !empty($blurayData['format']) ? $blurayData['format'] : detectFormat($title);
$publisher = !empty($blurayData['publisher']) ? $blurayData['publisher'] : (!empty($movieCoversData['publisher']) ? $movieCoversData['publisher'] : '');
$discs = !empty($blurayData['number_of_discs']) ? $blurayData['number_of_discs'] : 1;
$aspect = !empty($blurayData['aspect_ratio']) ? $blurayData['aspect_ratio'] : '';
$length = !empty($blurayData['length']) ? $blurayData['length'] : (!empty($movieCoversData['length']) ? $movieCoversData['length'] : '');
$poster = !empty($blurayData['poster']) ? $blurayData['poster'] : (!empty($movieCoversData['poster']) ? $movieCoversData['poster'] : '');
$desc = !empty($blurayData['description']) ? $blurayData['description'] : (!empty($movieCoversData['description']) ? $movieCoversData['description'] : '');
$director = !empty($blurayData['director']) ? $blurayData['director'] : (!empty($movieCoversData['director']) ? $movieCoversData['director'] : '');
$actors = !empty($blurayData['actors']) ? $blurayData['actors'] : (!empty($movieCoversData['actors']) ? $movieCoversData['actors'] : '');
// 4. Fallback TMDB si données manquantes
if (empty($poster) || empty($director) || empty($actors) || empty($desc) || empty($year)) {
error_log("Import: 🔄 Fallback TMDB pour '$title'");
$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++;
error_log("Import: ✅ Importé '$title' ($ean) - Poster: " . ($poster ? 'OK' : 'MANQUANT'));
}
} else {
// Import critiques (inchangé)
$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';
$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) {
$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;
}