import os import re import sys import threading import queue import subprocess import ttkbootstrap as ttk from ttkbootstrap.constants import * from ttkbootstrap.scrolled import ScrolledText from tkinter import filedialog, messagebox try: from PIL import Image, ImageTk except Exception: Image = None ImageTk = None def resource_path(filename): if hasattr(sys, "_MEIPASS"): return os.path.join(sys._MEIPASS, filename) return os.path.join(os.path.abspath("."), filename) class YTDLPGui: def __init__(self, root): self.root = root self.root.title("BR-dl") self.root.geometry("1120x760") self.root.minsize(940, 640) self.log_queue = queue.Queue() self.process = None self.cancel_requested = False self.logo_image = None self.path_var = ttk.StringVar(value=os.path.expanduser("~/Downloads")) self.playlist_var = ttk.StringVar(value="Meine Playlist") self.format_var = ttk.StringVar(value="mp3") outer = ttk.Frame(root, padding=20) outer.pack(fill=BOTH, expand=True) header = ttk.Frame(outer) header.pack(fill=X, pady=(0, 18)) header_left = ttk.Frame(header) header_left.pack(side=LEFT, fill=X, expand=True) ttk.Label( header_left, text="BR-dl", font=("Segoe UI", 26, "bold"), bootstyle="light" ).pack(anchor=W) ttk.Label( header_left, text="Mehrere Links einfügen, Format auswählen und gesammelt in einen Ordner laden.", font=("Segoe UI", 10), bootstyle="secondary" ).pack(anchor=W, pady=(4, 0)) header_right = ttk.Frame(header) header_right.pack(side=RIGHT, anchor=E) self.logo_label = ttk.Label(header_right, text="") self.logo_label.pack(anchor=E) self.load_logo() content = ttk.Frame(outer) content.pack(fill=BOTH, expand=True) left = ttk.Frame(content) left.pack(side=LEFT, fill=BOTH, expand=True, padx=(0, 10)) right = ttk.Frame(content) right.pack(side=LEFT, fill=Y, padx=(10, 0)) links_card = ttk.Labelframe(left, text=" Links ", padding=16, bootstyle="info") links_card.pack(fill=BOTH, expand=True, pady=(0, 12)) ttk.Label( links_card, text="Ein Link pro Zeile", bootstyle="secondary" ).pack(anchor=W, pady=(0, 8)) self.url_box = ScrolledText(links_card, height=12, autohide=True) self.url_box.pack(fill=BOTH, expand=True) log_card = ttk.Labelframe(left, text=" Verlauf ", padding=16, bootstyle="dark") log_card.pack(fill=BOTH, expand=True) self.log_box = ScrolledText(log_card, height=14, autohide=True) self.log_box.pack(fill=BOTH, expand=True) settings_card = ttk.Labelframe(right, text=" Einstellungen ", padding=18, bootstyle="primary") settings_card.pack(fill=X) ttk.Label(settings_card, text="Playlist-/Ordnername", bootstyle="light").pack(anchor=W) ttk.Entry( settings_card, textvariable=self.playlist_var, bootstyle="info" ).pack(fill=X, pady=(6, 14)) ttk.Label(settings_card, text="Audio-Format", bootstyle="light").pack(anchor=W) format_frame = ttk.Frame(settings_card) format_frame.pack(fill=X, pady=(8, 14)) self.mp3_radio = ttk.Radiobutton( format_frame, text="MP3", variable=self.format_var, value="mp3", bootstyle="success-toolbutton" ) self.mp3_radio.pack(side=LEFT, fill=X, expand=True, padx=(0, 6), ipady=8) self.wma_radio = ttk.Radiobutton( format_frame, text="WMA", variable=self.format_var, value="wma", bootstyle="warning-toolbutton" ) self.wma_radio.pack(side=LEFT, fill=X, expand=True, padx=(6, 0), ipady=8) ttk.Label( settings_card, text="MP3 direkt, WMA über FFmpeg-Konvertierung", bootstyle="secondary" ).pack(anchor=W, pady=(0, 14)) ttk.Label(settings_card, text="Zielordner", bootstyle="light").pack(anchor=W) path_row = ttk.Frame(settings_card) path_row.pack(fill=X, pady=(6, 14)) ttk.Entry( path_row, textvariable=self.path_var, bootstyle="info" ).pack(side=LEFT, fill=X, expand=True) ttk.Button( path_row, text="Wählen", command=self.choose_folder, bootstyle="secondary-outline" ).pack(side=LEFT, padx=(10, 0)) action_card = ttk.Labelframe(right, text=" Aktionen ", padding=18, bootstyle="success") action_card.pack(fill=X, pady=(12, 0)) self.download_btn = ttk.Button( action_card, text="Alle herunterladen", command=self.start_download, bootstyle="success", width=24 ) self.download_btn.pack(fill=X, pady=(0, 10), ipady=8) self.stop_btn = ttk.Button( action_card, text="Abbrechen", command=self.stop_download, bootstyle="danger-outline", state=DISABLED, width=24 ) self.stop_btn.pack(fill=X, ipady=8) ttk.Label( action_card, text="Die Dateien landen gesammelt in deinem gewählten Playlist-Ordner.", wraplength=260, justify=LEFT, bootstyle="secondary" ).pack(anchor=W, pady=(14, 10)) self.progress = ttk.Progressbar( action_card, mode="indeterminate", bootstyle="success-striped" ) self.progress.pack(fill=X) self.root.after(100, self.poll_log_queue) def load_logo(self): image_path = resource_path("logo.png") if not os.path.exists(image_path): return if Image is None or ImageTk is None: return try: img = Image.open(image_path).convert("RGBA") img.thumbnail((140, 140)) self.logo_image = ImageTk.PhotoImage(img) self.logo_label.configure(image=self.logo_image) except Exception: pass def choose_folder(self): folder = filedialog.askdirectory(initialdir=self.path_var.get()) if folder: self.path_var.set(folder) def log(self, text): self.log_box.insert("end", text + "\n") self.log_box.see("end") def poll_log_queue(self): while not self.log_queue.empty(): self.log(self.log_queue.get()) self.root.after(100, self.poll_log_queue) def safe_folder_name(self, name): name = name.strip() if not name: return "Meine Playlist" return re.sub(r'[<>:"/\\\\|?*]', "_", name) def start_download(self): raw_text = self.url_box.get("1.0", "end").strip() urls = [line.strip() for line in raw_text.splitlines() if line.strip()] out_dir = self.path_var.get().strip() playlist_name = self.safe_folder_name(self.playlist_var.get()) audio_format = self.format_var.get().strip().lower() if not urls: messagebox.showerror("Fehler", "Bitte mindestens einen Link eingeben.") return if not os.path.isdir(out_dir): messagebox.showerror("Fehler", "Der Zielordner existiert nicht.") return if audio_format not in {"mp3", "wma"}: messagebox.showerror("Fehler", "Bitte ein gültiges Audio-Format wählen.") return target_dir = os.path.join(out_dir, playlist_name) os.makedirs(target_dir, exist_ok=True) self.cancel_requested = False self.download_btn.config(state=DISABLED) self.stop_btn.config(state=NORMAL) self.progress.start(10) self.log(f"Starte Batch-Download mit {len(urls)} Link(s)...") self.log(f"Zielordner: {target_dir}") self.log(f"Format: {audio_format}") thread = threading.Thread( target=self.run_download, args=(urls, target_dir, audio_format), daemon=True ) thread.start() def run_download(self, urls, target_dir, audio_format): try: for index, url in enumerate(urls, start=1): if self.cancel_requested: self.log_queue.put("Download abgebrochen.") break self.log_queue.put(f"[{index}/{len(urls)}] Starte: {url}") if audio_format == "mp3": self.download_as_mp3(url, target_dir) elif audio_format == "wma": self.download_as_wma(url, target_dir) if not self.cancel_requested: self.log_queue.put("Alle Downloads abgeschlossen.") except FileNotFoundError: self.log_queue.put("yt-dlp.exe oder ffmpeg.exe wurde nicht gefunden.") self.log_queue.put("Stelle sicher, dass beide Dateien mit in die EXE eingebunden wurden.") except Exception as e: self.log_queue.put(f"Unerwarteter Fehler: {e}") finally: self.process = None self.root.after(0, self.reset_ui) def download_as_mp3(self, url, target_dir): yt_dlp_exe = resource_path("yt-dlp.exe") output_template = os.path.join(target_dir, "%(title)s.%(ext)s") cmd = [ yt_dlp_exe, "-x", "--audio-format", "mp3", "--audio-quality", "0", "--embed-metadata", "-o", output_template, url ] self.run_process(cmd) def download_as_wma(self, url, target_dir): yt_dlp_exe = resource_path("yt-dlp.exe") ffmpeg_exe = resource_path("ffmpeg.exe") temp_template = os.path.join(target_dir, "%(title)s.%(ext)s") cmd_download = [ yt_dlp_exe, "-f", "bestaudio", "--no-playlist", "-o", temp_template, url ] self.run_process(cmd_download) latest_file = self.find_latest_audio_file(target_dir) if not latest_file: self.log_queue.put("Konnte heruntergeladene Datei für WMA-Konvertierung nicht finden.") return base_name, _ = os.path.splitext(latest_file) wma_file = base_name + ".wma" self.log_queue.put(f"Konvertiere nach WMA: {os.path.basename(wma_file)}") cmd_ffmpeg = [ ffmpeg_exe, "-y", "-i", latest_file, "-vn", "-ar", "44100", "-ac", "2", "-c:a", "wmav2", "-b:a", "192k", wma_file ] self.run_process(cmd_ffmpeg) if os.path.exists(wma_file): try: os.remove(latest_file) self.log_queue.put(f"Temporäre Datei gelöscht: {os.path.basename(latest_file)}") except Exception as e: self.log_queue.put(f"Temporäre Datei konnte nicht gelöscht werden: {e}") def find_latest_audio_file(self, folder): files = [] for name in os.listdir(folder): path = os.path.join(folder, name) if os.path.isfile(path) and not name.lower().endswith(".wma"): files.append(path) if not files: return None files.sort(key=os.path.getmtime, reverse=True) return files[0] def run_process(self, cmd): self.process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, universal_newlines=True ) for line in self.process.stdout: self.log_queue.put(line.rstrip()) code = self.process.wait() if code != 0 and not self.cancel_requested: self.log_queue.put(f"Fehler: Prozess beendet mit Code {code}") def stop_download(self): self.cancel_requested = True if self.process and self.process.poll() is None: self.process.terminate() self.log("Abbruch angefordert...") def reset_ui(self): self.progress.stop() self.download_btn.config(state=NORMAL) self.stop_btn.config(state=DISABLED) if __name__ == "__main__": root = ttk.Window(themename="superhero") app = YTDLPGui(root) root.mainloop()