first commit

This commit is contained in:
2026-05-21 21:26:38 +02:00
commit 02bc1c23b3
20 changed files with 28726 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
datas = [('logo.png', '.')]
binaries = [('yt-dlp.exe', '.'), ('ffmpeg.exe', '.')]
hiddenimports = []
tmp_ret = collect_all('ttkbootstrap')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
a = Analysis(
['main.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='BR-dl',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+29
View File
@@ -0,0 +1,29 @@
This file lists modules PyInstaller was not able to find. This does not
necessarily mean these modules are required for running your program. Both
Python's standard library and 3rd-party Python packages often conditionally
import optional modules, some of which may be available only on certain
platforms.
Types of import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!
missing module named 'collections.abc' - imported by _colorize (top-level), typing (top-level), traceback (top-level), logging (top-level), selectors (top-level), http.client (top-level), importlib.resources.readers (top-level), tracemalloc (top-level), inspect (top-level), PIL.Image (top-level), PIL._typing (top-level), xml.etree.ElementTree (top-level), PIL.TiffImagePlugin (top-level), PIL.ImageOps (top-level), PIL.ImagePalette (top-level), PIL.GimpGradientFile (conditional), PIL.ImageFilter (top-level), PIL.ImageQt (conditional), PIL.ImageMath (conditional), PIL.ImageSequence (conditional), PIL.PngImagePlugin (conditional), PIL.ImageDraw (top-level), PIL._imagingft (top-level), PIL.Jpeg2KImagePlugin (conditional)
missing module named _scproxy - imported by urllib.request (conditional)
missing module named posix - imported by os (conditional, optional), posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional), pathlib._os (optional)
missing module named resource - imported by posix (top-level)
missing module named fcntl - imported by pathlib._os (optional), subprocess (optional)
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib (optional), subprocess (delayed, conditional, optional)
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (optional), netrc (delayed, optional), subprocess (delayed, conditional, optional)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional)
missing module named numpy - imported by PIL._typing (conditional, optional)
missing module named olefile - imported by PIL.FpxImagePlugin (top-level), PIL.MicImagePlugin (top-level)
missing module named defusedxml - imported by PIL.Image (optional)
missing module named _posixsubprocess - imported by subprocess (conditional)
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+401
View File
@@ -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()
BIN
View File
Binary file not shown.