[multiplex] Ajout de multiplex
This commit is contained in:
parent
593df4b028
commit
5d7ad7ebb0
11
README.md
11
README.md
@ -60,6 +60,17 @@ cd ProjetsPython/tree_stream
|
|||||||
pipx install .
|
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
|
### Installer tous les projets
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
3
multiplex/.idea/.gitignore
generated
vendored
Normal file
3
multiplex/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Fichiers ignorés par défaut
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
6
multiplex/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
multiplex/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
6
multiplex/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
multiplex/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
6
multiplex/.idea/misc.xml
generated
Normal file
6
multiplex/.idea/misc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.11 (multiplex)" />
|
||||||
|
</component>
|
||||||
|
</project>
|
8
multiplex/.idea/modules.xml
generated
Normal file
8
multiplex/.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/multiplex.iml" filepath="$PROJECT_DIR$/.idea/multiplex.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
10
multiplex/.idea/multiplex.iml
generated
Normal file
10
multiplex/.idea/multiplex.iml
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.11 (multiplex)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
6
multiplex/.idea/vcs.xml
generated
Normal file
6
multiplex/.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
181
multiplex/multiplex.py
Normal file
181
multiplex/multiplex.py
Normal file
@ -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 <SOURCE_DIR_1> <SOURCE_DIR_2> <SERIE_NAME> <SAISON>")
|
||||||
|
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()
|
17
multiplex/setup.py
Normal file
17
multiplex/setup.py
Normal file
@ -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',
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user