From 5d7ad7ebb0657f236cc88219c903e50a7f59bf41 Mon Sep 17 00:00:00 2001 From: lionel <.> Date: Mon, 9 Jun 2025 20:40:31 +0200 Subject: [PATCH] [multiplex] Ajout de multiplex --- README.md | 11 ++ multiplex/.idea/.gitignore | 3 + .../inspectionProfiles/Project_Default.xml | 6 + .../inspectionProfiles/profiles_settings.xml | 6 + multiplex/.idea/misc.xml | 6 + multiplex/.idea/modules.xml | 8 + multiplex/.idea/multiplex.iml | 10 + multiplex/.idea/vcs.xml | 6 + multiplex/multiplex.py | 181 ++++++++++++++++++ multiplex/setup.py | 17 ++ 10 files changed, 254 insertions(+) create mode 100644 multiplex/.idea/.gitignore create mode 100644 multiplex/.idea/inspectionProfiles/Project_Default.xml create mode 100644 multiplex/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 multiplex/.idea/misc.xml create mode 100644 multiplex/.idea/modules.xml create mode 100644 multiplex/.idea/multiplex.iml create mode 100644 multiplex/.idea/vcs.xml create mode 100644 multiplex/multiplex.py create mode 100644 multiplex/setup.py diff --git a/README.md b/README.md index b945333..ef62b9e 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,17 @@ cd ProjetsPython/tree_stream pipx install . ``` +### multiplex + +Un script Python pour assembler des pistes vidéo, audio et sous-titres issues de deux sources différentes en un seul fichier MKV, en utilisant la bibliothèque `pymkv2`. +Il gère la priorité des pistes françaises, la définition des pistes par défaut, la gestion des chapitres et le renommage intelligent des pistes. + +```bash +git clone https://git.netdldata.net/lionel/ProjetsPython.git +cd ProjetsPython/multiplex +pipx install . +``` + ### Installer tous les projets ```bash diff --git a/multiplex/.idea/.gitignore b/multiplex/.idea/.gitignore new file mode 100644 index 0000000..f52251b --- /dev/null +++ b/multiplex/.idea/.gitignore @@ -0,0 +1,3 @@ +# Fichiers ignorés par défaut +/shelf/ +/workspace.xml diff --git a/multiplex/.idea/inspectionProfiles/Project_Default.xml b/multiplex/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..cd83845 --- /dev/null +++ b/multiplex/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/multiplex/.idea/inspectionProfiles/profiles_settings.xml b/multiplex/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/multiplex/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/multiplex/.idea/misc.xml b/multiplex/.idea/misc.xml new file mode 100644 index 0000000..b70052d --- /dev/null +++ b/multiplex/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/multiplex/.idea/modules.xml b/multiplex/.idea/modules.xml new file mode 100644 index 0000000..21038a4 --- /dev/null +++ b/multiplex/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/multiplex/.idea/multiplex.iml b/multiplex/.idea/multiplex.iml new file mode 100644 index 0000000..0d0906a --- /dev/null +++ b/multiplex/.idea/multiplex.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/multiplex/.idea/vcs.xml b/multiplex/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/multiplex/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/multiplex/multiplex.py b/multiplex/multiplex.py new file mode 100644 index 0000000..bc4ec30 --- /dev/null +++ b/multiplex/multiplex.py @@ -0,0 +1,181 @@ +import os +import sys +import glob +from pymkv import MKVFile, MKVTrack + +from pathlib import Path + +#: Dictionnaire de correspondance entre les codes de langue et leur nom complet en français +LANG_MAP = { + 'fre': 'Français', + 'eng': 'Anglais', + 'ita': 'Italien', + 'spa': 'Espagnol', + 'por': 'Portugais', + 'rus': 'Russe', + 'jpn': 'Japonais', + 'chi': 'Chinois', + 'kor': 'Coréen', + 'nld': 'Néerlandais', +} + +def ligne_bleu(): + """Affiche une ligne bleue pleine largeur dans le terminal.""" + cols = os.get_terminal_size().columns + print("\033[0;34m" + "█" * cols + "\033[0m") + +def find_episode_file(source_dir, saison, episode): + """ + Recherche un fichier d'épisode dans un répertoire donné. + + :param source_dir: Répertoire source + :param saison: Numéro de la saison + :param episode: Numéro de l'épisode + :return: Chemin du fichier trouvé ou None + """ + pattern = f"*S0{saison}E{episode}*.mkv" + files = glob.glob(os.path.join(source_dir, pattern)) + return files[0] if files else None + +def find_first_video_track_index(mkv_file): + """ + Recherche l'index de la première piste vidéo dans un fichier MKV. + + :param mkv_file: Objet MKVFile + :return: Index de la piste vidéo ou None + """ + for idx, track in enumerate(mkv_file.tracks): + if track.track_type == 'video': + return idx + return None + +def has_named_chapters(chapters): + """ + Détermine si une liste de chapitres contient des noms réels (autres que des génériques). + """ + import re + pattern = re.compile(r'(?i)^\s*(chapter|chapitre)\s*\d+\s*$') + return any(not pattern.match(c.name or '') for c in chapters) + +def process_episode(episode, source_dir_1, source_dir_2, saison, serie_name, dest_dir): + """ + Traite un épisode en combinant les pistes vidéo et audio/sous-titres de deux sources. + + La vidéo provient de source_1_file. + Les pistes audio et sous-titres sont priorisées depuis source_2_file. + Les pistes audio/sous-titres en français sont réorganisées, annotées et une seule est définie par défaut. + + :param episode: Numéro de l'épisode (format chaîne "01", "02", etc.) + :param source_dir_1: Répertoire contenant la source vidéo + :param source_dir_2: Répertoire contenant la source audio/sous-titres + :param saison: Numéro de saison + :param serie_name: Nom de la série + :param dest_dir: Répertoire de sortie + """ + # Affiche une ligne bleue pour la lisibilité dans le terminal + ligne_bleu() + + # Recherche les fichiers source correspondants à l'épisode + source_1_file = find_episode_file(source_dir_1, saison, episode) + source_2_file = find_episode_file(source_dir_2, saison, episode) + + # Gère les cas où un fichier source est manquant + if not source_1_file and source_2_file: + print(f"Fichier source 1 manquant pour l'épisode {episode}") + return + if source_1_file and not source_2_file: + print(f"Fichier source 2 manquant pour l'épisode {episode}") + return + if not source_1_file and not source_2_file: + return + + # Crée le chemin complet pour le fichier de sortie + output_filename = f"{serie_name} S0{saison} E{episode}.mkv" + output_path = os.path.join(dest_dir, output_filename) + + # Crée un objet MKVFile pour le fichier final + mkv = MKVFile() + + # Charge les fichiers source + source_1_mkv = MKVFile(source_1_file) + source_2_mkv = MKVFile(source_2_file) + + # Ajout de la piste vidéo principale + video_track_index = find_first_video_track_index(source_1_mkv) + if video_track_index is not None: + mkv.add_track(MKVTrack(source_1_file, track_id=video_track_index)) + + # Sélection des pistes audio (FR prioritaires) + audio_tracks_fr = [t for t in source_2_mkv.tracks if t.track_type == 'audio' and t.language == 'fre'] + audio_tracks_other = [t for t in source_2_mkv.tracks if t.track_type == 'audio' and t.language != 'fre'] + + for idx, track in enumerate(audio_tracks_fr): + track.default_track = (idx == 0) + if not track.track_name: + track.track_name = LANG_MAP.get(track.language, track.language.title()) + + for track in audio_tracks_other: + track.default_track = False + if not track.track_name: + track.track_name = LANG_MAP.get(track.language, track.language.title()) + + # Sélection des pistes sous-titres (FR prioritaires) + subtitle_tracks_fr = [t for t in source_2_mkv.tracks if t.track_type == 'subtitles' and t.language == 'fre'] + subtitle_tracks_other = [t for t in source_2_mkv.tracks if t.track_type == 'subtitles' and t.language != 'fre'] + + for track in subtitle_tracks_fr + subtitle_tracks_other: + track.default_track = False + + # Renommage des pistes FR si exactement deux + if len(subtitle_tracks_fr) == 2: + for track in subtitle_tracks_fr: + codec = track.codec_id.upper() + if track.forced_track: + track.track_name = f"FR Forcé [{codec}]" + else: + track.track_name = f"FR Complet [{codec}]" + + # Ajout de toutes les pistes audio et sous-titres + for track in audio_tracks_fr + audio_tracks_other + subtitle_tracks_fr + subtitle_tracks_other: + mkv.add_track(track) + + # Traitement des chapitres : choix du fichier avec le plus de chapitres valides + chapters_1 = source_1_mkv.chapters or [] + chapters_2 = source_2_mkv.chapters or [] + if len(chapters_1) >= 4 and has_named_chapters(chapters_1) and len(chapters_1) >= len(chapters_2): + mkv.chapters = chapters_1 + elif len(chapters_2) >= 4 and has_named_chapters(chapters_2): + mkv.chapters = chapters_2 + else: + mkv.chapters = None + + # Supprime le titre et génère le fichier final + mkv.title = "" + mkv.mux(output_path) + print(f"✔ Fichier généré : {output_path}") + +def main(): + """ + Point d'entrée principal du script. + + Lit les arguments de ligne de commande, prépare les répertoires, + et lance le traitement de chaque épisode (de 01 à 30). + """ + if len(sys.argv) != 5: + print("Usage: script.py ") + sys.exit(1) + + source_dir_1 = sys.argv[1] + source_dir_2 = sys.argv[2] + serie_name = sys.argv[3] + saison = sys.argv[4] + dest_dir = f"/media/data/reencoded/{serie_name}/Saison {saison}" + + Path(dest_dir).mkdir(parents=True, exist_ok=True) + + for i in range(1, 31): + episode = f"{i:02}" + process_episode(episode, source_dir_1, source_dir_2, saison, serie_name, dest_dir) + +if __name__ == '__main__': + main() diff --git a/multiplex/setup.py b/multiplex/setup.py new file mode 100644 index 0000000..b37be1f --- /dev/null +++ b/multiplex/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup + +setup( + name='multiplex', + version='0.1.0', + description='Un script pour multiplexer les fichiers vidéo avec mkvmerge.', + py_modules=['multiplex'], + install_requires=[ + 'pymkv2', + ], + entry_points={ + 'console_scripts': [ + 'multiplex=multiplex:main', + ], + }, + python_requires='>=3.6', +)