From 3a105bbac8a313b4001dd57dfcb3129f632407d8 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Sun, 29 Mar 2026 17:05:38 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20d=C3=A9tection=20des=20releases=20int?= =?UTF-8?q?=C3=A9grales/compl=C3=A8tes=20dans=20Prowlarr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajoute is_integral_release() pour détecter "intégrale", "complet", "complete", "integral" dans les titres de torrents - Les releases intégrales matchent tous les volumes manquants d'une série - 4 tests unitaires (français, anglais, casse, faux positifs) Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/src/download_detection.rs | 21 ++++++++++---- apps/api/src/prowlarr.rs | 46 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/apps/api/src/download_detection.rs b/apps/api/src/download_detection.rs index 98e988f..807f3b5 100644 --- a/apps/api/src/download_detection.rs +++ b/apps/api/src/download_detection.rs @@ -808,11 +808,20 @@ async fn search_prowlarr_for_series( .into_iter() .filter_map(|r| { let title_volumes = prowlarr::extract_volumes_from_title_pub(&r.title); - let matched_vols: Vec = title_volumes - .iter() - .copied() - .filter(|v| missing_volumes.contains(v)) - .collect(); + + // "Intégrale" / "Complet" releases match ALL missing volumes + let is_integral = prowlarr::is_integral_release(&r.title); + + let matched_vols: Vec = if is_integral && !missing_volumes.is_empty() { + missing_volumes.to_vec() + } else { + title_volumes + .iter() + .copied() + .filter(|v| missing_volumes.contains(v)) + .collect() + }; + if matched_vols.is_empty() { None } else { @@ -823,7 +832,7 @@ async fn search_prowlarr_for_series( indexer: r.indexer, seeders: r.seeders, matched_missing_volumes: matched_vols, - all_volumes: title_volumes, + all_volumes: if is_integral { vec![] } else { title_volumes }, }) } }) diff --git a/apps/api/src/prowlarr.rs b/apps/api/src/prowlarr.rs index 0efe17d..08e304b 100644 --- a/apps/api/src/prowlarr.rs +++ b/apps/api/src/prowlarr.rs @@ -102,6 +102,22 @@ pub(crate) fn extract_volumes_from_title_pub(title: &str) -> Vec { extract_volumes_from_title(title) } +/// Returns true if the title indicates a complete/integral edition +/// (e.g., "intégrale", "complet", "complete", "integral"). +pub(crate) fn is_integral_release(title: &str) -> bool { + let lower = title.to_lowercase(); + // Strip accents for matching: "intégrale" → "integrale" + let normalized = lower + .replace('é', "e") + .replace('è', "e"); + let keywords = ["integrale", "integral", "complet", "complete", "l'integrale"]; + keywords.iter().any(|kw| { + // Match as whole word: check boundaries + normalized.split(|c: char| !c.is_alphanumeric() && c != '\'') + .any(|word| word == *kw) + }) +} + async fn load_prowlarr_config( pool: &sqlx::PgPool, ) -> Result<(String, String, Vec), ApiError> { @@ -711,4 +727,34 @@ mod tests { let v = extract_volumes_from_title("Naruto T05 - some 99 extra.cbz"); assert_eq!(v, vec![5], "should only find T05, not bare 99"); } + + use super::is_integral_release; + + #[test] + fn integral_french_accent() { + assert!(is_integral_release("One Piece - Intégrale [CBZ]")); + assert!(is_integral_release("Naruto Integrale FR")); + } + + #[test] + fn integral_complet() { + assert!(is_integral_release("Dragon Ball Complet [PDF]")); + assert!(is_integral_release("Bleach Complete Edition")); + } + + #[test] + fn integral_not_false_positive() { + assert!(!is_integral_release("One Piece T05")); + assert!(!is_integral_release("Naruto Tome 12")); + assert!(!is_integral_release("Les Géants - 07 - Moon.cbz")); + // "intégr" alone is not enough + assert!(!is_integral_release("Naruto integration test")); + } + + #[test] + fn integral_case_insensitive() { + assert!(is_integral_release("INTEGRALE")); + assert!(is_integral_release("COMPLET")); + assert!(is_integral_release("Intégrale")); + } }