2025-10-27 13:17:09 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
#
|
|
|
|
|
##############################################################################
|
|
|
|
|
### NZBGET POST-PROCESSING SCRIPT ###
|
|
|
|
|
#
|
|
|
|
|
# Corrige les problèmes d'encodage dans les noms de fichiers.
|
|
|
|
|
#
|
|
|
|
|
# Ce script détecte et corrige les noms de fichiers qui ont été mal encodés
|
|
|
|
|
# (UTF-8 interprété comme ISO-8859-1/Windows-1252) et les renomme correctement.
|
|
|
|
|
#
|
|
|
|
|
# NOTE: Ce script utilise Python 3.
|
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
##############################################################################
|
|
|
|
|
### OPTIONS ###
|
|
|
|
|
|
|
|
|
|
# Activer le mode débogage (affiche plus d'informations dans les logs).
|
|
|
|
|
#
|
|
|
|
|
# yes, no.
|
|
|
|
|
#Debug=no
|
|
|
|
|
|
|
|
|
|
# Extensions de fichiers à traiter (séparées par des virgules).
|
|
|
|
|
# Laisser vide pour traiter tous les fichiers.
|
|
|
|
|
#
|
|
|
|
|
# Exemples: .flac,.mp3,.mkv,.mp4
|
|
|
|
|
#FileExtensions=
|
|
|
|
|
|
|
|
|
|
# Mode de simulation (ne renomme pas les fichiers, affiche seulement ce qui serait fait).
|
|
|
|
|
#
|
|
|
|
|
# yes, no.
|
|
|
|
|
#DryRun=no
|
|
|
|
|
|
|
|
|
|
### NZBGET POST-PROCESSING SCRIPT ###
|
|
|
|
|
##############################################################################
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Exit codes utilisés par NZBGet
|
|
|
|
|
POSTPROCESS_SUCCESS = 93
|
|
|
|
|
POSTPROCESS_ERROR = 94
|
|
|
|
|
POSTPROCESS_NONE = 95
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_debug():
|
|
|
|
|
"""Vérifie si le mode debug est activé."""
|
|
|
|
|
return os.environ.get('NZBPO_DEBUG', 'no').lower() == 'yes'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_dry_run():
|
|
|
|
|
"""Vérifie si le mode simulation est activé."""
|
|
|
|
|
return os.environ.get('NZBPO_DRYRUN', 'no').lower() == 'yes'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_file_extensions():
|
|
|
|
|
"""Récupère la liste des extensions de fichiers à traiter."""
|
|
|
|
|
extensions = os.environ.get('NZBPO_FILEEXTENSIONS', '')
|
|
|
|
|
if not extensions:
|
|
|
|
|
return None
|
|
|
|
|
return [ext.strip().lower() for ext in extensions.split(',')]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def print_log(message, level='INFO'):
|
|
|
|
|
"""Affiche un message dans les logs NZBGet."""
|
|
|
|
|
print(f'[{level}] {message}')
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_encoding_issue(filename):
|
|
|
|
|
"""
|
|
|
|
|
Détecte si un nom de fichier contient des problèmes d'encodage typiques.
|
|
|
|
|
|
|
|
|
|
Recherche des motifs caractéristiques de l'UTF-8 mal interprété comme ISO-8859-1:
|
|
|
|
|
- Ã suivi de caractères accentués (à, â, é, è, ê, ô, etc.)
|
|
|
|
|
"""
|
|
|
|
|
# Motifs caractéristiques d'un problème d'encodage UTF-8 -> ISO-8859-1
|
|
|
|
|
patterns = [
|
|
|
|
|
'é', # é mal encodé
|
|
|
|
|
'è', # è mal encodé
|
|
|
|
|
'ê', # ê mal encodé
|
|
|
|
|
'ë', # ë mal encodé
|
2025-10-27 13:26:43 +00:00
|
|
|
'Ã ', # à mal encodé (avec espace normal)
|
|
|
|
|
'Ã\xa0', # à mal encodé (avec espace insécable)
|
2025-10-27 13:17:09 +00:00
|
|
|
'â', # â mal encodé
|
|
|
|
|
'ä', # ä mal encodé
|
|
|
|
|
'ç', # ç mal encodé
|
|
|
|
|
'ô', # ô mal encodé
|
|
|
|
|
'ö', # ö mal encodé
|
|
|
|
|
'ù', # ù mal encodé
|
|
|
|
|
'û', # û mal encodé
|
|
|
|
|
'ü', # ü mal encodé
|
|
|
|
|
'î', # î mal encodé
|
|
|
|
|
'ï', # ï mal encodé
|
|
|
|
|
'Å"', # œ mal encodé
|
|
|
|
|
'É', # É mal encodé
|
2025-10-27 13:26:43 +00:00
|
|
|
'À', # À mal encodé (avec espace normal)
|
|
|
|
|
'Â', #  mal encodé
|
|
|
|
|
'È', # È mal encodé
|
|
|
|
|
'Ê', # Ê mal encodé
|
|
|
|
|
'ÃŽ', # Î mal encodé
|
|
|
|
|
'Ã"', # Ô mal encodé
|
|
|
|
|
'Ù', # Ù mal encodé
|
|
|
|
|
'Û', # Û mal encodé
|
|
|
|
|
'Ç', # Ç mal encodé
|
2025-10-27 13:17:09 +00:00
|
|
|
]
|
|
|
|
|
|
2025-10-27 13:26:43 +00:00
|
|
|
if not any(pattern in filename for pattern in patterns):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Double vérification : essaye de convertir pour voir si ça produit un résultat valide
|
|
|
|
|
try:
|
|
|
|
|
fixed = filename.encode('iso-8859-1').decode('utf-8')
|
|
|
|
|
# Si la conversion réussit et produit quelque chose de différent, c'est un problème d'encodage
|
|
|
|
|
return fixed != filename
|
|
|
|
|
except (UnicodeDecodeError, UnicodeEncodeError):
|
|
|
|
|
# Si la conversion échoue, ce n'est peut-être pas le bon type de problème
|
|
|
|
|
# ou alors c'est un mélange d'encodages
|
|
|
|
|
return False
|
2025-10-27 13:17:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def fix_encoding(filename):
|
|
|
|
|
"""
|
|
|
|
|
Corrige le nom de fichier en supposant qu'il a été mal interprété.
|
|
|
|
|
|
|
|
|
|
Le problème typique est: UTF-8 -> interprété comme ISO-8859-1
|
|
|
|
|
Solution: encoder en ISO-8859-1 (pour revenir aux octets originaux UTF-8)
|
|
|
|
|
puis décoder en UTF-8
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Tente de corriger l'encodage
|
|
|
|
|
# On encode d'abord en ISO-8859-1 pour récupérer les octets originaux
|
|
|
|
|
# puis on décode en UTF-8
|
|
|
|
|
fixed = filename.encode('iso-8859-1').decode('utf-8')
|
|
|
|
|
return fixed
|
|
|
|
|
except (UnicodeDecodeError, UnicodeEncodeError):
|
|
|
|
|
# Si la conversion échoue, on retourne le nom original
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def process_file(filepath, dirname, filename, extensions_filter, dry_run):
|
|
|
|
|
"""
|
|
|
|
|
Traite un fichier et le renomme si nécessaire.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
bool: True si le fichier a été renommé, False sinon
|
|
|
|
|
"""
|
|
|
|
|
# Vérifie l'extension si un filtre est défini
|
|
|
|
|
if extensions_filter:
|
|
|
|
|
file_ext = os.path.splitext(filename)[1].lower()
|
|
|
|
|
if file_ext not in extensions_filter:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Vérifie si le nom contient des problèmes d'encodage
|
|
|
|
|
if not is_encoding_issue(filename):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Tente de corriger l'encodage
|
|
|
|
|
fixed_filename = fix_encoding(filename)
|
|
|
|
|
|
|
|
|
|
if not fixed_filename or fixed_filename == filename:
|
|
|
|
|
if is_debug():
|
|
|
|
|
print_log(f'Impossible de corriger: {filename}', 'DEBUG')
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Construit les chemins complets
|
|
|
|
|
old_path = os.path.join(dirname, filename)
|
|
|
|
|
new_path = os.path.join(dirname, fixed_filename)
|
|
|
|
|
|
|
|
|
|
# Vérifie que le nouveau nom n'existe pas déjà
|
|
|
|
|
if os.path.exists(new_path):
|
|
|
|
|
print_log(f'Le fichier de destination existe déjà: {fixed_filename}', 'WARNING')
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Affiche l'action
|
|
|
|
|
print_log(f'Renommage: {filename}')
|
|
|
|
|
print_log(f' -> {fixed_filename}')
|
|
|
|
|
|
|
|
|
|
# Renomme le fichier (sauf en mode simulation)
|
|
|
|
|
if not dry_run:
|
|
|
|
|
try:
|
|
|
|
|
os.rename(old_path, new_path)
|
|
|
|
|
return True
|
|
|
|
|
except OSError as e:
|
|
|
|
|
print_log(f'Erreur lors du renommage: {e}', 'ERROR')
|
|
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
print_log('[MODE SIMULATION - Fichier non renommé]', 'INFO')
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
"""Fonction principale du script."""
|
|
|
|
|
|
|
|
|
|
# Récupère le répertoire de travail depuis les variables d'environnement NZBGet
|
|
|
|
|
nzb_directory = os.environ.get('NZBPP_DIRECTORY')
|
|
|
|
|
|
|
|
|
|
if not nzb_directory:
|
|
|
|
|
print_log('Erreur: NZBPP_DIRECTORY non défini', 'ERROR')
|
|
|
|
|
sys.exit(POSTPROCESS_ERROR)
|
|
|
|
|
|
|
|
|
|
if not os.path.isdir(nzb_directory):
|
|
|
|
|
print_log(f'Erreur: Le répertoire n\'existe pas: {nzb_directory}', 'ERROR')
|
|
|
|
|
sys.exit(POSTPROCESS_ERROR)
|
|
|
|
|
|
|
|
|
|
print_log('=== Début du traitement de correction d\'encodage ===')
|
|
|
|
|
print_log(f'Répertoire: {nzb_directory}')
|
|
|
|
|
|
|
|
|
|
# Récupère les options
|
|
|
|
|
extensions_filter = get_file_extensions()
|
|
|
|
|
dry_run = is_dry_run()
|
|
|
|
|
|
|
|
|
|
if dry_run:
|
|
|
|
|
print_log('MODE SIMULATION ACTIVÉ - Aucun fichier ne sera modifié', 'WARNING')
|
|
|
|
|
|
|
|
|
|
if extensions_filter:
|
|
|
|
|
print_log(f'Filtrage par extensions: {", ".join(extensions_filter)}')
|
|
|
|
|
|
|
|
|
|
# Parcourt tous les fichiers récursivement
|
|
|
|
|
files_renamed = 0
|
|
|
|
|
files_processed = 0
|
|
|
|
|
|
|
|
|
|
for dirpath, dirnames, filenames in os.walk(nzb_directory):
|
|
|
|
|
for filename in filenames:
|
|
|
|
|
files_processed += 1
|
|
|
|
|
|
|
|
|
|
if process_file(os.path.join(dirpath, filename), dirpath, filename, extensions_filter, dry_run):
|
|
|
|
|
files_renamed += 1
|
|
|
|
|
|
|
|
|
|
# Affiche le résumé
|
|
|
|
|
print_log('=== Résumé ===')
|
|
|
|
|
print_log(f'Fichiers traités: {files_processed}')
|
|
|
|
|
print_log(f'Fichiers renommés: {files_renamed}')
|
|
|
|
|
|
|
|
|
|
if files_renamed > 0:
|
|
|
|
|
print_log('Correction d\'encodage terminée avec succès')
|
|
|
|
|
sys.exit(POSTPROCESS_SUCCESS)
|
|
|
|
|
else:
|
|
|
|
|
print_log('Aucun fichier à corriger')
|
|
|
|
|
sys.exit(POSTPROCESS_NONE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
main()
|