feat: détection des releases intégrales/complètes dans Prowlarr

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 17:05:38 +02:00
parent e34d7a671a
commit 3a105bbac8
2 changed files with 61 additions and 6 deletions

View File

@@ -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<i32> = 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<i32> = 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 },
})
}
})

View File

@@ -102,6 +102,22 @@ pub(crate) fn extract_volumes_from_title_pub(title: &str) -> Vec<i32> {
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<i32>), 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"));
}
}