Ajout de HandBrake_recursive proxmox_export_disk tree_stream
This commit is contained in:
commit
534b6d9455
6
HandBrake_recursive/.gitignore
vendored
Normal file
6
HandBrake_recursive/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
build
|
||||||
|
résumé_P#.xlsx
|
||||||
|
résumé_torrent.xlsx
|
||||||
|
résumé.xlsx
|
||||||
|
HandBrake_recursive.egg-info/
|
||||||
|
.venv/
|
3
HandBrake_recursive/.idea/.gitignore
generated
vendored
Normal file
3
HandBrake_recursive/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
14
HandBrake_recursive/.idea/HandBrake_recursive.iml
generated
Normal file
14
HandBrake_recursive/.idea/HandBrake_recursive.iml
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?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 (HandBrake_recursive)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="PLAIN" />
|
||||||
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
|
</component>
|
||||||
|
</module>
|
6
HandBrake_recursive/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
HandBrake_recursive/.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
HandBrake_recursive/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
HandBrake_recursive/.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>
|
7
HandBrake_recursive/.idea/misc.xml
generated
Normal file
7
HandBrake_recursive/.idea/misc.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.11 (HandBrake_recursive)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (HandBrake_recursive)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
8
HandBrake_recursive/.idea/modules.xml
generated
Normal file
8
HandBrake_recursive/.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/HandBrake_recursive.iml" filepath="$PROJECT_DIR$/.idea/HandBrake_recursive.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
468
HandBrake_recursive/HandBrake_recursive.py
Normal file
468
HandBrake_recursive/HandBrake_recursive.py
Normal file
@ -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()
|
2
HandBrake_recursive/requirements.txt
Normal file
2
HandBrake_recursive/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ffmpeg-python
|
||||||
|
openpyxl
|
18
HandBrake_recursive/setup.py
Normal file
18
HandBrake_recursive/setup.py
Normal file
@ -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',
|
||||||
|
)
|
3
proxmox_export_disk/.gitignore
vendored
Normal file
3
proxmox_export_disk/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
build
|
||||||
|
proxmox_export_disk.egg-info/
|
||||||
|
.venv/
|
3
proxmox_export_disk/.idea/.gitignore
generated
vendored
Normal file
3
proxmox_export_disk/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
6
proxmox_export_disk/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
proxmox_export_disk/.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
proxmox_export_disk/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
proxmox_export_disk/.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>
|
7
proxmox_export_disk/.idea/misc.xml
generated
Normal file
7
proxmox_export_disk/.idea/misc.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.13 (proxmox_export_disk)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (proxmox_export_disk)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
8
proxmox_export_disk/.idea/modules.xml
generated
Normal file
8
proxmox_export_disk/.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/proxmox_export_disk.iml" filepath="$PROJECT_DIR$/.idea/proxmox_export_disk.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
10
proxmox_export_disk/.idea/proxmox_export_disk.iml
generated
Normal file
10
proxmox_export_disk/.idea/proxmox_export_disk.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.13 (proxmox_export_disk)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
398
proxmox_export_disk/proxmox_export_disk.py
Normal file
398
proxmox_export_disk/proxmox_export_disk.py
Normal file
@ -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()
|
17
proxmox_export_disk/setup.py
Normal file
17
proxmox_export_disk/setup.py
Normal file
@ -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',
|
||||||
|
)
|
19
proxmox_export_disk/temps-exec
Normal file
19
proxmox_export_disk/temps-exec
Normal file
@ -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
|
3
tree_stream/.gitignore
vendored
Normal file
3
tree_stream/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
build
|
||||||
|
tree_stream.egg-info/
|
||||||
|
.venv/
|
3
tree_stream/.idea/.gitignore
generated
vendored
Normal file
3
tree_stream/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
6
tree_stream/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
tree_stream/.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
tree_stream/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
tree_stream/.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>
|
7
tree_stream/.idea/misc.xml
generated
Normal file
7
tree_stream/.idea/misc.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.11 (tree_stream)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (tree_stream)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
8
tree_stream/.idea/modules.xml
generated
Normal file
8
tree_stream/.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/tree_stream.iml" filepath="$PROJECT_DIR$/.idea/tree_stream.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
10
tree_stream/.idea/tree_stream.iml
generated
Normal file
10
tree_stream/.idea/tree_stream.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 (tree_stream)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
3
tree_stream/pyproject.toml
Normal file
3
tree_stream/pyproject.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
1
tree_stream/requirements.txt
Normal file
1
tree_stream/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
ffmpeg-python
|
22
tree_stream/setup.py
Normal file
22
tree_stream/setup.py
Normal file
@ -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',
|
||||||
|
)
|
1
tree_stream/tree_stream/__init__.py
Normal file
1
tree_stream/tree_stream/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .main import main
|
162
tree_stream/tree_stream/main.py
Normal file
162
tree_stream/tree_stream/main.py
Normal file
@ -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()
|
Loading…
x
Reference in New Issue
Block a user