428 lines
13 KiB
Python
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() |