Files
Youtube_Downloader/app.py
2026-05-30 14:11:33 +01:00

428 lines
13 KiB
Python

import queue
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from downloader import (
DOWNLOADS_DIR,
build_command,
load_settings,
validate_tools,
CommandRunner,
)
APP_VERSION = "1.0.0"
ABOUT_TEXT = f"""
YouTube Downloader
Version {APP_VERSION}
Développé par IM TECH
Cet outil permet de télécharger, découper et convertir des contenus audio ou vidéo
à partir d'une URL, selon les options choisies par l'utilisateur.
Fonctions principales :
- Télécharger une vidéo complète en MP4
- Télécharger un extrait vidéo en MP4
- Télécharger un audio complet en MP3
- Télécharger un extrait audio en MP3
- Choisir la qualité vidéo et audio
- Choisir le dossier de sortie
IMPORTANT - UTILISATION RESPONSABLE
Ce logiciel est fourni uniquement comme outil technique local.
L'utilisateur est seul responsable :
- des liens qu'il utilise ;
- des contenus qu'il télécharge ;
- des fichiers qu'il convertit ;
- de l'utilisation, publication ou redistribution des fichiers obtenus.
Ce logiciel doit être utilisé uniquement pour :
- vos propres vidéos ;
- des contenus libres de droits ;
- des contenus sous licence autorisant le téléchargement ou la réutilisation ;
- des contenus pour lesquels vous avez une autorisation explicite ;
- des usages permis par la loi applicable dans votre pays.
Ce logiciel n'est pas destiné à :
- violer les droits d'auteur ;
- contourner des protections techniques ;
- redistribuer illégalement des vidéos, musiques ou extraits ;
- télécharger des contenus sans autorisation lorsque cela est interdit.
Ce logiciel n'est pas affilié, associé, approuvé ou sponsorisé par YouTube,
Google, yt-dlp ou FFmpeg.
Les marques, logos et noms cités appartiennent à leurs propriétaires respectifs.
En utilisant ce logiciel, vous acceptez d'en faire un usage légal,
personnel et responsable.
"""
MODE_LABELS = {
"Vidéo complète MP4": "video_full",
"Extrait vidéo MP4": "video_extract",
"Audio complet MP3": "audio_full",
"Extrait audio MP3": "audio_extract",
}
class YouTubeDownloaderApp:
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("YouTube Downloader")
self.root.geometry("900x650")
self.root.minsize(820, 600)
self.settings = load_settings()
self.runner = CommandRunner()
self.log_queue = queue.Queue()
self.url_var = tk.StringVar()
self.mode_var = tk.StringVar(value="Extrait audio MP3")
self.start_var = tk.StringVar(value="00:00:00")
self.end_var = tk.StringVar(value="00:00:29")
self.filename_var = tk.StringVar(value="extrait_YT_Downloader")
self.output_var = tk.StringVar(value=str(DOWNLOADS_DIR))
self.video_quality_var = tk.StringVar(
value=self.settings.get("default_video_quality", "720")
)
self.audio_quality_var = tk.StringVar(
value=self.settings.get("default_audio_quality", "192K")
)
self.build_menu()
self.build_ui()
self.check_tools_on_start()
self.update_extract_fields()
self.poll_log_queue()
def build_menu(self):
menu_bar = tk.Menu(self.root)
help_menu = tk.Menu(menu_bar, tearoff=0)
help_menu.add_command(label="À propos / Disclaimer", command=self.show_about)
menu_bar.add_cascade(label="Aide", menu=help_menu)
self.root.config(menu=menu_bar)
def build_ui(self):
main = ttk.Frame(self.root, padding=20)
main.pack(fill="both", expand=True)
header = ttk.Frame(main)
header.pack(fill="x", pady=(0, 20))
title_block = ttk.Frame(header)
title_block.pack(side="left", fill="x", expand=True)
title = ttk.Label(
title_block,
text="YouTube Downloader",
font=("Segoe UI", 20, "bold"),
)
title.pack(anchor="w")
subtitle = ttk.Label(
title_block,
text="Télécharger une vidéo complète, un extrait, un MP4 ou un MP3.",
font=("Segoe UI", 10),
)
subtitle.pack(anchor="w")
about_button = ttk.Button(
header,
text="À propos",
command=self.show_about,
)
about_button.pack(side="right", padx=(15, 0))
form = ttk.Frame(main)
form.pack(fill="x")
ttk.Label(form, text="URL YouTube").grid(row=0, column=0, sticky="w")
url_entry = ttk.Entry(form, textvariable=self.url_var)
url_entry.grid(row=1, column=0, columnspan=4, sticky="ew", pady=(5, 15))
ttk.Label(form, text="Mode").grid(row=2, column=0, sticky="w")
mode_combo = ttk.Combobox(
form,
textvariable=self.mode_var,
values=list(MODE_LABELS.keys()),
state="readonly",
)
mode_combo.grid(row=3, column=0, sticky="ew", pady=(5, 15))
mode_combo.bind(
"<<ComboboxSelected>>",
lambda event: self.update_extract_fields(),
)
ttk.Label(form, text="Début").grid(row=2, column=1, sticky="w", padx=(15, 0))
self.start_entry = ttk.Entry(form, textvariable=self.start_var)
self.start_entry.grid(row=3, column=1, sticky="ew", padx=(15, 0), pady=(5, 15))
ttk.Label(form, text="Fin").grid(row=2, column=2, sticky="w", padx=(15, 0))
self.end_entry = ttk.Entry(form, textvariable=self.end_var)
self.end_entry.grid(row=3, column=2, sticky="ew", padx=(15, 0), pady=(5, 15))
ttk.Label(form, text="Nom du fichier").grid(row=4, column=0, sticky="w")
filename_entry = ttk.Entry(form, textvariable=self.filename_var)
filename_entry.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(5, 15))
ttk.Label(form, text="Qualité vidéo").grid(row=4, column=2, sticky="w", padx=(15, 0))
video_quality_combo = ttk.Combobox(
form,
textvariable=self.video_quality_var,
values=["480", "720", "1080", "best"],
state="readonly",
)
video_quality_combo.grid(row=5, column=2, sticky="ew", padx=(15, 0), pady=(5, 15))
ttk.Label(form, text="Qualité audio").grid(row=4, column=3, sticky="w", padx=(15, 0))
audio_quality_combo = ttk.Combobox(
form,
textvariable=self.audio_quality_var,
values=["128K", "192K", "256K", "320K"],
state="readonly",
)
audio_quality_combo.grid(row=5, column=3, sticky="ew", padx=(15, 0), pady=(5, 15))
ttk.Label(form, text="Dossier de sortie").grid(row=6, column=0, sticky="w")
output_entry = ttk.Entry(form, textvariable=self.output_var)
output_entry.grid(row=7, column=0, columnspan=3, sticky="ew", pady=(5, 15))
browse_button = ttk.Button(
form,
text="Choisir...",
command=self.choose_output_folder,
)
browse_button.grid(row=7, column=3, sticky="ew", padx=(15, 0), pady=(5, 15))
form.columnconfigure(0, weight=2)
form.columnconfigure(1, weight=1)
form.columnconfigure(2, weight=1)
form.columnconfigure(3, weight=1)
buttons = ttk.Frame(main)
buttons.pack(fill="x", pady=(5, 15))
self.download_button = ttk.Button(
buttons,
text="Télécharger",
command=self.start_download,
)
self.download_button.pack(side="left")
self.cancel_button = ttk.Button(
buttons,
text="Annuler",
command=self.cancel_download,
state="disabled",
)
self.cancel_button.pack(side="left", padx=(10, 0))
self.status_label = ttk.Label(
buttons,
text="Prêt.",
)
self.status_label.pack(side="left", padx=(20, 0))
log_frame = ttk.LabelFrame(main, text="Journal")
log_frame.pack(fill="both", expand=True)
self.log_text = tk.Text(
log_frame,
height=15,
wrap="word",
font=("Consolas", 10),
)
self.log_text.pack(side="left", fill="both", expand=True)
scrollbar = ttk.Scrollbar(
log_frame,
orient="vertical",
command=self.log_text.yview,
)
scrollbar.pack(side="right", fill="y")
self.log_text.configure(yscrollcommand=scrollbar.set)
def show_about(self):
about_window = tk.Toplevel(self.root)
about_window.title("À propos - YouTube Downloader")
about_window.geometry("720x560")
about_window.minsize(620, 460)
about_window.transient(self.root)
about_window.grab_set()
container = ttk.Frame(about_window, padding=20)
container.pack(fill="both", expand=True)
title = ttk.Label(
container,
text="À propos / Disclaimer",
font=("Segoe UI", 16, "bold"),
)
title.pack(anchor="w", pady=(0, 10))
text_frame = ttk.Frame(container)
text_frame.pack(fill="both", expand=True)
about_text = tk.Text(
text_frame,
wrap="word",
font=("Segoe UI", 10),
height=20,
)
about_text.pack(side="left", fill="both", expand=True)
scrollbar = ttk.Scrollbar(
text_frame,
orient="vertical",
command=about_text.yview,
)
scrollbar.pack(side="right", fill="y")
about_text.configure(yscrollcommand=scrollbar.set)
about_text.insert("1.0", ABOUT_TEXT.strip())
about_text.configure(state="disabled")
close_button = ttk.Button(
container,
text="Fermer",
command=about_window.destroy,
)
close_button.pack(anchor="e", pady=(15, 0))
def check_tools_on_start(self):
DOWNLOADS_DIR.mkdir(parents=True, exist_ok=True)
errors = validate_tools()
if errors:
self.add_log("Outils manquants :")
for error in errors:
self.add_log(f"- {error}")
self.add_log("")
self.add_log("Ajoute yt-dlp.exe et ffmpeg.exe dans le dossier bin.")
else:
self.add_log("yt-dlp et FFmpeg détectés correctement.")
def update_extract_fields(self):
mode = MODE_LABELS[self.mode_var.get()]
is_extract = mode in ["video_extract", "audio_extract"]
state = "normal" if is_extract else "disabled"
self.start_entry.configure(state=state)
self.end_entry.configure(state=state)
def choose_output_folder(self):
folder = filedialog.askdirectory()
if folder:
self.output_var.set(folder)
def start_download(self):
url = self.url_var.get().strip()
mode_label = self.mode_var.get()
mode = MODE_LABELS[mode_label]
if not url:
messagebox.showerror("Erreur", "Colle d'abord une URL YouTube.")
return
errors = validate_tools()
if errors:
messagebox.showerror("Outils manquants", "\n".join(errors))
return
try:
command = build_command(
url=url,
mode=mode,
output_folder=self.output_var.get().strip(),
filename=self.filename_var.get().strip(),
start_time=self.start_var.get().strip(),
end_time=self.end_var.get().strip(),
video_quality=self.video_quality_var.get(),
audio_quality=self.audio_quality_var.get(),
)
except Exception as error:
messagebox.showerror("Erreur", str(error))
return
self.download_button.configure(state="disabled")
self.cancel_button.configure(state="normal")
self.status_label.configure(text="Téléchargement en cours...")
self.add_log("")
self.add_log("Commande lancée :")
self.add_log(" ".join(command))
self.add_log("")
self.runner.run(
command=command,
on_line=self.queue_log,
on_finish=self.queue_finish,
)
def cancel_download(self):
self.runner.cancel()
self.status_label.configure(text="Annulation...")
def queue_log(self, line: str):
self.log_queue.put(("log", line))
def queue_finish(self, success: bool, message: str):
self.log_queue.put(("finish", success, message))
def poll_log_queue(self):
try:
while True:
item = self.log_queue.get_nowait()
if item[0] == "log":
self.add_log(item[1])
elif item[0] == "finish":
success, message = item[1], item[2]
self.add_log("")
self.add_log(message)
self.download_button.configure(state="normal")
self.cancel_button.configure(state="disabled")
if success:
self.status_label.configure(text="Terminé.")
else:
self.status_label.configure(text="Arrêté ou erreur.")
except queue.Empty:
pass
self.root.after(100, self.poll_log_queue)
def add_log(self, text: str):
self.log_text.insert("end", text + "\n")
self.log_text.see("end")
def main():
root = tk.Tk()
YouTubeDownloaderApp(root)
root.mainloop()
if __name__ == "__main__":
main()