Upload files to "/"
This commit is contained in:
461
GUI_Download-v4.pyw
Normal file
461
GUI_Download-v4.pyw
Normal file
@@ -0,0 +1,461 @@
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox
|
||||
import subprocess
|
||||
import threading
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from tkinter import ttk
|
||||
|
||||
# ── Colour palette ────────────────────────────────────────────────────────────
|
||||
BG = "#1a1a2e" # deep navy — main background
|
||||
BG2 = "#16213e" # slightly darker — section cards
|
||||
ACCENT = "#e94560" # red-pink — active buttons & highlights
|
||||
ACCENT2 = "#0f3460" # mid-navy — inactive / borders
|
||||
FG = "#eaeaea" # near-white text
|
||||
FG_DIM = "#7a7a9a" # dimmed label text
|
||||
LOG_BG = "#0d0d1a" # near-black log background
|
||||
LOG_OK = "#4ec9b0"
|
||||
LOG_ERR = "#f44747"
|
||||
LOG_WARN = "#dcdcaa"
|
||||
LOG_INFO = "#9cdcfe"
|
||||
LOG_PLAIN = "#c8c8c8"
|
||||
FONT = ("Segoe UI", 9)
|
||||
FONT_BOLD = ("Segoe UI", 9, "bold")
|
||||
FONT_LOG = ("Consolas", 9)
|
||||
|
||||
|
||||
# ── Reusable widgets ──────────────────────────────────────────────────────────
|
||||
|
||||
class FlatButton(tk.Canvas):
|
||||
"""Canvas-drawn flat button — consistent look regardless of Windows theme."""
|
||||
|
||||
def __init__(self, parent, text, command,
|
||||
bg=ACCENT, fg=FG, dis_bg=ACCENT2, dis_fg=FG_DIM,
|
||||
width=130, height=30, radius=6):
|
||||
try:
|
||||
frame_bg = parent["bg"]
|
||||
except Exception:
|
||||
frame_bg = BG
|
||||
super().__init__(parent, width=width, height=height,
|
||||
bd=0, highlightthickness=0, bg=frame_bg)
|
||||
self._text = text
|
||||
self._command = command
|
||||
self._bg = bg
|
||||
self._fg = fg
|
||||
self._hov = self._shift(bg, 30)
|
||||
self._dis_bg = dis_bg
|
||||
self._dis_fg = dis_fg
|
||||
self._r = radius
|
||||
self._on = True
|
||||
self._draw(bg, fg)
|
||||
self.bind("<Enter>", lambda _: self._draw(self._hov, fg) if self._on else None)
|
||||
self.bind("<Leave>", lambda _: self._draw(bg, fg) if self._on else None)
|
||||
self.bind("<ButtonPress-1>", lambda _: self._draw(dis_bg, fg) if self._on else None)
|
||||
self.bind("<ButtonRelease-1>", self._release)
|
||||
|
||||
def _shift(self, c, n):
|
||||
r = min(255, int(c[1:3], 16) + n)
|
||||
g = min(255, int(c[3:5], 16) + n)
|
||||
b = min(255, int(c[5:7], 16) + n)
|
||||
return f"#{r:02x}{g:02x}{b:02x}"
|
||||
|
||||
def _rrect(self, x1, y1, x2, y2, r, **kw):
|
||||
self.create_arc(x1, y1, x1+2*r, y1+2*r, start=90, extent=90, **kw)
|
||||
self.create_arc(x2-2*r, y1, x2, y1+2*r, start=0, extent=90, **kw)
|
||||
self.create_arc(x1, y2-2*r, x1+2*r, y2, start=180, extent=90, **kw)
|
||||
self.create_arc(x2-2*r, y2-2*r, x2, y2, start=270, extent=90, **kw)
|
||||
self.create_rectangle(x1+r, y1, x2-r, y2, **kw)
|
||||
self.create_rectangle(x1, y1+r, x2, y2-r, **kw)
|
||||
|
||||
def _draw(self, bg, fg):
|
||||
self.delete("all")
|
||||
w, h = int(self["width"]), int(self["height"])
|
||||
self._rrect(1, 1, w-1, h-1, self._r, fill=bg, outline=bg)
|
||||
self.create_text(w//2, h//2, text=self._text, fill=fg,
|
||||
font=FONT_BOLD, anchor="center")
|
||||
|
||||
def _release(self, _=None):
|
||||
if self._on:
|
||||
self._draw(self._bg, self._fg)
|
||||
self._command()
|
||||
|
||||
def config_state(self, state):
|
||||
self._on = (state == "normal")
|
||||
self._draw(self._bg if self._on else self._dis_bg,
|
||||
self._fg if self._on else self._dis_fg)
|
||||
|
||||
|
||||
class RadioRow(tk.Frame):
|
||||
"""Row of pill-shaped toggle labels acting as radio buttons."""
|
||||
|
||||
def __init__(self, parent, variable, choices):
|
||||
super().__init__(parent, bg=BG2)
|
||||
self._var = variable
|
||||
self._btns = {}
|
||||
for label, value in choices:
|
||||
lbl = tk.Label(self, text=label, font=FONT,
|
||||
bg=ACCENT2, fg=FG_DIM, padx=10, pady=3,
|
||||
cursor="hand2")
|
||||
lbl.pack(side="left", padx=(0, 3))
|
||||
lbl.bind("<Button-1>", lambda _, v=value: self._var.set(v))
|
||||
self._btns[value] = lbl
|
||||
variable.trace_add("write", lambda *_: self._refresh())
|
||||
self._refresh()
|
||||
|
||||
def _refresh(self):
|
||||
cur = self._var.get()
|
||||
for val, lbl in self._btns.items():
|
||||
lbl.config(bg=ACCENT if val == cur else ACCENT2,
|
||||
fg=FG if val == cur else FG_DIM)
|
||||
|
||||
|
||||
|
||||
class Section(tk.Frame):
|
||||
"""Labelled card-style grouping frame."""
|
||||
def __init__(self, parent, title):
|
||||
super().__init__(parent, bg=BG2,
|
||||
highlightbackground=ACCENT2, highlightthickness=1)
|
||||
tk.Label(self, text=title.upper(), bg=BG2, fg=FG_DIM,
|
||||
font=("Segoe UI", 7, "bold")).pack(anchor="w", padx=10, pady=(6, 2))
|
||||
|
||||
|
||||
# ── Main application ──────────────────────────────────────────────────────────
|
||||
|
||||
class YTDLPDownloader(tk.Tk):
|
||||
|
||||
_SKIP = [
|
||||
re.compile(r'^\[download\]\s+\d+\.\d+%'),
|
||||
re.compile(r'^\[ffmpeg\]'),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.iconbitmap("icon.ico")
|
||||
self.wm_iconbitmap("icon.ico")
|
||||
self.title("NiX's YT Downloader")
|
||||
self.geometry("660x610")
|
||||
self.minsize(520, 500)
|
||||
self.resizable(True, True)
|
||||
self.configure(bg=BG)
|
||||
|
||||
self.download_format = tk.StringVar(value="mp3")
|
||||
self.bitrate_option = tk.StringVar(value="default")
|
||||
self.speed_limit = tk.StringVar(value="none")
|
||||
self.download_folder = tk.StringVar(value="")
|
||||
self.video_url = tk.StringVar(value="")
|
||||
self.url_list_file = tk.StringVar(value="")
|
||||
self.cookies_file = tk.StringVar(value="")
|
||||
self._process = None
|
||||
self._stop_req = False
|
||||
|
||||
self.ytdlp_path = os.path.join(os.path.dirname(sys.argv[0]), "yt-dlp.exe")
|
||||
self._build_ui()
|
||||
|
||||
if not os.path.isfile(self.ytdlp_path):
|
||||
self.log("⚠ yt-dlp.exe not found in script directory!", "warn")
|
||||
|
||||
# ── Build UI ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _make_entry(self, parent, textvariable):
|
||||
"""Dark-styled Entry with a right-click context menu for cut/copy/paste."""
|
||||
e = tk.Entry(parent, textvariable=textvariable,
|
||||
bg=LOG_BG, fg=FG, insertbackground=FG,
|
||||
relief="flat", font=FONT, bd=6)
|
||||
menu = tk.Menu(e, tearoff=0, bg=BG2, fg=FG,
|
||||
activebackground=ACCENT, activeforeground=FG,
|
||||
bd=0, relief="flat")
|
||||
menu.add_command(label="Cut", command=lambda: e.event_generate("<<Cut>>"))
|
||||
menu.add_command(label="Copy", command=lambda: e.event_generate("<<Copy>>"))
|
||||
menu.add_command(label="Paste", command=lambda: e.event_generate("<<Paste>>"))
|
||||
menu.add_separator()
|
||||
menu.add_command(label="Select all", command=lambda: e.select_range(0, "end"))
|
||||
menu.add_command(label="Clear", command=lambda: textvariable.set(""))
|
||||
e.bind("<Button-3>", lambda ev: menu.tk_popup(ev.x_root, ev.y_root))
|
||||
return e
|
||||
|
||||
def _build_ui(self):
|
||||
|
||||
# Header
|
||||
hdr = tk.Frame(self, bg=ACCENT2)
|
||||
hdr.pack(fill="x")
|
||||
tk.Label(hdr, text=" ▶ YT Audio Downloader",
|
||||
bg=ACCENT2, fg=FG, font=("Segoe UI", 11, "bold"),
|
||||
pady=9).pack(side="left")
|
||||
|
||||
# URL
|
||||
sec = Section(self, "Video URL (single link)")
|
||||
sec.pack(fill="x", padx=12, pady=(10, 4))
|
||||
self._make_entry(sec, self.video_url).pack(fill="x", padx=10, pady=(0, 8))
|
||||
|
||||
# Options row
|
||||
row = tk.Frame(self, bg=BG)
|
||||
row.pack(fill="x", padx=12, pady=4)
|
||||
|
||||
fmt_sec = Section(row, "Format")
|
||||
fmt_sec.pack(side="left", fill="both", expand=True, padx=(0, 4))
|
||||
RadioRow(fmt_sec, self.download_format,
|
||||
[("MP3", "mp3"), ("Opus", "opus"), ("Original", "original")]
|
||||
).pack(anchor="w", padx=10, pady=(0, 8))
|
||||
|
||||
br_sec = Section(row, "Bitrate")
|
||||
br_sec.pack(side="left", fill="both", expand=True, padx=(0, 4))
|
||||
RadioRow(br_sec, self.bitrate_option,
|
||||
[("Default", "default"), ("High", "high")]
|
||||
).pack(anchor="w", padx=10, pady=(0, 8))
|
||||
|
||||
spd_sec = Section(row, "Speed limit")
|
||||
spd_sec.pack(side="left", fill="both", expand=True)
|
||||
RadioRow(spd_sec, self.speed_limit,
|
||||
[("∞", "none"), ("100M", "100M"), ("10M", "10M"), ("1M", "1M")]
|
||||
).pack(anchor="w", padx=10, pady=(0, 8))
|
||||
|
||||
# Folder
|
||||
fsec = Section(self, "Download folder")
|
||||
fsec.pack(fill="x", padx=12, pady=4)
|
||||
frow = tk.Frame(fsec, bg=BG2)
|
||||
frow.pack(fill="x", padx=10, pady=(0, 8))
|
||||
FlatButton(frow, "Browse…", self.select_folder,
|
||||
width=90, height=26).pack(side="left")
|
||||
self.folder_label = tk.Label(frow, text="No folder selected",
|
||||
bg=BG2, fg=FG_DIM, font=FONT, anchor="w")
|
||||
self.folder_label.pack(side="left", padx=10, fill="x", expand=True)
|
||||
|
||||
# URL list (.txt)
|
||||
usec = Section(self, "URL list (optional — .txt, one link per line)")
|
||||
usec.pack(fill="x", padx=12, pady=4)
|
||||
urow = tk.Frame(usec, bg=BG2)
|
||||
urow.pack(fill="x", padx=10, pady=(0, 8))
|
||||
FlatButton(urow, "Browse…", self.select_url_list,
|
||||
width=90, height=26).pack(side="left")
|
||||
FlatButton(urow, "Clear", self.clear_url_list,
|
||||
bg=ACCENT2, width=60, height=26).pack(side="left", padx=(4, 0))
|
||||
self.url_list_label = tk.Label(urow, text="No file selected",
|
||||
bg=BG2, fg=FG_DIM, font=FONT, anchor="w")
|
||||
self.url_list_label.pack(side="left", padx=10, fill="x", expand=True)
|
||||
|
||||
# Cookies (.txt)
|
||||
csec = Section(self, "Cookies file (optional — for age-restricted / private content)")
|
||||
csec.pack(fill="x", padx=12, pady=4)
|
||||
crow = tk.Frame(csec, bg=BG2)
|
||||
crow.pack(fill="x", padx=10, pady=(0, 8))
|
||||
FlatButton(crow, "Browse…", self.select_cookies,
|
||||
width=90, height=26).pack(side="left")
|
||||
FlatButton(crow, "Clear", self.clear_cookies,
|
||||
bg=ACCENT2, width=60, height=26).pack(side="left", padx=(4, 0))
|
||||
self.cookies_label = tk.Label(crow, text="No file selected",
|
||||
bg=BG2, fg=FG_DIM, font=FONT, anchor="w")
|
||||
self.cookies_label.pack(side="left", padx=10, fill="x", expand=True)
|
||||
|
||||
# Progress — styled ttk bar (accent colour via Style)
|
||||
style = ttk.Style()
|
||||
style.theme_use("default")
|
||||
style.configure("Accent.Horizontal.TProgressbar",
|
||||
troughcolor=ACCENT2,
|
||||
background=ACCENT,
|
||||
bordercolor=BG,
|
||||
lightcolor=ACCENT,
|
||||
darkcolor=ACCENT)
|
||||
self.progress = ttk.Progressbar(self, style="Accent.Horizontal.TProgressbar",
|
||||
mode="indeterminate", length=200)
|
||||
self.progress.pack(fill="x", padx=12, pady=6)
|
||||
|
||||
# Buttons
|
||||
brow = tk.Frame(self, bg=BG)
|
||||
brow.pack(pady=4)
|
||||
self.dl_btn = FlatButton(brow, "⬇ Download",
|
||||
self.start_download_thread,
|
||||
width=140, height=32)
|
||||
self.dl_btn.pack(side="left", padx=5)
|
||||
self.stop_btn = FlatButton(brow, "⏹ Stop",
|
||||
self.stop_download,
|
||||
bg=ACCENT2, width=110, height=32)
|
||||
self.stop_btn.config_state("disabled")
|
||||
self.stop_btn.pack(side="left", padx=5)
|
||||
FlatButton(brow, "🗑 Clear log", self.clear_log,
|
||||
bg=ACCENT2, width=120, height=32).pack(side="left", padx=5)
|
||||
|
||||
# Log
|
||||
lsec = Section(self, "Log")
|
||||
lsec.pack(fill="both", expand=True, padx=12, pady=(4, 12))
|
||||
lframe = tk.Frame(lsec, bg=LOG_BG)
|
||||
lframe.pack(fill="both", expand=True, padx=10, pady=(0, 8))
|
||||
|
||||
self.log_box = tk.Text(lframe, state="disabled", wrap="word",
|
||||
bg=LOG_BG, fg=LOG_PLAIN, font=FONT_LOG,
|
||||
relief="flat", bd=0, selectbackground=ACCENT2)
|
||||
sb = tk.Scrollbar(lframe, command=self.log_box.yview,
|
||||
bg=BG2, troughcolor=LOG_BG,
|
||||
activebackground=ACCENT, bd=0,
|
||||
highlightthickness=0, width=10)
|
||||
self.log_box.configure(yscrollcommand=sb.set)
|
||||
sb.pack(side="right", fill="y")
|
||||
self.log_box.pack(side="left", fill="both", expand=True)
|
||||
|
||||
for tag, color in [("ok", LOG_OK), ("err", LOG_ERR),
|
||||
("warn", LOG_WARN), ("info", LOG_INFO),
|
||||
("plain", LOG_PLAIN)]:
|
||||
self.log_box.tag_configure(tag, foreground=color)
|
||||
|
||||
# ── Logging ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _tag_for(self, t):
|
||||
tl = t.lower()
|
||||
if any(w in tl for w in ("error", "failed", "❌")): return "err"
|
||||
if any(w in tl for w in ("warning", "warn", "⚠")): return "warn"
|
||||
if any(w in tl for w in ("complete", "✅", "already", "skip")): return "ok"
|
||||
if t.startswith("[") or "downloading" in tl or "destination" in tl: return "info"
|
||||
return "plain"
|
||||
|
||||
def log(self, msg, tag=None):
|
||||
for p in self._SKIP:
|
||||
if p.search(msg):
|
||||
return
|
||||
self.log_box.config(state="normal")
|
||||
self.log_box.insert("end", msg + "\n", tag or self._tag_for(msg))
|
||||
self.log_box.see("end")
|
||||
self.log_box.config(state="disabled")
|
||||
|
||||
def clear_log(self):
|
||||
self.log_box.config(state="normal")
|
||||
self.log_box.delete("1.0", "end")
|
||||
self.log_box.config(state="disabled")
|
||||
|
||||
# ── Folder / file pickers ─────────────────────────────────────────────────
|
||||
|
||||
def select_folder(self):
|
||||
f = filedialog.askdirectory()
|
||||
if f:
|
||||
self.download_folder.set(f)
|
||||
self.folder_label.config(text=f, fg=FG)
|
||||
|
||||
def select_url_list(self):
|
||||
f = filedialog.askopenfilename(
|
||||
title="Select URL list",
|
||||
filetypes=[("Text files", "*.txt"), ("All files", "*.*")])
|
||||
if f:
|
||||
self.url_list_file.set(f)
|
||||
self.url_list_label.config(text=f, fg=FG)
|
||||
|
||||
def clear_url_list(self):
|
||||
self.url_list_file.set("")
|
||||
self.url_list_label.config(text="No file selected", fg=FG_DIM)
|
||||
|
||||
def select_cookies(self):
|
||||
f = filedialog.askopenfilename(
|
||||
title="Select cookies file",
|
||||
filetypes=[("Text files", "*.txt"), ("All files", "*.*")])
|
||||
if f:
|
||||
self.cookies_file.set(f)
|
||||
self.cookies_label.config(text=f, fg=FG)
|
||||
|
||||
def clear_cookies(self):
|
||||
self.cookies_file.set("")
|
||||
self.cookies_label.config(text="No file selected", fg=FG_DIM)
|
||||
|
||||
# ── Download control ──────────────────────────────────────────────────────
|
||||
|
||||
def start_download_thread(self):
|
||||
self._stop_req = False
|
||||
self.stop_btn.config_state("normal")
|
||||
threading.Thread(target=self.download_video, daemon=True).start()
|
||||
|
||||
def stop_download(self):
|
||||
self._stop_req = True
|
||||
if self._process and self._process.poll() is None:
|
||||
self._process.terminate()
|
||||
self.log("⏹ Stop requested…", "warn")
|
||||
self.stop_btn.config_state("disabled")
|
||||
|
||||
def download_video(self):
|
||||
url = self.video_url.get().strip()
|
||||
url_list = self.url_list_file.get().strip()
|
||||
cookies = self.cookies_file.get().strip()
|
||||
folder = self.download_folder.get()
|
||||
fmt = self.download_format.get()
|
||||
bitrate = self.bitrate_option.get()
|
||||
speed = self.speed_limit.get()
|
||||
|
||||
# Need at least one source
|
||||
if not url and not url_list:
|
||||
messagebox.showerror("Error", "Please enter a URL or select a URL list file.")
|
||||
self._finish(); return
|
||||
if url_list and not os.path.isfile(url_list):
|
||||
messagebox.showerror("Error", f"URL list file not found:\n{url_list}")
|
||||
self._finish(); return
|
||||
if cookies and not os.path.isfile(cookies):
|
||||
messagebox.showerror("Error", f"Cookies file not found:\n{cookies}")
|
||||
self._finish(); return
|
||||
if not folder:
|
||||
messagebox.showerror("Error", "Please select a download folder.")
|
||||
self._finish(); return
|
||||
if not os.path.isfile(self.ytdlp_path):
|
||||
messagebox.showerror("Error", "yt-dlp.exe not found in script directory.")
|
||||
self._finish(); return
|
||||
|
||||
self.progress.start()
|
||||
self.log("── New download " + "─" * 42, "info")
|
||||
if url:
|
||||
self.log(f"URL : {url}", "info")
|
||||
if url_list:
|
||||
self.log(f"List : {url_list}", "info")
|
||||
if cookies:
|
||||
self.log(f"Cookies: {cookies}", "info")
|
||||
self.log(f"Format : {fmt} │ Bitrate : {bitrate} │ Speed : {speed}", "info")
|
||||
self.log(f"Folder : {folder}", "info")
|
||||
|
||||
out = os.path.join(folder, "%(title)s.%(ext)s")
|
||||
cmd = [self.ytdlp_path, "--no-cache-dir", "--no-overwrites"]
|
||||
|
||||
if cookies:
|
||||
cmd += ["--cookies", cookies]
|
||||
if speed != "none":
|
||||
cmd += ["--limit-rate", speed]
|
||||
if fmt == "original":
|
||||
cmd += ["-f", "bestaudio", "-o", out]
|
||||
else:
|
||||
cmd += ["-f", "bestaudio", "-x",
|
||||
"--audio-format", fmt, "--embed-thumbnail"]
|
||||
cmd += ["--audio-quality",
|
||||
("320k" if bitrate == "high" else "192k") if fmt == "mp3"
|
||||
else ("0" if bitrate == "high" else "5")]
|
||||
cmd += ["-o", out]
|
||||
|
||||
# Source: list file takes priority; single URL is appended last
|
||||
if url_list:
|
||||
cmd += ["--batch-file", url_list]
|
||||
if url:
|
||||
cmd += [url]
|
||||
|
||||
try:
|
||||
self._process = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, cwd=os.path.dirname(self.ytdlp_path),
|
||||
creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
for line in iter(self._process.stdout.readline, ""):
|
||||
if self._stop_req:
|
||||
break
|
||||
if line.strip():
|
||||
self.log(line.strip())
|
||||
self._process.stdout.close()
|
||||
self._process.wait()
|
||||
if self._stop_req:
|
||||
self.log("⏹ Download stopped by user.", "warn")
|
||||
elif self._process.returncode == 0:
|
||||
self.log("✅ Download complete!", "ok")
|
||||
else:
|
||||
self.log(f"❌ Download failed (exit code {self._process.returncode}).", "err")
|
||||
except Exception as e:
|
||||
self.log(f"❌ Unexpected error: {e}", "err")
|
||||
|
||||
self._finish()
|
||||
|
||||
def _finish(self):
|
||||
self.progress.stop()
|
||||
self._process = None
|
||||
self.stop_btn.config_state("disabled")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = YTDLPDownloader()
|
||||
app.mainloop()
|
||||
BIN
YT_Downloader_Documentation.docx
Normal file
BIN
YT_Downloader_Documentation.docx
Normal file
Binary file not shown.
BIN
YT_Downloader_Documentation.pdf
Normal file
BIN
YT_Downloader_Documentation.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user