first commit
This commit is contained in:
+45
@@ -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.
@@ -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
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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
Binary file not shown.
Reference in New Issue
Block a user