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',
+)