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( "<>", 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()