Files
2026-05-21 21:26:38 +02:00

401 lines
12 KiB
Python

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