first commit
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user