39 000 torrents, un benchmark vert et le bug que les données n'ont pas vu

[Torrent911] Inception.2010.TRUEFRENCH.1080p.BluRay.x264-YIFY

Voilà un vrai nom de fichier. L'objectif : en extraire "Inception" et "2010", interroger l'API TMDB (The Movie Database, la référence libre pour les métadonnées de films et séries), et afficher la bonne fiche. Affiche, note, synopsis. Automatiquement, pour une bibliothèque de quelques centaines de fichiers.

Le contexte : un media-server PHP auto-hébergé. Pense Plex, mais allégé, sans cloud, sans compte. L'interface ressemble à Netflix. La difficulté, c'est la reconnaissance. Les noms de releases n'obéissent à aucune norme formelle : [Torrent911] peut précéder le titre, TRUEFRENCH peut l'interrompre, un groupe de release traîne à la fin. Les films ont une année. Les séries, presque jamais. Et les animés populaires ont des numéros d'épisodes à quatre chiffres : One.Piece.S01E1164.VOSTFR.

J'avais accumulé 39 188 vrais noms de torrents sur des années : 22 058 films, 17 130 séries. Assez pour construire un banc de test honnête. J'ai utilisé un assistant IA pour rejouer les variantes rapidement et analyser les fuites dans les métriques.

Deux choses se sont passées. D'abord, une optimisation data-driven propre : mesurer, itérer, s'arrêter au bon moment. Ensuite, un piège classique : le benchmark affichait du vert pendant qu'un bug silencieux transformait toutes mes séries en films.

Ce sont deux leçons différentes sur ce que les données mesurent, et ce qu'elles ne mesurent pas.

Un champ de mines dans une chaîne de caractères

Le pipeline a deux étapes distinctes.

La première : extraire un titre propre et une année depuis le nom de fichier. C'est la fondation. Si elle renvoie "Inception 1080p BluRay x264" au lieu de "Inception", TMDB ne trouvera rien d'utile. La règle de nommage des releases place le titre en tête et les métadonnées techniques après, mais les variantes sont nombreuses : préfixes de site ([Torrent911]), tags de langue (VOSTFR, TRUEFRENCH), codec (x264, H.265), résolution (1080p, 4K), groupes de release.

La deuxième : interroger TMDB avec ce titre et choisir le bon résultat parmi les candidats. Une recherche "Breaking Bad" renvoie des dizaines de résultats, dont des films et des documentaires homonymes. Il faut scorer et trancher.

Pour mesurer l'efficacité de l'extraction, trois métriques : la propreté (% de titres sans tag technique résiduel : un 1080p, un WEB-DL ou un S01E02 qui aurait dû être coupé), la couverture (% de titres non vides), et le rappel d'année (taux d'années correctement extraites). La propreté est la métrique principale : un titre "sale" génère des faux positifs ou des zéro-résultats sur TMDB. La couverture était déjà proche de 100 % dès la V0.

V0 → V4 : mesurer avant d'optimiser

Le banc de test est entièrement offline. Il rejoue l'algorithme d'extraction sur les 39 188 noms du corpus, calcule les métriques, et renvoie un rapport en quelques secondes. Déterministe, 100 % rejouable, sans appel réseau. C'est là que l'assistant IA aide concrètement : construire ce banc, écrire les variantes, analyser les 65 fuites restantes pour identifier les patterns. Pas "l'IA a optimisé l'algo", mais "l'IA a permis d'itérer cinq fois en une heure".

Version Approche Propreté Fuites
V0 — naïf Séparateurs seulement (. et _ → espace) 10,84 % 34 939
V1 — basique Couper 8 tags connus (1080p, BluRay…) 50,23 % 19 492
V2 — couper au 1er tag Tronquer dès le premier marqueur technique rencontré 99,83 % 65
V3 — fix épisodes Pattern épisode étendu à 3-4 chiffres (\d{0,4}) 99,93 % 28
V4 — expérimental Normalisation articles, bracket-rescue… 99,93 % 29

La V0 applique une normalisation basique des séparateurs. 10,84 % de propreté, 34 939 fuites. Baseline catastrophique, mais c'est le point de départ honnête.

La V1 coupe 8 tags techniques connus. 50 %. Le corpus en contient bien plus de huit. Cette approche ne passe pas à l'échelle : chaque site, chaque groupe de release a ses propres conventions.

La V2 introduit l'idée centrale : ne pas lister tous les tags connus, mais tronquer dès le premier tag technique rencontré. La logique de nommage des releases place le titre en tête, les métadonnées techniques après. Couper au premier tag fait bondir la propreté de 10,84 % à 99,83 %. 65 fuites restantes sur 39 000 noms. Ce seul changement de paradigme représente 99 % du résultat total.

// Étape 1 — les préfixes entre crochets partent en premier, sans liste :
// [Torrent911], [HorribleSubs], [FW]... un seul pattern les couvre tous.
$clean = preg_replace('/\[.*?\]/', '', $clean);
$clean = preg_replace('/[._()[\]{}:]+/', ' ', $clean);

// Étape 2 — tout ce qui suit (et inclut) le premier marqueur technique est jeté.
// C'est ce qui fait passer la propreté de 10,8 % à 99,8 %.
$title = preg_replace(
    '/\b(multi|vff|truefrench|french|vostfr|bluray|web-?dl|hdtv|x264|x265|hevc'
  . '|10bit|remux|2160p|1080p|720p|480p|uhd|hdr|dts|aac|ac3|proper|repack'
  . '|extended|directors?-?cut|complete|s\d{1,2}e?\d{0,4}|e\d{2,4})\b.*/i',
    '', $clean
);
// s\d{1,2}e?\d{0,4} : le {0,4} (au lieu de {0,2}) capte les épisodes à 3-4 chiffres
// (One Piece S01E1164, Plus Belle La Vie S12E248) — trouvé en analysant 39 000 torrents.

La V3 règle un cas révélé par l'analyse des 65 fuites restantes. Les numéros d'épisodes à trois ou quatre chiffres n'étaient pas capturés : S01E1164 pour One Piece, S12E248 pour Plus Belle La Vie. Un seul ajustement dans la regex (\d{0,2}\d{0,4}) divise les fuites par deux, de 65 à 28. Impossible à deviner en regardant dix exemples manuellement. Le corpus les a forcées à apparaître dans les statistiques.

La V4 teste une normalisation plus agressive. Résultat : 29 fuites, une de plus que V3. Les données disent stop. Ajouter de la complexité pour régresser, c'est non.

Propreté des titres extraits V0 à V4 — de 10 % à 99,9 % Propreté des titres extraits (% sans tag technique résiduel) 10,84 % V0 naïf 34 939 fuites 50,23 % V1 coupe basique 19 492 fuites 99,83 % V2 coupe 1er tag 65 fuites 99,93 % V3 + fix épisode 28 fuites 99,93 % V4 expérimental 29 fuites
V2 (tronquer au premier tag technique) = 99 % du gain. V3 divise les fuites restantes par deux avec un seul caractère de regex.

Trois leçons de cette partie.

Le gros levier était binaire. Passer de "conserver tout" à "tronquer au premier tag technique" : 10,8 % → 99,8 % de propreté en une seule idée. Le raffinement (V3, V4) vient après, pas avant.

Le big data révèle ce que l'oeil ne voit pas. Sans 39 000 exemples, les numéros d'épisodes à quatre chiffres restaient un angle mort. La masse les a fait apparaître dans les statistiques.

Savoir s'arrêter. La V4 est plus complexe et moins bonne que V3. YAGNI prouvé par les données, pas supposé.

Le matching TMDB et l'optimisation prématurée

Extraire un titre propre, c'est la moitié du travail. L'autre moitié : interroger TMDB et choisir le bon résultat parmi les candidats.

Le scoring combine plusieurs signaux. La similarité de titre compte pour 65 % (comparaison normalisée sur toutes les variantes de titre). L'écart d'année pèse 15 %. La cohérence du type (film ou série) : 10 à 18 % selon la certitude. La popularité TMDB (nombre de votes) : 12 %, pour que les homonymes obscurs perdent face aux oeuvres connues. Au-dessus de 55, c'est un match fort. Entre 35 et 55, mise en quarantaine pour revue manuelle. En dessous, rejeté.

Quand la requête exacte ne donne pas de bon résultat, l'algo la raccourcit mot par mot jusqu'au seuil de confiance. Un titre avec des sous-titres ou une ponctuation inhabituelle peut ne pas matcher directement : on retire les mots par la fin, un par un, et on garde la requête la plus longue qui dépasse le seuil.

// Les releases collent souvent un sous-titre ou une langue après le vrai titre.
// On retire les mots PAR LA FIN, un par un, et on garde la requête la PLUS LONGUE
// qui dépasse le seuil de confiance (55). Le titre est toujours en tête.
$words    = explode(' ', trim($title));
$minWords = max(1, count($words) - 4);   // au plus 5 essais
$best = null; $bestScore = 0;
for ($n = count($words); $n >= $minWords; $n--) {
    $query = implode(' ', array_slice($words, 0, $n));
    foreach (tmdb_search($query, $year, $preferTv) as $cand) {
        $score = score_candidate($query, $year, $cand, $preferTv);
        if ($score > $bestScore) { $bestScore = $score; $best = $cand; }
    }
    if ($bestScore >= 55) break; // match fiable → on arrête de tronquer
}

Ça règle les cas comme "Avengers: Age of Ultron" (les deux-points disparaissent au nettoyage) ou "Le Voyage de Chihiro" (titre localisé qui ne colle pas au nom de fichier anglais). Chaque itération coûte un appel API, mais elle évite les zéro-match sur des titres avec ponctuation ou caractères spéciaux.

C'est là qu'une "optimisation" s'était glissée dans le code. La variable $preferTv était censée signaler à TMDB de prioriser les séries. Pour économiser des appels API, j'avais configuré l'algo pour interroger en priorité le type "probable" (movie par défaut, sauf si la série était explicitement détectée dans le nom de fichier), et n'interroger le type tv qu'en l'absence totale de candidats. Le banc de test réseau affichait des taux flatteurs : 90 % de match pour les films, 96,7 % pour les séries.

Je l'ai navigué en conditions réelles. Toutes mes séries matchaient un film.

  • Breaking Bad → Breaking Bad Wolf (un film indépendant homonyme)
  • Game of Thrones → Game of Thrones: The Last Watch (un documentaire HBO)
  • The Mandalorian → The Mandalorian and Grogu (un film annoncé)
  • One Piece → One Piece: Gold (un long-métrage)

0 série correcte sur 11.

Pourquoi le benchmark affichait du vert

Le banc de test offline ne mesurait que l'extraction. Normal, c'est son périmètre. Il ne savait rien du matching.

Le banc de test réseau, lui, fournissait le type de contenu (movie ou tv) depuis la catégorie connue de chaque entrée du corpus. Il passait directement $preferTv = true pour les séries. Il n'exerçait donc jamais la détection automatique de type, parce qu'il n'en avait pas besoin : la bonne réponse était déjà injectée dans les paramètres. La détection automatique qui tournait en production n'avait jamais été exercée. La métrique agrégée était verte sur un chemin que la navigation normale n'empruntait pas.

Un benchmark teste ce qu'on lui demande de tester. Ses angles morts sont silencieux.

Avant et après le fix : détection du type film/série pour le matching TMDB AVANT « Breaking Bad » type = film (auto-estimé) TMDB : films uniquement Breaking Bad Wolf ✗ faux positif APRÈS « Breaking Bad » movie + tv en parallèle scoring tranche par popularité Breaking Bad (série) ✓ match correct
L'optimisation "économiser un appel API" avait retiré tv du circuit. Un scoring ne peut pas choisir entre deux types s'il n'en voit qu'un.

Le fix en 4 leviers

1. Toujours interroger les deux types.

Movie et tv sont maintenant interrogés à chaque fois. Le scoring tranche. Breaking Bad (série TMDB, popularité élevée) écrase n'importe quel film homonyme obscur par son seul score de popularité pondéré. C'était la régression centrale : économiser un appel API avait cassé la logique de sélection. Le correctif est littéralement d'enlever la condition qui limitait le deuxième type.

2. La structure du dossier comme signal.

Parser le nom de fichier pour deviner film ou série, c'est fragile. Un dossier structuré en saisons — "Show/Season 1/", "Show/01/", "Show/02/" — ne contient aucun S01E01 dans son nom propre. La structure du dossier, elle, ne ment pas.

// Un dossier est une SÉRIE s'il contient plusieurs épisodes (≥ 2 vidéos à plat)
// OU des sous-dossiers de saison / numérotés (Season 1, Saison 3, 01, 02…).
// Un film n'a qu'une seule vidéo. La FORME du dossier bat le parsing du nom de fichier.
$videoCount = 0; $seasonish = 0;
foreach (scandir($folder) as $sub) {
    if ($sub[0] === '.') continue;
    if (is_dir("$folder/$sub")) {
        if (preg_match('/^(s\d{1,2}\b|saison|season|saga|arc|part|\d{1,3}$)/i', $sub))
            $seasonish++;
    } elseif (in_array(strtolower(pathinfo($sub, PATHINFO_EXTENSION)), $VIDEO_EXTS, true)) {
        $videoCount++;
    }
}
$isSeries = ($videoCount >= 2 || $seasonish >= 1);

Ce signal est utilisé pour pondérer le bonus de cohérence de type dans le scoring. Il ne remplace pas la double interrogation, il l'affine.

3. Fusion bilingue des titres.

TMDB renvoie plusieurs variantes de titre par résultat : titre original, titre local, titre alternatif. Quand on cherche "Attack on Titan", TMDB peut renvoyer "L'Attaque des Titans" (français) et "Shingeki no Kyojin" (japonais). La déduplication par ID ne gardait que le premier titre rencontré : faible similarité avec la requête anglaise, match potentiellement raté.

Fix : agréger toutes les variantes de titre par ID et scorer contre la meilleure. Bonus inattendu : Spirited Away est passé de match faible à match fort, car "Sen to Chihiro no Kamikakushi" (titre japonais original) était dans les données TMDB et contribuait au score de similarité.

4. Bonus de cohérence de type renforcé.

Quand la structure du dossier confirme clairement une série (sous-dossiers numérotés, dizaines de fichiers vidéo), le bonus de type tv dans le scoring passe de 10 à 18 %. Le signal structurel amplifie la décision du scoring sans la court-circuiter.

Résultat : 11 séries et animés corrects en haute confiance, 18 films restent des films. 0 → 11 sur les séries.

Code source complet
ShareBox est open source. L'algorithme décrit dans cet article se trouve dans functions.php : extract_title_year() (extraction), tmdb_match() (boucle word-removal), tmdb_score_candidate() (scoring).

Conclusion

Le big data a brillamment optimisé l'extraction. De 10,8 % à 99,9 % de propreté en trois itérations mesurées, sur 39 000 noms. Les données ont révélé un angle mort (S01E1164) qu'aucun humain ne teste manuellement, et elles ont dit stop quand une variante plus complexe régressait d'une unité.

Le matching a raté différemment. Une économie d'appels API a causé une régression invisible aux métriques agrégées parce que le benchmark s'alimentait lui-même de la bonne réponse. La navigation en conditions réelles a trouvé le bug en trente secondes.

La vraie leçon : les données optimisent brillamment ce qu'elles mesurent. Un benchmark mesure un chemin. Il ne sait pas qu'il en existe d'autres. La métrique agrégée était un indicateur honnête ; elle n'était pas un indicateur complet. Tester en conditions réelles reste irremplaçable, même quand tout est vert.

Commentaires (0)