Files
mon-petit-cinema/api.php
T
2026-07-01 11:20:34 +02:00

945 lines
41 KiB
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 ---
// Récupère le token d'authentification envoyé par le client, en tolérant les
// configurations Apache/WAMP qui ne transmettent pas HTTP_AUTHORIZATION à PHP
// par défaut (l'en-tête est bien envoyé par le navigateur, mais Apache le
// "mange" avant qu'il n'atteigne $_SERVER, sauf si CGIPassAuth est activé).
function getAuthToken() {
if (!empty($_SERVER['HTTP_AUTHORIZATION'])) {
return $_SERVER['HTTP_AUTHORIZATION'];
}
// Cas fréquent avec RewriteRule / certains proxys : le header est déplacé ici
if (!empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
return $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
}
// Filet de sécurité générique : relire les en-têtes bruts de la requête
if (function_exists('getallheaders')) {
foreach (getallheaders() as $name => $value) {
if (strcasecmp($name, 'Authorization') === 0) {
return $value;
}
}
} elseif (function_exists('apache_request_headers')) {
foreach (apache_request_headers() as $name => $value) {
if (strcasecmp($name, 'Authorization') === 0) {
return $value;
}
}
}
return '';
}
function checkAuth($pdo) {
if ($pdo->query("SELECT COUNT(*) FROM users")->fetchColumn() == 0) return true;
$token = getAuthToken();
if ($token !== md5(ENCRYPTION_KEY . 'session')) {
error_log("Auth: ❌ Token invalide ou absent (HTTP_AUTHORIZATION reçu: " . (empty($token) ? "VIDE — vérifier CGIPassAuth/config Apache" : "présent mais différent") . ")");
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;
}
// Vérifie qu'une URL d'image répond bien en 200 (utilisé pour valider les URLs
// de couverture "devinées" à partir d'un ID, sans avoir à parser du HTML fragile)
function urlExists($url) {
if (empty($url)) return false;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_NOBODY => true, // requête HEAD
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 8,
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',
]);
curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $code === 200;
}
// Retire les accents/diacritiques et normalise une chaîne pour construire
// une URL de type moviecovers.com (ex: "L'Étrange Noël" -> "L ETRANGE NOEL")
function removeAccentsForUrl($str) {
$str = str_replace(['œ', 'Œ'], ['oe', 'OE'], $str);
$str = str_replace(['æ', 'Æ'], ['ae', 'AE'], $str);
// Translittère les accents (fonctionne même si l'extension intl est absente)
$transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str);
if ($transliterated !== false && $transliterated !== '') {
$str = $transliterated;
}
return $str;
}
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' => ''
];
}
function makeStableId($type, $title, $year) {
return (abs(crc32(strtolower(trim($type ?? '')) . '|' . strtolower(trim($title ?? '')) . '|' . trim($year ?? ''))) % 2000000000) + 100000000;
}
// ── 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");
$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
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");
sleep(2); // Délai avant la 2ème requête
$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 AVEC LES NOUVELLES REGEX ──
// Titre (dans <h3> inside #movie_info)
if (preg_match('/<div[^>]*id="movie_info"[^>]*>.*?<h3>([^<]+)<\/h3>/is', $movieHtml, $m)) {
$empty['title'] = trim($m[1]);
} elseif (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 (juste après le <h3>)
if (preg_match('/<h3[^>]*>([^<]+)<\/h3>\s*(?:&nbsp;)?\((\d{4})\)/i', $movieHtml, $m)) {
$empty['year'] = $m[2];
}
// Studio/Éditeur
if (preg_match('/href="[^"]*studioid=\d+[^"]*"[^>]*>([^<]+)<\/a>/i', $movieHtml, $m)) {
$empty['publisher'] = trim($m[1]);
}
// Durée (dans un span avant "Rated")
if (preg_match('/(\d+)\s*min<\/span>/i', $movieHtml, $m)) {
$empty['length'] = $m[1] . ' min';
} elseif (preg_match('/(\d+)\s*min\s*\|\s*Rated/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 (gère "Three-disc set" en plus des chiffres)
if (preg_match('/(\w+)-disc\s+set/i', $movieHtml, $m)) {
$wordToNum = ['single' => 1, 'one' => 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
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 (dans #movie_info)
if (preg_match('/<div[^>]*id="movie_info"[^>]*>(.*?)<div[^>]*id="movie_review_intro"/is', $movieHtml, $infoBlock)) {
$infoHtml = $infoBlock[1];
// Synopsis (après la balise <font> vide)
if (preg_match('/<font[^>]*><br><\/font>\s*(.*?)<br><br><br>Directors:/is', $infoHtml, $m)) {
$synopsis = trim(strip_tags($m[1]));
$synopsis = preg_replace('/\s+/', ' ', $synopsis);
$empty['description'] = $synopsis;
}
// Réalisateur (extrait depuis les balises <a>)
if (preg_match('/Directors?:\s*(.*?)(?:<br>|<\/div>)/is', $infoHtml, $m)) {
preg_match_all('/<a[^>]*>([^<]+)<\/a>/i', $m[1], $dirMatches);
if (!empty($dirMatches[1])) {
$empty['director'] = implode(', ', array_map('trim', array_slice($dirMatches[1], 0, 2)));
}
}
// Acteurs (extrait depuis les balises <a>)
if (preg_match('/Starring:\s*(.*?)(?:<br>|<\/div>)/is', $infoHtml, $m)) {
preg_match_all('/<a[^>]*>([^<]+)<\/a>/i', $m[1], $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;
}
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 l'URL (les espaces deviennent des +)
$urlTitle = strtoupper(str_replace(' ', '+', $title));
$url = "https://moviecovers.com/film/titre_{$urlTitle}.html";
$html = httpGet($url, 10);
if (!$html) return $empty;
// Extraire le titre
if (preg_match('/<meta property="og:title" content="([^"]+)"/i', $html, $m)) {
$empty['title'] = trim($m[1]);
} elseif (preg_match('/<TITLE>([^<]+)<\/TITLE>/i', $html, $m)) {
$empty['title'] = trim($m[1]);
}
// Extraire l'IDMC pour construire l'URL de l'image HD (comme dans votre script Python)
$idmc = '';
if (preg_match('/IDMC.*?<PRE>([^<]+)<\/PRE>/is', $html, $m)) {
$idmc = trim($m[1]);
} elseif (preg_match('/<meta property="og:image" content="[^"]*\/getjpg\.html\/([^"\']+)\.jpg"/i', $html, $m)) {
$idmc = urldecode($m[1]);
}
// Essayer de récupérer l'affiche HD via les URLs du script Python
if ($idmc) {
$idmc_encoded = urlencode($idmc);
$hd_urls = [
"https://moviecovers.com/DATA/zipcache/{$idmc_encoded}.jpg",
"https://moviecovers.com/getjpg.html/{$idmc_encoded}.jpg"
];
foreach ($hd_urls as $hd_url) {
$img_data = httpGet($hd_url, 5);
if ($img_data && strlen($img_data) > 5000) { // Vérifie que c'est une vraie image (>5Ko)
$empty['poster'] = $hd_url;
break;
}
}
}
// Fallback sur le thumbnail si pas d'image HD trouvée
if (empty($empty['poster'])) {
// CORRECTION : La regex gère le cas où title= est avant src=
if (preg_match('/<img[^>]*title="Recto[^"]*"[^>]*src="([^"]+)"/i', $html, $m)) {
$empty['poster'] = $m[1];
} elseif (preg_match('/<img[^>]*src="([^"]+)"[^>]*title="Recto/i', $html, $m)) {
$empty['poster'] = $m[1];
} elseif (preg_match('/<meta[^>]*property="og:image"[^>]*content="([^"]+)"/i', $html, $m)) {
$empty['poster'] = $m[1];
}
}
// Extraire le réalisateur
if (preg_match('/R&eacute;alisateur<\/TH>\s*<TD[^>]*>.*?<a[^>]*>([^<]+)<\/a>/is', $html, $m)) {
$empty['director'] = trim($m[1]);
}
// Extraire l'année
if (preg_match('/Ann&eacute;e<\/TH>\s*<TD[^>]*>.*?<a[^>]*>(\d{4})<\/a>/is', $html, $m)) {
$empty['year'] = $m[1];
}
// Extraire la durée
if (preg_match('/Dur&eacute;e<\/TH>\s*<TD[^>]*>([\dH]+[\dmin]*)/is', $html, $m)) {
$empty['length'] = trim($m[1]);
}
// Extraire les acteurs
if (preg_match_all('/<a href="\/multicrit\.html\?acteur=[^"]*">([^<]+)<\/a>/i', $html, $matches)) {
$empty['actors'] = implode(', ', array_slice($matches[1], 0, 5));
}
// Extraire le synopsis
if (preg_match('/<meta[^>]*property="og:description"[^>]*content="([^"]+)"/i', $html, $m)) {
$empty['description'] = html_entity_decode($m[1]);
}
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;
case 'import_batch':
checkAuth($pdo);
$data = json_decode(file_get_contents("php://input"), true);
$type = $data['type'] ?? '';
$items = $data['items'] ?? [];
if ($type !== 'videotheque') {
// Conserver l'ancien comportement pour les critiques (inchangé)
// ...
break;
}
// Caches en mémoire pour cette session d'import
static $blurayCache = [];
static $movieCoversCache = [];
static $tmdbCache = [];
$pdo->beginTransaction();
$imported = 0;
$skipped = 0;
try {
$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) {
// Nettoyage de l'EAN et récupération du titre CSV
$ean = preg_replace('/[^0-9]/', '', (string)($item['ean'] ?? ''));
$csvTitle = trim($item['title'] ?? '');
// Si aucun titre et aucun EAN, on ignore
if (empty($csvTitle) && strlen($ean) < 8) {
$skipped++;
continue;
}
// Étape 1 : Essayer de récupérer les infos depuis Blu-ray.com (si EAN valide)
$blurayData = [];
if (strlen($ean) >= 8) {
$cacheKey = 'bluray_' . $ean;
if (!isset($blurayCache[$cacheKey])) {
$blurayCache[$cacheKey] = fetchFromBlurayCom($ean);
}
$blurayData = $blurayCache[$cacheKey];
}
// Titre final : on prend celui de Blu-ray.com s'il existe, sinon celui du CSV
$title = !empty($blurayData['title']) ? $blurayData['title'] : $csvTitle;
if (empty($title)) {
$skipped++;
continue;
}
// Remplir les champs avec les données de Blu-ray.com (ou valeurs par défaut)
$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'] ?? '';
// Étape 2 : Compléter avec MovieCovers.com (toujours, car il fonctionne bien avec les titres)
$cacheKey = 'moviecovers_' . strtolower(trim($title));
if (!isset($movieCoversCache[$cacheKey])) {
$movieCoversCache[$cacheKey] = fetchFromMovieCovers($title, $year);
}
$mcData = $movieCoversCache[$cacheKey];
if (!empty($mcData)) {
if (empty($poster) || $poster === 'assets/img/default_physical_media.jpg') {
$poster = $mcData['poster'] ?? $poster;
}
if (empty($director)) $director = $mcData['director'] ?? '';
if (empty($actors)) $actors = $mcData['actors'] ?? '';
if (empty($desc)) $desc = $mcData['description'] ?? '';
if (empty($length) && !empty($mcData['length'])) $length = $mcData['length'];
if (empty($year) && !empty($mcData['year'])) $year = $mcData['year'];
// Option : on peut aussi améliorer le titre si MovieCovers en donne un plus propre
if (!empty($mcData['title'])) {
$title = cleanTitle($mcData['title']);
}
}
// Étape 3 : Fallback TMDB pour ce qui manque encore
$cacheKey = 'tmdb_' . strtolower(trim($title));
if (!isset($tmdbCache[$cacheKey])) {
$tmdbCache[$cacheKey] = fetchTmdbPosterAndSynopsis($title, $year, $pdo);
}
$tmdbData = $tmdbCache[$cacheKey];
if (!empty($tmdbData)) {
if (empty($poster) || $poster === 'assets/img/default_physical_media.jpg') {
$poster = $tmdbData['poster'] ?? $poster;
}
if (empty($director)) $director = $tmdbData['director'] ?? '';
if (empty($actors)) $actors = $tmdbData['actors'] ?? '';
if (empty($desc)) $desc = $tmdbData['description'] ?? '';
if (empty($length) && !empty($tmdbData['length'])) $length = $tmdbData['length'];
if (empty($year) && !empty($tmdbData['year'])) $year = $tmdbData['year'];
}
// Génération d'un ID stable
$id = makeStableId('videotheque', $title, $year);
// Insertion
$stmt->execute([
$id, $title, $year, $format, $poster, $ean, $desc, $length,
$discs, $aspect, $actors, $publisher, $director
]);
$imported++;
}
$pdo->commit();
echo json_encode(["success" => true, "imported" => $imported, "skipped" => $skipped]);
} catch (Throwable $e) {
$pdo->rollBack();
error_log("import_batch error: " . $e->getMessage() . " ligne " . $e->getLine());
http_response_code(500);
echo json_encode(["success" => false, "error" => $e->getMessage(), "line" => $e->getLine()]);
}
break;
}