From 534b6d9455b4180c196c0960cf0ac357b0381996 Mon Sep 17 00:00:00 2001
From: lionel <.>
Date: Mon, 12 May 2025 13:28:19 +0200
Subject: [PATCH] Ajout de HandBrake_recursive proxmox_export_disk tree_stream
---
HandBrake_recursive/.gitignore | 6 +
HandBrake_recursive/.idea/.gitignore | 3 +
.../.idea/HandBrake_recursive.iml | 14 +
.../inspectionProfiles/Project_Default.xml | 6 +
.../inspectionProfiles/profiles_settings.xml | 6 +
HandBrake_recursive/.idea/misc.xml | 7 +
HandBrake_recursive/.idea/modules.xml | 8 +
HandBrake_recursive/HandBrake_recursive.py | 468 ++++++++++++++++++
HandBrake_recursive/requirements.txt | 2 +
HandBrake_recursive/setup.py | 18 +
proxmox_export_disk/.gitignore | 3 +
proxmox_export_disk/.idea/.gitignore | 3 +
.../inspectionProfiles/Project_Default.xml | 6 +
.../inspectionProfiles/profiles_settings.xml | 6 +
proxmox_export_disk/.idea/misc.xml | 7 +
proxmox_export_disk/.idea/modules.xml | 8 +
.../.idea/proxmox_export_disk.iml | 10 +
proxmox_export_disk/proxmox_export_disk.py | 398 +++++++++++++++
proxmox_export_disk/setup.py | 17 +
proxmox_export_disk/temps-exec | 19 +
tree_stream/.gitignore | 3 +
tree_stream/.idea/.gitignore | 3 +
.../inspectionProfiles/Project_Default.xml | 6 +
.../inspectionProfiles/profiles_settings.xml | 6 +
tree_stream/.idea/misc.xml | 7 +
tree_stream/.idea/modules.xml | 8 +
tree_stream/.idea/tree_stream.iml | 10 +
tree_stream/pyproject.toml | 3 +
tree_stream/requirements.txt | 1 +
tree_stream/setup.py | 22 +
tree_stream/tree_stream/__init__.py | 1 +
tree_stream/tree_stream/main.py | 162 ++++++
32 files changed, 1247 insertions(+)
create mode 100644 HandBrake_recursive/.gitignore
create mode 100644 HandBrake_recursive/.idea/.gitignore
create mode 100644 HandBrake_recursive/.idea/HandBrake_recursive.iml
create mode 100644 HandBrake_recursive/.idea/inspectionProfiles/Project_Default.xml
create mode 100644 HandBrake_recursive/.idea/inspectionProfiles/profiles_settings.xml
create mode 100644 HandBrake_recursive/.idea/misc.xml
create mode 100644 HandBrake_recursive/.idea/modules.xml
create mode 100644 HandBrake_recursive/HandBrake_recursive.py
create mode 100644 HandBrake_recursive/requirements.txt
create mode 100644 HandBrake_recursive/setup.py
create mode 100644 proxmox_export_disk/.gitignore
create mode 100644 proxmox_export_disk/.idea/.gitignore
create mode 100644 proxmox_export_disk/.idea/inspectionProfiles/Project_Default.xml
create mode 100644 proxmox_export_disk/.idea/inspectionProfiles/profiles_settings.xml
create mode 100644 proxmox_export_disk/.idea/misc.xml
create mode 100644 proxmox_export_disk/.idea/modules.xml
create mode 100644 proxmox_export_disk/.idea/proxmox_export_disk.iml
create mode 100644 proxmox_export_disk/proxmox_export_disk.py
create mode 100644 proxmox_export_disk/setup.py
create mode 100644 proxmox_export_disk/temps-exec
create mode 100644 tree_stream/.gitignore
create mode 100644 tree_stream/.idea/.gitignore
create mode 100644 tree_stream/.idea/inspectionProfiles/Project_Default.xml
create mode 100644 tree_stream/.idea/inspectionProfiles/profiles_settings.xml
create mode 100644 tree_stream/.idea/misc.xml
create mode 100644 tree_stream/.idea/modules.xml
create mode 100644 tree_stream/.idea/tree_stream.iml
create mode 100644 tree_stream/pyproject.toml
create mode 100644 tree_stream/requirements.txt
create mode 100644 tree_stream/setup.py
create mode 100644 tree_stream/tree_stream/__init__.py
create mode 100644 tree_stream/tree_stream/main.py
diff --git a/HandBrake_recursive/.gitignore b/HandBrake_recursive/.gitignore
new file mode 100644
index 0000000..d7c6ecf
--- /dev/null
+++ b/HandBrake_recursive/.gitignore
@@ -0,0 +1,6 @@
+build
+résumé_P#.xlsx
+résumé_torrent.xlsx
+résumé.xlsx
+HandBrake_recursive.egg-info/
+.venv/
diff --git a/HandBrake_recursive/.idea/.gitignore b/HandBrake_recursive/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/HandBrake_recursive/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/HandBrake_recursive/.idea/HandBrake_recursive.iml b/HandBrake_recursive/.idea/HandBrake_recursive.iml
new file mode 100644
index 0000000..d1268a1
--- /dev/null
+++ b/HandBrake_recursive/.idea/HandBrake_recursive.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HandBrake_recursive/.idea/inspectionProfiles/Project_Default.xml b/HandBrake_recursive/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..cd83845
--- /dev/null
+++ b/HandBrake_recursive/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HandBrake_recursive/.idea/inspectionProfiles/profiles_settings.xml b/HandBrake_recursive/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/HandBrake_recursive/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HandBrake_recursive/.idea/misc.xml b/HandBrake_recursive/.idea/misc.xml
new file mode 100644
index 0000000..799d018
--- /dev/null
+++ b/HandBrake_recursive/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HandBrake_recursive/.idea/modules.xml b/HandBrake_recursive/.idea/modules.xml
new file mode 100644
index 0000000..af2df4e
--- /dev/null
+++ b/HandBrake_recursive/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HandBrake_recursive/HandBrake_recursive.py b/HandBrake_recursive/HandBrake_recursive.py
new file mode 100644
index 0000000..8330341
--- /dev/null
+++ b/HandBrake_recursive/HandBrake_recursive.py
@@ -0,0 +1,468 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+import mimetypes
+import subprocess
+import shutil
+import math
+import json
+import argparse
+import ffmpeg
+
+import openpyxl
+from openpyxl.styles import PatternFill
+from openpyxl.formatting.rule import CellIsRule
+
+# Définir la largeur de chaque colonne à environ 3 cm (approximativement 10.5 unités)
+def set_column_width(ws, column_range, width=15):
+ for col in column_range:
+ ws.column_dimensions[col].width = width
+
+def export_xlsx(lignes, fichier_sortie="résumé.xlsx"):
+ # Créer un nouveau classeur et sélectionner la feuille active
+ wb = openpyxl.Workbook()
+ ws = wb.active
+ ws.title = "Résumé"
+
+ # En-têtes
+ headers = ["Fichier", "Codec", "Résolution", "Action", "Durée", "Taille SRC", "Bitrate SRC",
+ "Taille DES", "Bitrate DES", "Gain %", "Résultat"]
+ ws.append(headers)
+ ws.auto_filter.ref = "A1:K1"
+ ws.freeze_panes = "B2"
+
+ # Appliquer à toutes les colonnes
+ set_column_width(ws, ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'])
+
+ # Styles pour les cellules
+ styles = {
+ "Réussi": PatternFill(start_color="d4f5dd", end_color="d4f5dd", fill_type="solid"),
+ "Erreur": PatternFill(start_color="f8d7da", end_color="f8d7da", fill_type="solid"),
+ "Interrompu": PatternFill(start_color="fdd692", end_color="fdd692", fill_type="solid"),
+ "Existant": PatternFill(start_color="dbeff7", end_color="dbeff7", fill_type="solid"),
+ "Simulé": PatternFill(start_color="d1e7ff", end_color="d1e7ff", fill_type="solid"),
+ "Réencodé": PatternFill(start_color="e9d5e5", end_color="e9d5e5", fill_type="solid"),
+ "Copié": PatternFill(start_color="fff3cd", end_color="fff3cd", fill_type="solid"),
+ "Gain > 50%": PatternFill(start_color="d4f5dd", end_color="d4f5dd", fill_type="solid"),
+ "Gain > 30%": PatternFill(start_color="fff3cd", end_color="fff3cd", fill_type="solid"),
+ "Gain < 30%": PatternFill(start_color="f8d7da", end_color="f8d7da", fill_type="solid"),
+ "Taille <": PatternFill(start_color="d4f5dd", end_color="d4f5dd", fill_type="solid"),
+ "Taille =": PatternFill(start_color="fff3cd", end_color="fff3cd", fill_type="solid"),
+ "Taille >": PatternFill(start_color="f8d7da", end_color="f8d7da", fill_type="solid"),
+ }
+
+ # Données
+ for ligne in lignes:
+ file, codec, resolution, action, duration, input_size, output_size, input_bitrate, output_bitrate, result = ligne
+
+ gain_percent = calc_gain_percent(input_size, output_size, raw=True)
+
+ row = [
+ file, # string
+ codec, # string
+ resolution, # string
+ action, # string
+ duration / 86400 if isinstance(duration, (float, int)) else duration, # float fraction de jour
+ input_size / 1024 ** 2, # int (octets)
+ input_bitrate, # int
+ output_size / 1024 ** 2, # int
+ output_bitrate, # int
+ gain_percent, # float (vrai pourcentage)
+ result # string
+ ]
+ ws.append(row)
+ nb_lignes = ws.max_row
+
+ for row in ws.iter_rows(min_row=2, max_col=len(headers)): # skip header
+ row[4].number_format = '[h]"h"mm"m"ss"s"' # Excel-style format
+ row[5].number_format = '#,##0 "Mo"' # Taille SRC
+ row[6].number_format = '#,##0 "kb/s"' # Bitrate SRC
+ row[7].number_format = '#,##0 "Mo"' # Taille DES
+ row[8].number_format = '#,##0 "kb/s"' # Bitrate DES
+ row[9].number_format = '0.0%' # Gain %
+
+ # Appliquer couleurs sur colonne Action (colonne D = index 3)
+ for row in ws.iter_rows(min_row=2, min_col=4, max_col=4, max_row=ws.max_row):
+ cell = row[0]
+ action = cell.value
+ if action in styles:
+ for col_index in [0, 1, 2, 3, 4]: # Colonnes Fichier, Codec, Résolution, Action, Durée
+ ws.cell(row=cell.row, column=col_index + 1).fill = styles[action]
+
+ # Appliquer couleurs sur colonne Résultat (colonne K = index 10)
+ for row in ws.iter_rows(min_row=2, min_col=11, max_col=11, max_row=ws.max_row):
+ cell = row[0]
+ resultat = cell.value
+ if resultat in styles:
+ cell.fill = styles[resultat]
+
+ # Ajouter la mise en forme conditionnelle
+ # Taille DES
+ ws.conditional_formatting.add(f'H2:H{nb_lignes}', CellIsRule(operator='lessThan', formula=[f'F2:F{nb_lignes}'], stopIfTrue=True, fill=styles["Taille <"]))
+ ws.conditional_formatting.add(f'H2:H{nb_lignes}', CellIsRule(operator='equal', formula=[f'F2:F{nb_lignes}'], stopIfTrue=True, fill=styles["Taille ="]))
+ ws.conditional_formatting.add(f'H2:H{nb_lignes}', CellIsRule(operator='greaterThan', formula=[f'F2:F{nb_lignes}'], stopIfTrue=True, fill=styles["Taille >"]))
+
+ # Gain %
+ ws.conditional_formatting.add(f'J2:J{nb_lignes}', CellIsRule(operator='greaterThan', formula=['50%'], stopIfTrue=True, fill=styles["Gain > 50%"]))
+ ws.conditional_formatting.add(f'J2:J{nb_lignes}', CellIsRule(operator='greaterThan', formula=['30%'], stopIfTrue=True, fill=styles["Gain > 30%"]))
+ ws.conditional_formatting.add(f'J2:J{nb_lignes}', CellIsRule(operator='lessThan', formula=['30%'], stopIfTrue=True, fill=styles["Gain < 30%"]))
+
+ # Sauvegarder le fichier
+ wb.save(fichier_sortie)
+
+def human_readable_size(size_bytes):
+ """Convertit une taille en octets en une taille lisible (Ko, Mo, Go)."""
+ if size_bytes == 0:
+ return "0 B"
+ size_name = ("B", "KB", "MB", "GB", "TB")
+ i = int(math.floor(math.log(size_bytes, 1024)))
+ p = math.pow(1024, i)
+ s = round(size_bytes / p, 2)
+ return f"{s} {size_name[i]}"
+
+def format_duration(seconds):
+ """Formate la durée en h/m/s lisible"""
+ if not isinstance(seconds, (float, int)) or seconds <= 0:
+ return ""
+ else:
+ return f"{int(seconds // 3600)}h" * (seconds >= 3600) + f"{int((seconds % 3600) // 60)}m" * (seconds >= 60) + f"{int(seconds % 60)}s"
+
+def calc_ratio(size, duration):
+ """Calcule le bitrate approximatif (octets/sec)."""
+ return str(int(size / duration)) if duration else ""
+
+def calc_gain_percent(input_size, output_size, raw=False):
+ if input_size > 0 and output_size > 0:
+ value = (input_size - output_size) / input_size
+ return value if raw else value * 100
+ return 0
+
+def check_file_exists(file_path):
+ """Vérifie si un fichier existe."""
+ if not os.path.exists(file_path):
+ print(f"\033[91mErreur : Le fichier {file_path} n'existe pas.\033[0m")
+ sys.exit(1)
+
+def check_directory_existence(directory_path, create_if_missing=False):
+ """Vérifie si un dossier existe et le crée si nécessaire."""
+ if not os.path.exists(directory_path):
+ if create_if_missing:
+ try:
+ os.makedirs(directory_path)
+ print(f"Dossier créé : {directory_path}")
+ except OSError as e:
+ print(f"\033[91mErreur : Impossible de créer le dossier {directory_path}. {e}\033[0m")
+ sys.exit(1)
+ else:
+ print(f"\033[91mErreur : Le dossier {directory_path} n'existe pas.\033[0m")
+ sys.exit(1)
+
+def check_directory_writable(directory_path):
+ """Vérifie si un dossier est accessible en écriture."""
+ if not os.access(directory_path, os.W_OK):
+ print(f"\033[91mErreur : Le dossier {directory_path} n'est pas accessible en écriture.\033[0m")
+ sys.exit(1)
+
+def get_video_duration(file_path):
+ """Retourne la durée d'une vidéo en secondes."""
+ try:
+ probe = ffmpeg.probe(file_path)
+ return float(probe['format']['duration'])
+ except Exception as e:
+ print(f"\033[91mErreur lors de la récupération de la durée pour {file_path}: {e}\033[0m")
+ return 0.0
+
+def get_video_codec(file_path):
+ """Retourne le codec vidéo d'un fichier avec ffmpeg."""
+ try:
+ probe = ffmpeg.probe(file_path)
+ video_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None)
+ if video_stream:
+ return video_stream['codec_name']
+ return "inconnu"
+ except ffmpeg.Error as e:
+ print(f"\033[91mErreur lors de l'analyse du codec vidéo pour {file_path}: {e}\033[0m")
+ return "inconnu"
+
+def get_video_resolution(file_path):
+ """Retourne la résolution de la vidéo au format 720p, 1080p, etc."""
+ try:
+ probe = ffmpeg.probe(file_path)
+ height = next((stream['height'] for stream in probe['streams'] if stream['codec_type'] == 'video'), None)
+ return f"{height}p" if height else ""
+ except Exception as e:
+ print(f"\033[91mErreur lors de l'analyse de la résolution vidéo pour {file_path}: {e}\033[0m")
+ return ""
+
+def get_video_bitrate(file_path):
+ """Récupère le bitrate vidéo d'un fichier via ffmpeg."""
+ try:
+ probe = ffmpeg.probe(file_path)
+ for stream in probe['streams']:
+ if stream['codec_type'] == 'video':
+ # 1. Priorité : le champ 'bit_rate' s'il est présent
+ if 'bit_rate' in stream:
+ return int(stream['bit_rate']) // 1000
+
+ # 2. Sinon : on tente d'utiliser 'stream_size' s'il existe
+ elif 'stream_size' in stream:
+ return int((int(stream.get('stream_size')) * 8) / float(probe['format']['duration']) / 1000)
+
+ # 3. Fallback : calcul à partir de la taille du fichier entier (moins précis)
+ else:
+ return int((os.path.getsize(file_path) * 8) / float(probe['format']['duration']) / 1000)
+ except Exception as e:
+ print(f"\033[91mErreur lors de l'analyse du bitrate vidéo pour {file_path}: {e}\033[0m")
+ return 0
+
+def execute_command(command, dry_run=False):
+ """Exécute ou simule une commande en ligne."""
+ if dry_run:
+ print(f"Commande simulée : {' '.join(command)}")
+ result = "Simulé"
+ else:
+ print(f"Commande exécuté : {' '.join(command)}")
+ subprocess.run(command, check=True)
+ result = "Réussi"
+ return result
+
+def copyfile(file, destination, dry_run=False):
+ """Copie ou simule une copie de fichier."""
+ if dry_run:
+ result = "Simulé"
+ else:
+ shutil.copyfile(file, destination)
+ result = "Réussi"
+ return result
+
+def find_video_files(directory):
+ """Trouve tous les fichiers vidéo dans un dossier."""
+ video_files = []
+ for root, dirs, files in os.walk(directory):
+ for file in files:
+ file_path = os.path.join(root, file)
+ mime_type, _ = mimetypes.guess_type(file_path)
+ if mime_type and mime_type.startswith('video'):
+ video_files.append(os.path.relpath(file_path, directory))
+ return video_files
+
+def get_preset_name_from_file(preset_file):
+ """Extrait le nom du preset à partir du fichier JSON."""
+ try:
+ with open(preset_file, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ return data["PresetList"][0]["PresetName"]
+ except Exception as e:
+ print(f"\033[91mErreur : Impossible de lire le preset name depuis {preset_file} : {e}\033[0m")
+ sys.exit(1)
+
+def afficher_tableau(lignes, use_pager=False):
+ buffer = []
+
+ # En-tête avec colonnes ajustées
+ header = (
+ f"{'Fichier':<100} | "
+ f"{'Codec':<10} | "
+ f"{'Résolution':<10} | "
+ f"{'Action':<10} | "
+ f"{'Durée':<10} | "
+ f"{'Taille SRC':<10} | "
+ f"{'Bitrate SRC':<12} | "
+ f"{'Taille DES':<10} | "
+ f"{'Bitrate DES':<12} | "
+ f"{'Gain %':<6} | "
+ f"{'Résultat':<10} |"
+ )
+
+ # Calculer la longueur de la ligne de séparation en fonction de la longueur de l'en-tête
+ separator_length = len(header)
+
+ # En-tête et ligne de séparation
+ buffer.append("\nRésumé des opérations :")
+ buffer.append(header)
+ buffer.append("-" * separator_length)
+
+ for ligne in lignes:
+ file, codec, resolution, action, duration, input_size, output_size, input_bitrate, output_bitrate, result = ligne
+
+ gain_percent = calc_gain_percent(input_size, output_size)
+
+ action_color = ""
+ result_color = ""
+ reset_color = "\033[0m"
+
+ # Couleurs d'action
+ if action == "Réencodé":
+ action_color = "\033[1;35m" # Magenta gras
+ elif action == "Copié":
+ action_color = "\033[1;33m" # Jaune gras
+
+ # Couleurs de résultat
+ if result == "Réussi":
+ result_color = "\033[1;32m" # Vert gras
+ elif result == "Erreur":
+ result_color = "\033[1;31m" # Rouge gras
+ elif result == "Interrompu":
+ result_color = "\033[1;38;5;208m" # Orange gras
+ elif result == "Existant":
+ result_color = "\033[1;36m" # Cyan gras
+ elif result == "Simulé":
+ result_color = "\033[1;34m" # Bleu vif gras
+
+ # Couleur de la taille destination selon gain ou perte
+ if output_size < input_size:
+ output_size_color = "\033[1;32m" # Vert gras (gain)
+ elif output_size == input_size:
+ output_size_color = "\033[1;38;5;208m" # Orange gras (égal)
+ else:
+ output_size_color = "\033[1;31m" # Rouge gras (perte)
+
+ # Définir une couleur selon le gain
+ # > 50% => vert, > 30% => orange, sinon rouge
+ if gain_percent > 50:
+ gain_color = "\033[1;32m" # Vert gras
+ elif gain_percent > 30:
+ gain_color = "\033[1;38;5;208m" # Orange gras
+ else:
+ gain_color = "\033[1;31m" # Rouge gras
+
+ buffer.append(
+ f"{action_color}{file[:100]:<100}{reset_color} | " # Fichier
+ f"{action_color}{codec[:10]:<10}{reset_color} | " # Codec
+ f"{action_color}{resolution[:10]:>10}{reset_color} | " # Résolution
+ f"{action_color}{action[:10]:<10}{reset_color} | " # Action
+ f"{action_color}{format_duration(duration)[:10]:>10}{reset_color} | " # Durée
+ f"{human_readable_size(input_size)[:10]:>10} | " # Taille SRC
+ f"{(f'{input_bitrate} kb/s' if isinstance(input_bitrate, (float, int)) else '')[:12]:>12} | " # Bitrate SRC
+ f"{output_size_color}{human_readable_size(output_size)[:10]:>10}{reset_color} | " # Taille DES
+ f"{(f'{output_bitrate} kb/s' if isinstance(output_bitrate, (float, int)) else '')[:12]:>12} | " # Bitrate DES
+ f"{gain_color}{gain_percent:>5.1f}%{reset_color} | " # Gain %
+ f"{result_color}{result[:10]:<10}{reset_color} |" # Résultat
+ )
+
+ tableau = "\n".join(buffer)
+ if use_pager:
+ subprocess.run(["less", "-SR"], input=tableau.encode('utf-8'))
+ else:
+ print(tableau)
+
+def main():
+ # Configuration des arguments en ligne de commande
+ parser = argparse.ArgumentParser(description='Traitement des fichiers vidéo avec HandBrakeCLI.')
+ parser.add_argument('-p', '--preset', required=True, help='Fichier preset JSON à utiliser avec HandBrakeCLI')
+ parser.add_argument('-i', '--input_directory', required=True, help='Dossier d’entrée contenant les fichiers vidéo')
+ parser.add_argument('-o', '--output_directory', required=True, help='Dossier de sortie pour les fichiers traités')
+ parser.add_argument('--dry-run', action='store_true', help='Simule les actions sans modifier les fichiers')
+ parser.add_argument('--pager', action='store_true', help='Utilise less pour paginer le résumé final')
+ args = parser.parse_args()
+
+ # Récupération des arguments
+ preset_file = args.preset
+ input_directory = args.input_directory
+ output_directory = args.output_directory
+ dry_run = args.dry_run
+ use_pager = args.pager
+
+ # Vérification de l'existence des fichiers et dossiers
+ check_file_exists(preset_file)
+ check_directory_existence(input_directory)
+ if not dry_run:
+ check_directory_existence(output_directory, create_if_missing=True)
+ check_directory_writable(output_directory)
+
+ # Récupération du nom du preset
+ preset_name = get_preset_name_from_file(preset_file)
+
+ # Recherche des fichiers vidéo dans le dossier d'entrée
+ video_files = find_video_files(input_directory)
+
+ # Liste pour enregistrer le résumé
+ lignes = []
+
+ try:
+ # Traitement de chaque fichier vidéo
+ for video_file in video_files:
+ input_file_path = os.path.join(input_directory, video_file)
+ output_file_path = os.path.join(output_directory, video_file)
+ if not dry_run: check_directory_existence(os.path.dirname(output_file_path), create_if_missing=True)
+
+ # Récupération du codec du fichier vidéo
+ codec = get_video_codec(input_file_path)
+
+ # Choix de l'action
+ if codec == "hevc":
+ action = "Copié"
+ output_file_path = os.path.join(output_directory, video_file)
+ else:
+ action = "Réencodé"
+ output_file_path = os.path.splitext(os.path.join(output_directory, video_file))[0] + '.mp4'
+
+ try:
+ # Si le fichier existe déjà
+ if os.path.exists(output_file_path):
+ result = "Existant"
+ print(f"\033[96mFichier existant : {input_file_path} vers {output_file_path} (action aurait été : {action})\033[0m")
+ elif action == "Réencodé":
+ print(f"\033[95mRéencodage de {input_file_path} vers {output_file_path} (Non-HEVC détecté)\033[0m")
+ command = ['HandBrakeCLI', '--preset-import-file', preset_file, '-Z', preset_name, '-i',
+ input_file_path, '-o', output_file_path]
+ result = execute_command(command, dry_run)
+ elif action == "Copié":
+ print(f"\033[93mCopie de {input_file_path} vers {output_file_path} (HEVC détecté)\033[0m")
+ result = copyfile(input_file_path, output_file_path, dry_run)
+ except KeyboardInterrupt:
+ print("\033[91mInterruption clavier détectée.\033[0m")
+ if os.path.exists(output_file_path):
+ os.remove(output_file_path)
+ result = "Interrompu"
+ raise
+ except Exception as e:
+ print(f"\033[91mErreur lors du traitement de {input_file_path} : {e}\033[0m")
+ if os.path.exists(output_file_path):
+ os.remove(output_file_path)
+ result = "Erreur"
+ finally:
+ # Enregistrer le résultat même en cas d'erreur ou interruption
+
+ # Récupère des informations du fichier de départ
+ resolution = get_video_resolution(input_file_path)
+ input_size = os.path.getsize(input_file_path)
+ input_bitrate = get_video_bitrate(input_file_path)
+
+ # Récupère des informations du fichier final
+ output_size = os.path.getsize(output_file_path) if os.path.exists(output_file_path) else 0
+ output_bitrate = get_video_bitrate(output_file_path) if os.path.exists(output_file_path) else ""
+
+ duration = get_video_duration(input_file_path)
+ lignes.append((video_file, codec, resolution, action, duration, input_size, output_size, input_bitrate, output_bitrate, result))
+
+ except KeyboardInterrupt:
+ pass
+ finally:
+ # Calcul des tailles des dossiers source et destination (après traitement)
+ input_total = sum(r[5] for r in lignes)
+ output_total = sum(r[6] for r in lignes)
+
+ # Insertion d'une ligne récapitulative des tailles des dossiers
+ lignes.insert(0, (
+ "[Taille Totale]", # Fichier
+ "", # Codec
+ "", # Résolution
+ "", # Action
+ "", # Durée
+ input_total, # Taille SRC
+ output_total, # Taille DES
+ "", # Bitrate SRC
+ "", # Bitrate DES
+ "" # Résultat
+ ))
+
+ # Affichage du résumé
+ afficher_tableau(lignes, use_pager=use_pager)
+ xlsx_file = "résumé.xlsx"
+ export_xlsx(lignes, fichier_sortie=xlsx_file)
+ print(f"📝 Classeur généré : {xlsx_file}")
+
+if __name__ == "__main__":
+ main()
diff --git a/HandBrake_recursive/requirements.txt b/HandBrake_recursive/requirements.txt
new file mode 100644
index 0000000..125da89
--- /dev/null
+++ b/HandBrake_recursive/requirements.txt
@@ -0,0 +1,2 @@
+ffmpeg-python
+openpyxl
\ No newline at end of file
diff --git a/HandBrake_recursive/setup.py b/HandBrake_recursive/setup.py
new file mode 100644
index 0000000..5fab5e4
--- /dev/null
+++ b/HandBrake_recursive/setup.py
@@ -0,0 +1,18 @@
+from setuptools import setup
+
+setup(
+ name='HandBrake_recursive',
+ version='0.1.0',
+ description='Un script pour traiter les fichiers vidéo avec HandBrake de manière récursive.',
+ py_modules=['HandBrake_recursive'],
+ install_requires=[
+ 'ffmpeg-python',
+ 'openpyxl',
+ ],
+ entry_points={
+ 'console_scripts': [
+ 'handbrake_recursive=HandBrake_recursive:main',
+ ],
+ },
+ python_requires='>=3.6',
+)
diff --git a/proxmox_export_disk/.gitignore b/proxmox_export_disk/.gitignore
new file mode 100644
index 0000000..6093b56
--- /dev/null
+++ b/proxmox_export_disk/.gitignore
@@ -0,0 +1,3 @@
+build
+proxmox_export_disk.egg-info/
+.venv/
diff --git a/proxmox_export_disk/.idea/.gitignore b/proxmox_export_disk/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/proxmox_export_disk/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/proxmox_export_disk/.idea/inspectionProfiles/Project_Default.xml b/proxmox_export_disk/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..cd83845
--- /dev/null
+++ b/proxmox_export_disk/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/proxmox_export_disk/.idea/inspectionProfiles/profiles_settings.xml b/proxmox_export_disk/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/proxmox_export_disk/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/proxmox_export_disk/.idea/misc.xml b/proxmox_export_disk/.idea/misc.xml
new file mode 100644
index 0000000..4656c86
--- /dev/null
+++ b/proxmox_export_disk/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/proxmox_export_disk/.idea/modules.xml b/proxmox_export_disk/.idea/modules.xml
new file mode 100644
index 0000000..3585d08
--- /dev/null
+++ b/proxmox_export_disk/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/proxmox_export_disk/.idea/proxmox_export_disk.iml b/proxmox_export_disk/.idea/proxmox_export_disk.iml
new file mode 100644
index 0000000..b8597a7
--- /dev/null
+++ b/proxmox_export_disk/.idea/proxmox_export_disk.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/proxmox_export_disk/proxmox_export_disk.py b/proxmox_export_disk/proxmox_export_disk.py
new file mode 100644
index 0000000..28d6fd6
--- /dev/null
+++ b/proxmox_export_disk/proxmox_export_disk.py
@@ -0,0 +1,398 @@
+#!/usr/bin/env python3
+
+import subprocess
+import json
+import re
+import openpyxl
+import time
+import tempfile
+import os
+import shutil
+
+# Configuration
+hote_proxmox = "proxmox.lan"
+utilisateur_ssh = "impt"
+classeur = "proxmox_export_disk"
+
+def executer_commande_ssh(commande, entree=None):
+ """Exécute une commande sur le serveur Proxmox via SSH en utilisant sudo."""
+ commande_ssh = [
+ "ssh",
+ f"{utilisateur_ssh}@{hote_proxmox}",
+ f"sudo {commande}"
+ ]
+ resultat = subprocess.run(commande_ssh, input=entree, capture_output=True, text=True)
+
+ # Affiche stderr s’il y a quelque chose dedans
+ if resultat.stderr:
+ print("---- SSH STDERR ----")
+ print(resultat.stderr)
+
+ if resultat.returncode != 0:
+ return None
+ return resultat.stdout
+
+def recuperer_noeuds():
+ """Récupère la liste des nœuds du cluster Proxmox."""
+ donnees_noeuds = executer_commande_ssh("pvesh get /nodes --output-format json")
+ return json.loads(donnees_noeuds) if donnees_noeuds else []
+
+def recuperer_vms(noeuds):
+ """Récupère toutes les VM et conteneurs sur un nœud donné."""
+ vms = []
+ for noeud in noeuds:
+ nom_noeud = noeud.get("node")
+ for type_vm in ["qemu", "lxc"]:
+ cmd = f"pvesh get /nodes/{nom_noeud}/{type_vm} --output-format json"
+ donnees_vms = executer_commande_ssh(cmd)
+ liste_vm = json.loads(donnees_vms)
+ for vm in liste_vm:
+ vm['vmid'] = int(vm.get('vmid'))
+ vms.extend(liste_vm)
+ return vms
+
+def recuperer_configs_vms(vms):
+ """Récupère les configurations de toutes les VMs/CTs en une seule commande SSH (retour JSON)."""
+
+ # Contenu du script Python à exécuter côté serveur
+ script = """
+import json, sys
+res={}
+for i in json.load(sys.stdin):
+ v = i.get('vmid')
+ t = i.get('type')
+ c = f'/etc/pve/{t}/{v}.conf'
+ res[v]=open(c).read()
+print(json.dumps(res))
+"""
+
+ # Préparer les données avec les VMID et les types
+ donnees = [{'vmid': vm.get('vmid'), 'type': vm.get('type', 'qemu-server')} for vm in vms]
+
+ # On prépare la commande SSH qui exécute le script Python
+ commande = f'python3 -c "{script}"'
+
+ # On exécute en passant 'data' comme entrée standard du script
+ sortie = executer_commande_ssh(commande, entree=json.dumps(donnees, separators=(",", ":")))
+
+ # Parser le JSON retourné
+ try:
+ configs_vms = json.loads(sortie)
+ return configs_vms
+ except json.JSONDecodeError as e:
+ print("Erreur lors du décodage du JSON de configurations des VMs :", e)
+ return {}
+
+def recuperer_infos_stockage():
+ """Récupère les informations de configuration du stockage."""
+ cmd = "cat /etc/pve/storage.cfg"
+ sortie = executer_commande_ssh(cmd)
+ if sortie:
+ infos_stockage = {}
+ stockage_actuel = None
+ for ligne in sortie.splitlines():
+ if not ligne:
+ stockage_actuel = None
+ elif ligne.startswith('\t'):
+ parties = ligne[1:].split(' ', 1)
+ cle = parties[0].strip()
+ valeur = parties[1].strip() if len(parties) > 1 else True
+ if stockage_actuel:
+ infos_stockage[stockage_actuel][cle] = valeur
+ else:
+ type_stockage, stockage_actuel = ligne.split(': ', 1)
+ infos_stockage[stockage_actuel] = {'type': type_stockage}
+ return infos_stockage
+
+def recuperer_info_disques(vms, infos_stockage, configs):
+ """Récupère tous les disques à partir des configurations et des tailles déjà récupérées."""
+
+ info_disques = []
+ for vm in vms:
+ vmid = vm.get("vmid")
+ type_vm = "lxc" if vm.get("type") == "lxc" else "qemu"
+ config = configs.get(str(vmid))
+ if not config:
+ continue
+
+ for ligne in config.splitlines():
+ if "media=cdrom" in ligne or "iso" in ligne:
+ continue
+ elif not ligne: # Arrêter à la première ligne vide
+ break
+ elif re.match(r"^(scsi\d|sata\d|ide\d|mp\d|rootfs)", ligne):
+ try:
+ infos_ligne = ligne.split(":", 1)[1].split(",")
+ nom_stockage, nom_disque = [x.strip() for x in infos_ligne[0].split(":")]
+ point_montage = None
+ if type_vm == "lxc":
+ if re.match(r"^(mp\d)", ligne):
+ point_montage = ligne.split("mp=")[1].split(",")[0]
+ elif ligne.startswith("rootfs"):
+ point_montage = "/"
+ if nom_stockage in infos_stockage:
+ type_stockage = infos_stockage[nom_stockage]['type']
+ if type_stockage == "lvmthin":
+ # LVM
+ chemin = f"/dev/{infos_stockage[nom_stockage]['vgname']}/{nom_disque}"
+ elif type_stockage == "dir":
+ # Fichier raw ou qcow2
+ chemin = f"{infos_stockage[nom_stockage]['path']}/images/{nom_disque}"
+ nom_disque = nom_disque.replace(str(vmid) + "/", "")
+ else:
+ continue
+ info_disques.append({'vm': vm, 'chemin': chemin, 'nom_disque': nom_disque, 'nom_stockage': nom_stockage, 'type_stockage': type_stockage, 'point_montage': point_montage})
+ except (ValueError, AttributeError):
+ print(f"Erreur de parsing de la ligne de configuration : {ligne}")
+ return info_disques
+
+def recuperer_taille_disques(info_disques):
+ tailles_disques = {}
+ chemin_disques_lvms = []
+ chemin_disques_fichiers = []
+ for info_disque in info_disques:
+ # Récupère la taille d'un disque.
+ if info_disque.get('type_stockage') == "lvmthin":
+ # LVM
+ chemin_disques_lvms.append(info_disque.get('chemin'))
+ elif info_disque.get('type_stockage') == "dir":
+ # Fichier raw ou qcow2
+ chemin_disques_fichiers.append(info_disque.get('chemin'))
+ tailles_disques.update(recuperer_taille_lvm(chemin_disques_lvms) or {})
+ tailles_disques.update(recuperer_taille_fichier(chemin_disques_fichiers) or {})
+ return tailles_disques
+
+def recuperer_taille_lvm(chemin_disques):
+ """Analyse les tailles d'un volume logique LVM."""
+ tailles_disques = {}
+
+ cmd = f'lvs -o lv_path,lv_size,data_percent --noheadings --units g --separator "|" --nosuffix {" ".join(chemin_disques)}'
+ sortie = executer_commande_ssh(cmd)
+ lignes = sortie.splitlines()
+
+ # Analyse LVM
+ for ligne in lignes:
+ chemin, taille, pourcentage = ligne.strip().replace(',', '.').split('|')
+ tailles_disques[chemin] = {
+ "taille_totale": float(taille),
+ "pourcentage_utilise": float(pourcentage),
+ "taille_utilisee": float(taille) * float(pourcentage) / 100
+ }
+ return tailles_disques
+
+def recuperer_taille_fichier(chemin_disques):
+ """Analyse les tailles d'une image disque (qcow2/raw)."""
+ tailles_disques = {}
+ for chemin_disque in chemin_disques:
+ cmd = f"qemu-img info {chemin_disque}"
+ sortie = executer_commande_ssh(cmd)
+ if sortie:
+ lignes = sortie.splitlines()
+ taille_virtuelle = None
+ taille_disque = None
+ for ligne in lignes:
+ if 'virtual size:' in ligne:
+ taille_virtuelle = ligne.split(':')[1].strip().split('(')[0].strip()
+ elif 'disk size:' in ligne:
+ taille_disque = ligne.split(':')[1].strip()
+ if taille_virtuelle and taille_disque:
+ valeur_virt, unite_virt = taille_virtuelle.split(' ')
+ valeur_phys, unite_phys = taille_disque.split(' ')
+ taille_virt = float(valeur_virt) * {'GiB': 1, 'MiB': 1/1024, 'KiB': 1/1024/1024}[unite_virt]
+ taille_phys = float(valeur_phys) * {'GiB': 1, 'MiB': 1/1024, 'KiB': 1/1024/1024}[unite_phys]
+ pourcentage_utilise = (taille_phys / taille_virt) * 100
+ tailles_disques[chemin_disque] = {
+ "taille_totale": taille_virt,
+ "pourcentage_utilise": pourcentage_utilise,
+ "taille_utilisee": taille_phys
+ }
+ return tailles_disques
+
+def croisement_info(info_disques, taille_disques):
+ disques = []
+ for info_disque in info_disques:
+ vm = info_disque.get('vm')
+ taille_disque = taille_disques.get(info_disque.get('chemin'))
+ if taille_disque is None:
+ continue
+ disques.append({
+ "VMID": vm.get('vmid'),
+ "Nom": vm.get("name"),
+ "Type": vm.get("type", "qemu"),
+ "Disque": info_disque.get('nom_disque'),
+ "Stockage": info_disque.get('nom_stockage'),
+ "Taille Totale (GB)": taille_disque.get('taille_totale'),
+ "% Utilisé": taille_disque.get('pourcentage_utilise') / 100,
+ "Taille Utilisée (GB)": taille_disque.get('taille_utilisee'),
+ "Point de Montage": info_disque.get('point_montage'),
+ })
+ return disques
+
+def exporter_classeur(disques, fichier_sortie):
+ """Exporte les données des disques dans un fichier, en choisissant automatiquement entre XLSX et ODS."""
+ # Vérifie si LibreOffice est disponible sur le système
+ libreoffice_disponible = subprocess.run(
+ ["which", "soffice"], capture_output=True, text=True
+ ).returncode == 0
+
+ # Si LibreOffice est disponible, exporte en ODS, sinon en XLSX
+ if libreoffice_disponible:
+ fichier_sortie_avec_extension = ajouter_extension(fichier_sortie, ".ods")
+ print("LibreOffice est disponible, exportation en ODS...")
+ exporter_vers_ods(disques, fichier_sortie_avec_extension)
+ else:
+ fichier_sortie_avec_extension = ajouter_extension(fichier_sortie, ".xlsx")
+ print("LibreOffice n'est pas disponible, exportation en XLSX...")
+ exporter_vers_xlsx(disques, fichier_sortie_avec_extension)
+ return fichier_sortie_avec_extension
+
+def exporter_vers_xlsx(disques, fichier_sortie):
+ """Exporte les données des disques dans un fichier XLSX avec un filtre automatique."""
+ champs = ["VMID", "Nom", "Type", "Disque", "Stockage", "Taille Totale (GB)", "% Utilisé", "Taille Utilisée (GB)", "Point de Montage"]
+
+ wb = openpyxl.Workbook()
+ ws = wb.active
+ ws.title = "Disques"
+
+ ws.append(champs)
+ ws.freeze_panes = "B2"
+
+ # Tri croissant des disques par VMID
+ disques = sorted(disques, key=lambda x: x.get("VMID")) # Si VMID est absent, on lui attribue la valeur 0 par défaut
+
+ for disque in disques:
+ ligne = [disque.get(champ, "") for champ in champs]
+ ws.append(ligne)
+
+ derniere_ligne = ws.max_row
+
+ ws[f'A{derniere_ligne + 2}'] = f'=SUBTOTAL(103, A2:A{derniere_ligne})'
+ ws[f'H{derniere_ligne + 2}'] = f'=SUBTOTAL(109, H2:H{derniere_ligne})'
+
+ for ligne in ws.iter_rows(min_row=2, max_row=ws.max_row, max_col=len(champs)):
+ ligne[5].number_format = '#,##0.00 "GB"'
+ ligne[6].number_format = '0.00%'
+ ligne[7].number_format = '#,##0.00 "GB"'
+
+ ws.auto_filter.ref = f"A1:{ws.cell(row=derniere_ligne, column=len(champs)).coordinate}"
+
+ for colonne in ws.columns:
+ cellules = [cell for cell in colonne if cell.row <= derniere_ligne]
+ longueur = max(len(str(cell.value)) if cell.value is not None else 0 for cell in cellules)
+ largeur = max(longueur + 2, 8)
+ ws.column_dimensions[colonne[0].column_letter].width = largeur
+
+ wb.save(fichier_sortie)
+
+def exporter_vers_ods(disques, fichier_sortie):
+ """Exporte directement en ODS sans conserver le XLSX sur disque."""
+
+ # Crée un dossier temporaire pour le XLSX
+ with tempfile.TemporaryDirectory() as dossier_temp:
+ xlsx_temp = os.path.join(dossier_temp, "temp.xlsx")
+ ods_temp = os.path.join(dossier_temp, "temp.ods")
+
+ # Crée le XLSX
+ exporter_vers_xlsx(disques, xlsx_temp)
+
+ # Utilise LibreOffice pour convertir le fichier temporaire
+ commande = [
+ "soffice",
+ "--headless",
+ "--convert-to", "ods",
+ "--outdir", dossier_temp,
+ xlsx_temp
+ ]
+ subprocess.run(commande, capture_output=True, check=True)
+
+ # Renomme le fichier généré par LibreOffice au bon nom de sortie
+ chemin_ods_converti = os.path.join(dossier_temp, ods_temp)
+ shutil.copyfile(chemin_ods_converti, fichier_sortie)
+
+def ajouter_extension(chemin_fichier, extension):
+ """Ajoute l’extension souhaitée si elle est absente ou incorrecte."""
+ racine, ext = os.path.splitext(chemin_fichier)
+ ext = ext.lower()
+ extension = extension.lower()
+
+ # S’assure que l’extension commence par un point
+ if not extension.startswith('.'):
+ extension = '.' + extension
+
+ # Si l’extension est déjà correcte → ne rien changer
+ if ext == extension:
+ return chemin_fichier
+
+ # Sinon, remplace ou ajoute la bonne extension
+ return racine + extension
+
+def main():
+ """ Récupère les informations sur les disques des VM et conteneurs de tous les nœuds Proxmox, puis exporte les données dans un fichier XLSX. """
+
+ print("Récupération des nœuds...")
+ debut = time.time()
+ noeuds = recuperer_noeuds()
+ fin = time.time()
+ print(f"Temps écoulé pour la récupération des nœuds : {fin - debut:.2f} secondes")
+ if not noeuds:
+ print("Aucun nœud trouvé.")
+ return
+
+ print("Récupération des VMs...")
+ debut = time.time()
+ vms = recuperer_vms(noeuds)
+ fin = time.time()
+ print(f"Temps écoulé pour la récupération des VMs : {fin - debut:.2f} secondes")
+ if not vms:
+ print("Aucune VM trouvée.")
+ return
+
+ print("Récupération des stockages...")
+ debut = time.time()
+ infos_stockage = recuperer_infos_stockage()
+ fin = time.time()
+ print(f"Temps écoulé pour la récupération des stockages : {fin - debut:.2f} secondes")
+ if not infos_stockage:
+ print("Aucun stockage trouvé.")
+ return
+
+ print("Récupération des configurations des VMs...")
+ debut = time.time()
+ configs = recuperer_configs_vms(vms)
+ fin = time.time()
+ print(f"Temps écoulé pour la récupération des configurations : {fin - debut:.2f} secondes")
+
+ print("Récupération de emplacement des disques...")
+ debut = time.time()
+ chemin_disques = recuperer_info_disques(vms, infos_stockage, configs)
+ fin = time.time()
+ print(f"Temps écoulé pour la récupération de emplacement des disques : {fin - debut:.2f} secondes")
+ if not chemin_disques:
+ print("Aucune information à exporter.")
+ return
+
+ print("Récupération de la taille des disques...")
+ debut = time.time()
+ taille_disques = recuperer_taille_disques(chemin_disques)
+ fin = time.time()
+ print(f"Temps écoulé pour la récupération de la taille des disques : {fin - debut:.2f} secondes")
+ if not taille_disques:
+ print("Aucune information à exporter.")
+ return
+
+ print("Croisement des informations...")
+ debut = time.time()
+ disques = croisement_info(chemin_disques, taille_disques)
+ fin = time.time()
+ print(f"Temps écoulé pour la récupération des disques : {fin - debut:.2f} secondes")
+ if not disques:
+ print("Aucune information à exporter.")
+ return
+
+ print("Exportation des données en Classeur...")
+ nom_du_classeur = exporter_classeur(disques, classeur)
+ print(f"Données exportées dans {nom_du_classeur}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/proxmox_export_disk/setup.py b/proxmox_export_disk/setup.py
new file mode 100644
index 0000000..c47abc8
--- /dev/null
+++ b/proxmox_export_disk/setup.py
@@ -0,0 +1,17 @@
+from setuptools import setup
+
+setup(
+ name='proxmox_export_disk',
+ version='0.1.0',
+ description='Un script pour récupérer l\'utilisation des disques des VMs dans Proxmox.',
+ py_modules=['proxmox_export_disk'],
+ install_requires=[
+ 'openpyxl',
+ ],
+ entry_points={
+ 'console_scripts': [
+ 'proxmox_export_disk=proxmox_export_disk:main',
+ ],
+ },
+ python_requires='>=3.6',
+)
diff --git a/proxmox_export_disk/temps-exec b/proxmox_export_disk/temps-exec
new file mode 100644
index 0000000..afa1f6c
--- /dev/null
+++ b/proxmox_export_disk/temps-exec
@@ -0,0 +1,19 @@
+Récupération des nœuds...
+Temps écoulé pour la récupération des nœuds : 1.04 secondes
+Récupération des VMs...
+Temps écoulé pour la récupération des VMs : 1.83 secondes
+Récupération des stockages...
+Temps écoulé pour la récupération des stockages : 0.16 secondes
+Récupération des disques...
+Erreur de parsing de la ligne de configuration : mp0: /media/videos,mp=/media/videos,ro=1
+Erreur de parsing de la ligne de configuration : mp0: /media/data,mp=/media/data,ro=1
+Erreur de parsing de la ligne de configuration : mp1: /media/videos,mp=/media/videos,ro=1
+Erreur de parsing de la ligne de configuration : mp0: /media/data,mp=/media/data,ro=1
+Erreur de parsing de la ligne de configuration : mp1: /media/videos,mp=/media/videos,ro=1
+Erreur de parsing de la ligne de configuration : mp3: /var/lib/vz/dump/,mp=/media/dump,ro=1
+Erreur de parsing de la ligne de configuration : mp0: /media/videos,mp=/media/videos,ro=1
+Erreur de parsing de la ligne de configuration : mp1: /media/data/documents/media/music,mp=/media/music,ro=1
+Temps écoulé pour la récupération des disques : 63.49 secondes
+Exportation des données en XLSX...
+Temps écoulé pour l'exportation en XLSX : 0.01 secondes
+Données exportées dans proxmox_export_disk.xlsx
\ No newline at end of file
diff --git a/tree_stream/.gitignore b/tree_stream/.gitignore
new file mode 100644
index 0000000..08da8a6
--- /dev/null
+++ b/tree_stream/.gitignore
@@ -0,0 +1,3 @@
+build
+tree_stream.egg-info/
+.venv/
diff --git a/tree_stream/.idea/.gitignore b/tree_stream/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/tree_stream/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/tree_stream/.idea/inspectionProfiles/Project_Default.xml b/tree_stream/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..cd83845
--- /dev/null
+++ b/tree_stream/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tree_stream/.idea/inspectionProfiles/profiles_settings.xml b/tree_stream/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/tree_stream/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tree_stream/.idea/misc.xml b/tree_stream/.idea/misc.xml
new file mode 100644
index 0000000..579ca86
--- /dev/null
+++ b/tree_stream/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tree_stream/.idea/modules.xml b/tree_stream/.idea/modules.xml
new file mode 100644
index 0000000..4875be3
--- /dev/null
+++ b/tree_stream/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tree_stream/.idea/tree_stream.iml b/tree_stream/.idea/tree_stream.iml
new file mode 100644
index 0000000..19efa1b
--- /dev/null
+++ b/tree_stream/.idea/tree_stream.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tree_stream/pyproject.toml b/tree_stream/pyproject.toml
new file mode 100644
index 0000000..9787c3b
--- /dev/null
+++ b/tree_stream/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/tree_stream/requirements.txt b/tree_stream/requirements.txt
new file mode 100644
index 0000000..9abb303
--- /dev/null
+++ b/tree_stream/requirements.txt
@@ -0,0 +1 @@
+ffmpeg-python
\ No newline at end of file
diff --git a/tree_stream/setup.py b/tree_stream/setup.py
new file mode 100644
index 0000000..77353b0
--- /dev/null
+++ b/tree_stream/setup.py
@@ -0,0 +1,22 @@
+from setuptools import setup, find_packages
+
+setup(
+ name="tree_stream",
+ version="0.1.0",
+ description="Affiche une arborescence enrichie des fichiers vidéo avec chapitres et flux",
+ author="TonNom",
+ packages=find_packages(),
+ install_requires=["ffmpeg-python"],
+ entry_points={
+ "console_scripts": [
+ "tree_stream=tree_stream.main:main",
+ ]
+ },
+ classifiers=[
+ "Programming Language :: Python :: 3",
+ "Environment :: Console",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ ],
+ python_requires='>=3.6',
+)
diff --git a/tree_stream/tree_stream/__init__.py b/tree_stream/tree_stream/__init__.py
new file mode 100644
index 0000000..c28a133
--- /dev/null
+++ b/tree_stream/tree_stream/__init__.py
@@ -0,0 +1 @@
+from .main import main
diff --git a/tree_stream/tree_stream/main.py b/tree_stream/tree_stream/main.py
new file mode 100644
index 0000000..8f97e03
--- /dev/null
+++ b/tree_stream/tree_stream/main.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+import argparse
+import ffmpeg
+
+# Codes des couleurs ANSI
+NOIR = "\033[30m"
+ROUGE = "\033[31m"
+VERT = "\033[32m"
+ORANGE = "\033[33m" # On considère le marron comme orange
+BLEU = "\033[34m"
+VIOLET = "\033[35m"
+CYAN = "\033[36m"
+GRIS_CLAIR = "\033[37m"
+
+GRIS_FONCE = "\033[90m"
+ROUGE_CLAIR = "\033[91m"
+VERT_CLAIR = "\033[92m"
+JAUNE = "\033[93m"
+BLEU_CLAIR = "\033[94m"
+VIOLET_CLAIR = "\033[95m"
+CYAN_CLAIR = "\033[96m"
+BLANC = "\033[97m"
+
+# Styles de texte
+NORMAL = "\033[0m" # Normal (pas de style)
+GRAS = "\033[1m" # Gras
+SOULIGNE = "\033[4m" # Souligné
+CLIGNOTANT = "\033[5m" # Clignotant
+INVERSE = "\033[7m" # Inversé (texte clair sur fond sombre)
+
+
+def couleur(nom_fichier, est_dossier):
+ if est_dossier:
+ return BLEU + nom_fichier + NORMAL
+ elif nom_fichier.lower().endswith(('.mp4', '.mkv', '.avi')):
+ return VIOLET + nom_fichier + NORMAL # 💜 fichier vidéo
+ else:
+ return nom_fichier
+
+def analyser_fichier_video(chemin):
+ try:
+ probe = ffmpeg.probe(chemin, show_chapters=None) # Contrairement à ce que l'on pourrait croire, on demande les chapitres, c'est l'option show_chapters qui ne prend pas de paramètre.
+ info = []
+ max_index_len = len(str(len(probe.get("streams", [])) - 1))
+ streams = probe.get('streams', [])
+
+ # Filtrer pour ne garder que les flux pertinents (video, audio, subtitle)
+ flux_valides = [stream for stream in streams if stream.get('codec_type') in ['video', 'audio', 'subtitle']]
+
+ # Si on n'a pas de flux valide à afficher, on s'arrête
+ if not flux_valides:
+ return []
+
+ for i, stream in enumerate(flux_valides):
+ codec_type = stream.get('codec_type', 'und')
+ codec_name = stream.get('codec_name', 'und')
+ langue = stream.get('tags', {}).get('language', 'und')
+ titre = stream.get('tags', {}).get('title', '')
+ disposition = stream.get('disposition', {})
+ defaut = f"{ROUGE}défaut{NORMAL}" if disposition.get('default') == 1 else ""
+ force = f"{ROUGE}forcé{NORMAL}" if disposition.get('forced') == 1 else ""
+
+ # Déterminer les informations à afficher
+ ligne = (
+ f"{ORANGE}Stream{NORMAL} "
+ f"{ROUGE}{i}{NORMAL}"
+ f"{BLANC}:{NORMAL} "
+ f"{BLEU}{codec_type:<8}{NORMAL} "
+ f"{BLANC}({codec_name}, {langue}){NORMAL}"
+ )
+
+ # Ajouter les flags défaut/forcé s'ils existent
+ flags = []
+ if defaut:
+ flags.append(defaut)
+ if force:
+ flags.append(force)
+ if flags:
+ ligne += f" {' '.join(flags)}"
+
+ if titre:
+ ligne += f" {VERT}\"{titre}\"{NORMAL}"
+
+ info.append(ligne)
+
+ nb_chapitres = len(probe.get('chapters', []))
+ info.append(
+ f"{ORANGE}Chapitres{NORMAL} "
+ f"{BLANC}:{NORMAL} "
+ f"{VERT}{nb_chapitres}{NORMAL}")
+
+ return info
+
+ except ffmpeg.Error as e:
+ return [f"{ROUGE}Erreur ffmpeg : {e}{NORMAL}"]
+ except Exception as e:
+ return [f"{ROUGE}Erreur : {e}{NORMAL}"]
+
+def afficher_arborescence(dossier, prefixe="", tout_afficher=False, niveau_max=None, niveau_actuel=0):
+ if niveau_max is not None and niveau_actuel > niveau_max:
+ return
+
+ try:
+ elements = sorted(
+ os.listdir(dossier),
+ key=lambda x: (not os.path.isdir(os.path.join(dossier, x)), x)
+ )
+ except PermissionError:
+ print(prefixe + "└── [Permission refusée]")
+ return
+
+ if not tout_afficher:
+ elements = [e for e in elements if not e.startswith('.')]
+ for index, nom in enumerate(elements):
+ chemin_complet = os.path.join(dossier, nom)
+ est_dernier = (index == len(elements) - 1)
+ branche = "└── " if est_dernier else "├── "
+ sous_prefixe = " " if est_dernier else "│ "
+ est_dossier = os.path.isdir(chemin_complet)
+ print(prefixe + branche + couleur(nom, est_dossier))
+
+ if est_dossier:
+ afficher_arborescence(
+ chemin_complet,
+ prefixe + sous_prefixe,
+ tout_afficher=tout_afficher,
+ niveau_max=niveau_max,
+ niveau_actuel=niveau_actuel + 1
+ )
+ elif nom.lower().endswith(('.mp4', '.mkv', '.avi')):
+ infos = analyser_fichier_video(chemin_complet)
+ for i, ligne in enumerate(infos):
+ print(prefixe + sous_prefixe + ("└── " if i == len(infos) - 1 else "├── ") + ligne)
+
+def main():
+ parser = argparse.ArgumentParser(description="Affiche l’arborescence avec informations sur les fichiers vidéo.")
+ parser.add_argument("dossiers", nargs="*", default=["."], help="Un ou plusieurs dossiers à analyser")
+ parser.add_argument("-a", "--tout", action="store_true", help="Afficher les fichiers masqués")
+ parser.add_argument("-L", "--niveau", type=int, help="Niveau d’affichage maximale")
+
+ args = parser.parse_args()
+
+ try:
+ for dossier in args.dossiers:
+ if not os.path.exists(dossier):
+ print(f"{ROUGE}Erreur : le dossier '{dossier}' n'existe pas.{NORMAL}")
+ continue
+
+ print(couleur(dossier, est_dossier=True))
+ afficher_arborescence(
+ dossier,
+ tout_afficher=args.tout,
+ niveau_max=args.niveau
+ )
+ except KeyboardInterrupt:
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()