commit a0adfb4e8a863245184227484dd88e11771a31d4 Author: unixaxwell Date: Sat Apr 11 23:33:28 2026 +0000 Upload files to "/" diff --git a/GUI_Download-v4.4.pyw b/GUI_Download-v4.4.pyw new file mode 100644 index 0000000..adec7c1 --- /dev/null +++ b/GUI_Download-v4.4.pyw @@ -0,0 +1,486 @@ +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("", lambda _: self._draw(self._hov, fg) if self._on else None) + self.bind("", lambda _: self._draw(bg, fg) if self._on else None) + self.bind("", lambda _: self._draw(dis_bg, fg) if self._on else None) + self.bind("", 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("", 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.save_description = tk.BooleanVar(value=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("<>")) + menu.add_command(label="Copy", command=lambda: e.event_generate("<>")) + menu.add_command(label="Paste", command=lambda: e.event_generate("<>")) + 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("", 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 + # Options checkboxes + chk_row = tk.Frame(self, bg=BG) + chk_row.pack(pady=(0, 2)) + tk.Checkbutton(chk_row, text="Save description - for mixes with tracklist", variable=self.save_description, + bg=BG, fg=FG, selectcolor=BG2, activebackground=BG, + activeforeground=FG, font=FONT).pack(side="left", padx=8) + + 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"Format : {fmt} | Speed : {speed}", "info") + self.log(f"Folder : {folder}", "info") + + #out = os.path.join(folder, "%(title)s.%(ext)s") + out = os.path.join(folder, "%(artist&{} - |)s%(title)s.%(ext)s") + cmd = [self.ytdlp_path, "--no-cache-dir", "--no-overwrites", "--js-runtimes", "node"] + if self.save_description.get(): + cmd += ["--write-description"] + + if cookies: + cmd += ["--cookies", cookies] + if speed != "none": + cmd += ["--limit-rate", speed] + + #This block replaces the one after it + if fmt == "original": + cmd += ["-f", "bestaudio/best", + "--write-thumbnail", + "--embed-metadata", "-o", out] + else: + cmd += ["-f", "bestaudio", "-x", + "--audio-format", fmt, "--embed-thumbnail", + "--embed-metadata", + "--audio-quality", "0", + "-o", out] + + #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() diff --git a/YT_Downloader_Documentation_v4.4.pdf b/YT_Downloader_Documentation_v4.4.pdf new file mode 100644 index 0000000..b99d36a Binary files /dev/null and b/YT_Downloader_Documentation_v4.4.pdf differ