import re import unicodedata def normalize_title(title): if not title: return "" # Remove accents title = unicodedata.normalize('NFD', title).encode('ascii', 'ignore').decode('utf-8') # Remove punctuation and special characters, replace with space title = re.sub(r'[^\w\s]', ' ', title) # lowercase and normalize spaces return " ".join(title.lower().split()) def check_title_match(torrent_name, title_fr, title_en, year=None, is_movie=False): """ Vérifie si le titre français ou anglais est présent dans le nom du torrent. Amélioré pour détecter les faux positifs (ex: Narcos match Narcos Mexico). """ if not title_fr and not title_en: return True norm_torrent = normalize_title(torrent_name) norm_fr = normalize_title(title_fr) norm_en = normalize_title(title_en) # Mots qui peuvent suivre un titre sans que ce soit un autre média tech_tags = { '1080p', '720p', '4k', '2160p', 'uhd', 'dvd', 'sd', 'bluray', 'brrip', 'bdrip', 'webrip', 'web', 'webdl', 'dvdrip', 'cam', 'ts', 'vff', 'vf', 'vfq', 'vf2', 'vostfr', 'multi', 'truefrench', 'french', 'eng', 'english', 'en', 'vo', 'fr', 'x264', 'x265', 'hevc', 'h264', 'h265', 'av1', 'hdr', 'dv', 'dolby', 'vision', '10bit', 'light', 'repack', 'proper', 'internal', 'extended', 'uncut', 'subfrench', 'subforced', 'nf', 'netflix', 'amzn', 'amazon', 'dnp', 'dsnp', 'hmax', 'ac3', 'dts', 'dd5', 'dd2', 'aac', 'mkv', 'mp4', 'avi', 'tv', 'show', 'complete', 'integrale', 'integra', 'vol', 'volume', 'part', 'party', 'uncut', 'dual', 'hdtv' } def is_strict_match(target, torrent): if not target: return False # On cherche le titre exact avec bordures de mots pattern = r'\b' + re.escape(target) + r'\b' match = re.search(pattern, torrent) if not match: return False # On vérifie ce qui suit immédiatement le titre end_idx = match.end() after_text = torrent[end_idx:].strip() if not after_text: return True # Titre exact à la fin # On prend le prochain mot next_word = after_text.split()[0] # Si le prochain mot est une année, un épisode (s01, 1x01) ou un tag technique -> OK if re.match(r'^(19|20)\d{2}$', next_word): # Année return True if re.match(r'^[sx]\d+$', next_word): # S01, x01 return True if re.match(r'^s\d+e\d+$', next_word): # S01E01 return True if next_word in tech_tags: return True # Si le prochain mot fait partie de l'AUTRE titre (ex: "Narcos" vs "Narcos Mexico") # Si on cherche "Narcos" (FR) et que EN est "Narcos Mexico", et que le torrent a "Mexico" -> OK # Mais ici on vérifie si next_word est dans norm_fr ou norm_en if next_word in norm_fr.split() or next_word in norm_en.split(): return True # Si c'est un mot inconnu (ex: "Mexico" alors que la cible est juste "Narcos") -> Faux positif probable return False title_match = is_strict_match(norm_fr, norm_torrent) or is_strict_match(norm_en, norm_torrent) if not title_match: return False if is_movie and year: try: y = int(year) # Pour les films, l'année est cruciale pour éviter les remakes/suites if str(y) not in norm_torrent and str(y-1) not in norm_torrent and str(y+1) not in norm_torrent: return False except ValueError: if str(year) not in norm_torrent: return False return True def format_size(size_bytes): """Formate une taille en octets vers une chaine lisible (Go, Mo)""" try: size = float(size_bytes) except (ValueError, TypeError): return "0 B" if size >= 1024**3: return f"{size / (1024**3):.2f} Go" elif size >= 1024**2: return f"{size / (1024**2):.2f} Mo" else: return f"{size / 1024:.2f} Ko" def is_video_file(filename): """ Vérifie si le fichier/torrent semble être une vidéo. On exclut les extensions non-vidéo connues (.iso, .pdf, .epub, .zip, etc.). Pour les torrents, le nom n'a souvent pas d'extension, on l'accepte par défaut. """ if not filename: return False filename_lower = filename.lower() # Liste des extensions non-vidéo à exclure absolument bad_extensions = ( '.iso', '.pdf', '.epub', '.txt', '.nfo', '.jpg', '.jpeg', '.png', '.rar', '.zip', '.7z', '.tar', '.gz', '.exe', '.doc', '.docx' ) if filename_lower.endswith(bad_extensions): return False # Liste des extensions vidéo connues (pour acceptation immédiate) video_extensions = ( '.mkv', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.mpg', '.mpeg', '.ts', '.m2ts', '.vob', '.divx' ) if filename_lower.endswith(video_extensions): return True # Si le nom contient un point suivi de 2-4 caractères à la fin, # c'est probablement une extension inconnue (et potentiellement non-vidéo) # Mais attention, beaucoup de torrents ont des points dans le nom (ex: Movie.Name.1080p) # On ne filtre PAS si ça ressemble à un titre de torrent standard (sans extension de fichier à la fin) # Si le nom se termine par une extension (3-4 caractères après le dernier point) # qui n'est pas dans notre liste de vidéos, on pourrait être suspicieux. # Mais pour l'instant, on va rester permissif : on ne bloque que les "bad_extensions". return True def parse_torrent_name(name): """Analyse le nom du torrent pour extraire qualité, langue, codec et type de release""" if not name: return {"name": "", "quality": "", "codec": "", "language": "", "release_type": ""} name_upper = name.upper() # Qualité quality = "" if any(q in name_upper for q in ["2160P", "4K", "UHD"]): quality = "4K" elif "1080P" in name_upper: quality = "1080p" elif "720P" in name_upper: quality = "720p" elif any(q in name_upper for q in ["480P", "SD", "DVD"]): quality = "SD" # Codec codec = "" if any(c in name_upper for c in ["X265", "HEVC", "H265"]): codec = "x265" elif any(c in name_upper for c in ["X264", "AVC", "H264"]): codec = "x264" elif "AV1" in name_upper: codec = "AV1" # HDR / Bitrate extras = [] if "HDR" in name_upper: extras.append("HDR") if any(dv in name_upper for dv in ["DV", "DOLBY VISION"]): extras.append("DV") if "10BIT" in name_upper: extras.append("10bit") # Release Type release_type = "" if "WEBRIP" in name_upper: release_type = "WebRIP" elif "WEB-DL" in name_upper or "WEBDL" in name_upper: release_type = "WEB-DL" elif re.search(r'\bWEB\b', name_upper): release_type = "WEB-DL" elif any(br in name_upper for br in ["BRRIP", "BDRIP", "BLURAY", "BDLIGHT"]): release_type = "BDRip" elif "DVDRIP" in name_upper: release_type = "DVDRip" elif re.search(r'\bCAM\b', name_upper) or re.search(r'\bHDTS\b|\bHD-TS\b|\bTS\b', name_upper): release_type = "CAM" # Langues languages = [] if "MULTI" in name_upper: languages.append("Multi") if "VOSTFR" in name_upper: languages.append("VOSTFR") if "TRUEFRENCH" in name_upper or "VFF" in name_upper: languages.append("VFF") elif "VF2" in name_upper: languages.append("VF2") elif "VFQ" in name_upper: languages.append("VFQ") elif "FRENCH" in name_upper or "VF" in name_upper: languages.append("VF") # Si rien de détecté mais original_title != title if not languages and ("EN" in name_upper or "VO" in name_upper or "ENG" in name_upper): languages.append("VO") # Formatage de l'affichage classique (title) display_langs = [] for lang in languages: if lang == "Multi": display_langs.append("🇫🇷+🇺🇸 MULTI") elif lang == "VFF": display_langs.append("🇫🇷 VFF") elif lang == "VF": display_langs.append("🇫🇷 VF") elif lang == "VFQ": display_langs.append("🇨🇦 VFQ") elif lang == "VOSTFR": display_langs.append("🇫🇷🇯🇵 VOSTFR") elif lang == "VO": display_langs.append("🇺🇸 VO") title_parts = [] if quality: title_parts.append(f"📺 {quality}") if release_type: title_parts.append(f"📦 {release_type}") if codec: title_parts.append(f"🎞️ {codec}") if extras: title_parts.append(f"✨ {' '.join(extras)}") if display_langs: title_parts.append(f"{' '.join(display_langs)}") return { "name": " | ".join(title_parts), "quality": quality, "codec": codec, "language": ", ".join(languages) if languages else "Français", "release_type": release_type } STOPWORDS_TITLE = {'le', 'la', 'les', 'de', 'des', 'du', 'the', 'of', 'no', 'and', 'et', 'wa', 'to', 'ni'} def check_title_tokens(name, *titles): """ Vérification de titre souple pour les nommages fansub (Nyaa/NekoBT) : tous les mots significatifs d'un des titres doivent apparaître entiers dans le nom du torrent. Évite les hors-sujet du type "Monster" qui matche "Pocket Monsters" via un pack/range d'épisodes. """ name_words = set(normalize_title(name).split()) for title in titles: if not title: continue tokens = [w for w in normalize_title(title).split() if len(w) >= 2 and w not in STOPWORDS_TITLE] if tokens and all(tok in name_words for tok in tokens): return True return False def check_special_episode(name, target_episode, exclude_packs=False): """ Vérifie si le torrent correspond à un épisode spécial/OVA (saison 0) au nommage fansub : "Titre OVA 06 VOSTFR", "OAV 1+2", "S01 + OAV", ou le classique "S00E01". """ if check_season_episode(name, 0, target_episode, exclude_packs=exclude_packs): return True name_upper = name.upper() if not re.search(r'\b(?:OVA|OAV|OAD|SPECIALS?|SP)\b', name_upper): return False # Numéros accolés au tag : "OVA 06", "OAV 1+2", "Special 3" nums = re.findall(r'\b(?:OVA|OAV|OAD|SPECIALS?|SP)\b[ ._#-]*0*(\d{1,3})(?:[ ._+~-]+0*(\d{1,3}))?', name_upper) if not nums: # Tag OVA sans numéro : pack d'OVA ou bundle ("S01 + OAV") return not exclude_packs for start, end in nums: try: s = int(start) e = int(end) if end else s if s > e: s, e = e, s if s != e and exclude_packs: continue if s <= target_episode <= e: return True except ValueError: continue return False def check_absolute_episode(name, absolute_episode, exclude_packs=False): """ Vérifie si le torrent correspond à l'épisode en numérotation absolue, utilisée par les fansubs anime (ex: "One Piece S01E1122", "One Piece - 1122 VOSTFR", ou un pack "One Piece 1100-1150"). """ if absolute_episode is None: return False name_upper = name.upper() # SxxEyyyy : numérotation absolue déguisée en saison 1 (ex: S01E1122, avec ranges S01E1100-1150) se_pattern = re.compile(r'(?:S|SAISON|SEASON)[ ._-]?0?1[ ._-]?E(\d{1,4})(?:[ ._-]*(?:E|-|~)[ ._-]*(\d{1,4}))?', re.IGNORECASE) for e_start, e_end in se_pattern.findall(name_upper): try: start = int(e_start) end = int(e_end) if e_end else start if end < start: continue if start < end and exclude_packs: continue if start <= absolute_episode <= end: return True except ValueError: continue # Ranges nus (packs) : "1100-1150" if not exclude_packs: range_pattern = re.compile(r'(? OK sauf si on exclut les packs if e_start is None: if exclude_packs: return False return True start = int(e_start) end = int(e_end) if e_end else start # Vérification de l'épisode (dans le range) if start <= target_episode <= end: return True except ValueError: continue return False