Files
ProjetsPython/multiplex/multiplex.py
lionel 4d7170f570
All checks were successful
/ 🐍 Build & Publish Python Packages (push) Successful in 10s
[multiplex] Change la gestion des chapitres
2025-07-24 01:07:42 +02:00

243 lines
8.7 KiB
Python

import os
import sys
import glob
import re
import argparse
from pathlib import Path
from pymkv import MKVFile, MKVTrack
#: 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',
}
TRADUCTIONS = {
"Forced": "Forcé",
"Full": "Complet",
"French": "Français",
"English": "Anglais",
"Japan": "Japonais",
"subtitle": "Sous-titre",
}
def traduire(texte):
for ancien, nouveau in TRADUCTIONS.items():
texte = re.sub(re.escape(ancien), nouveau, texte, flags=re.IGNORECASE)
return texte
def ligne_bleu():
"""Affiche une ligne bleue pleine largeur dans le terminal."""
try:
cols = os.get_terminal_size().columns
except OSError:
cols = 100
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é (insensible à la casse).
: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 = re.compile(rf"s{int(saison):02d}[ex]{int(episode):02d}", re.IGNORECASE)
p = Path(source_dir)
# On récupère tous les fichiers mkv ou mp4 en une seule liste
files = list(p.glob("*.mkv")) + list(p.glob("*.mp4"))
for file in files:
if pattern.search(os.path.basename(file)):
return file
return 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 process_episode(episode, source_dir_1, source_dir_2, saison, serie_name, dest_dir, chapitre):
"""
Traite un épisode en combinant les pistes vidéo et audio/sous-titres de deux sources.
: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
:param chapitre: Indique quels chapitres inclure dans le fichier final
:return: None
"""
# Affiche une ligne bleue pour la lisibilité dans le terminal
ligne_bleu()
# 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)
# Vérifie si le fichier final existe déjà
if os.path.exists(output_path):
print(f'\033[91m{output_filename} existe déjà.\033[0m')
return
# 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 un objet MKVFile pour le fichier final
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:
final_mkv.add_track(source_1_mkv.tracks[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 idx, track in enumerate(subtitle_tracks_fr):
track.default_track = (idx == 0) and track.default_track
if not track.track_name:
codec = track._track_codec
if track.forced_track:
track.track_name = f"FR Forcé [{codec}]"
else:
track.track_name = f"FR Complet [{codec}]"
for track in subtitle_tracks_other:
track.default_track = False
# Ajout de toutes les pistes audio et sous-titres
for track in audio_tracks_fr + audio_tracks_other + subtitle_tracks_fr + subtitle_tracks_other:
# Traduction des noms des pistes
track.track_name = traduire(track.track_name)
final_mkv.add_track(track)
# Traitement des chapitres
if chapitre in ('v', 'video'):
source_2_mkv.no_chapters()
elif chapitre in ('a', 'audio'):
source_1_mkv.no_chapters()
else:
source_1_mkv.no_chapters()
source_2_mkv.no_chapters()
# Éffacer tous les tags
source_1_mkv.no_global_tags()
source_2_mkv.no_global_tags()
final_mkv.no_global_tags()
source_1_mkv.no_track_tags()
source_2_mkv.no_track_tags()
final_mkv.no_track_tags()
final_mkv.title = ""
#print(final_mkv.command(output_path))
final_mkv.mux(output_path)
print(f"✔ Fichier généré : {output_path}")
def main():
"""
@brief Point d'entrée du script.
@details Parse les arguments en ligne de commande et lance le traitement
des épisodes de la série.
@arg -v, --video Répertoire source vidéo
@arg -a, --audio Répertoire source audio/sous-titres
@arg -va Répertoire source commun pour vidéo et audio
@arg -n, --nom Nom de la série
@arg -s, --saison Numéro de la saison
@arg -d, --dest Répertoire de sortie
@return None
"""
parser = argparse.ArgumentParser(
description="Combine des épisodes MKV depuis deux sources (vidéo + audio/sous-titres)."
)
parser.add_argument("-v", "--video", help="Répertoire source vidéo" )
parser.add_argument("-a", "--audio", help="Répertoire source audio/sous-titres" )
parser.add_argument("-va", help="Répertoire source commun pour vidéo et audio" )
parser.add_argument("-n", "--nom", required=True, help="Nom de la série")
parser.add_argument("-s", "--saison", required=True, help="Numéro de la saison")
parser.add_argument("-d", "--dest", required=True, help="Répertoire de sortie")
parser.add_argument("-c", "--chapitre", choices=['n', 'non', 'v', 'video', 'a', 'audio'], default='a', help="Gestion des chapitres : 'n'/'non' pour aucun, 'v'/'video' pour source vidéo, 'a'/'audio' pour source audio (par défaut 'a')")
args = parser.parse_args()
# Gestion de l'option commune -va
if args.va:
source_dir_1 = source_dir_2 = args.va
else:
source_dir_1 = args.video
source_dir_2 = args.audio
# Vérification que les deux dossiers sont renseignés
if not source_dir_1 or not source_dir_2:
parser.error("Vous devez fournir soit -va, soit à la fois -v et -a.")
serie_name = args.nom
saison = args.saison
chapitre = args.chapitre
# Chemin de sortie
dest_dir = f"{args.dest}/{serie_name}/Saison {saison}"
Path(dest_dir).mkdir(parents=True, exist_ok=True)
# Compte le nombre de fichiers dans chaque répertoire source
count1 = sum(1 for _ in Path(source_dir_1).iterdir() if _.is_file())
count2 = sum(1 for _ in Path(source_dir_2).iterdir() if _.is_file())
for i in range(1, max(count1, count2) + 1):
episode = f"{i:02}"
process_episode(episode, source_dir_1, source_dir_2, saison, serie_name, dest_dir, chapitre)
if __name__ == '__main__':
main()