What makes us different from other similar websites? › Forums › Tech › How to Stop Ad Blocker Detection on Websites Like YouTube
Tagged: Adblocker warning., Anti-Adblocker Killer, script, Tamper Monkey, YouTube, YouTube Linux Application, YouTube Red, yt-dlp
- This topic has 16 replies, 1 voice, and was last updated 1 week, 3 days ago by
thumbtak.
-
AuthorPosts
-
July 17, 2025 at 9:47 am #8154
thumbtak
ModeratorBatch mode added, interface changes, more reliable downloads, and more.
import tkinter as tk from tkinter import filedialog, messagebox, scrolledtext import subprocess import os import shutil import re import threading import json import glob import sys import webbrowser import time # Import yt_dlp library # This library is crucial for the application's core functionality. # If it's not installed, the application will prompt the user and exit. try: import yt_dlp except ImportError: messagebox.showerror("Error", "The 'yt-dlp' Python library is not installed.\n" "Please install it using: pip install yt-dlp\n" "Then try running the application again.") exit() # --- Configuration Constants --- # Base name for temporarily downloaded video files. # yt-dlp will append the correct accurate extension (e.g., downloaded_video.mp4). # This is now a template for the final output, including title and ID for uniqueness. OUTPUT_FILENAME_TEMPLATE = "%(title)s-%(id)s.%(ext)s" # File to store the path of the last used save folder for user convenience. # Stored in JSON format for robustness. LAST_SAVE_FOLDER_FILE = ".last_save_folder.json" # Temporary WAV file for the audio test sound. TEST_SOUND_FILE = "taks_shack_test_test_sound.wav" # --- Windows 95 Theme Colors --- _WINDOW_BG = '#C0C0C0' # Standard light gray _BUTTON_FACE = '#C0C0C0' _TEXT_BG = '#FFFFFF' # White for entry/text _TEXT_FG = '#000000' # Black for entry/text _LOG_BG = '#000000' # Black for debug console _LOG_FG = '#DDDDDD' # Light gray for debug text _ACCENT_BLUE = '#000080' # Dark blue, typical for selections/progress _DARK_GRAY = '#808080' # For sunken/raised effects _LIGHT_GRAY = '#F0F0F0' # For highlight effects # Defragmenter specific colors _NOT_DEFRAGMENTED_COLOR = '#000080' # Dark Blue _IN_PROGRESS_COLOR = '#FF0000' # Red _DEFRAGMENTED_COLOR = '#00FFFF' # Cyan (Light Blue) # --- Windows 95 Theme Fonts --- _HEADER_FONT = ("Fixedsys", 14, "bold") _GENERAL_FONT = ("MS Sans Serif", 9) # Using generic "MS Sans Serif" which is often mapped to a suitable system font _MONOSPACE_FONT = ("Fixedsys", 10) # --- Progress Grid Configuration --- _GRID_ROWS = 40 # Doubled rows _GRID_COLS = 160 # Doubled columns _GRID_LINE_THICKNESS = 2 # Thickness of the grid lines (Increased for more prominent lines) # --- yt-dlp Format Strategies (Ordered from most preferred to least) --- # These strategies will be attempted sequentially if a download fails, # particularly if it's due to a 403 Forbidden error. FORMAT_STRATEGIES = [ 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]', # Best separate video/audio (MP4), then best combined MP4 'best[height<=1080][ext=mp4]', # Best MP4 up to 1080p (combined) 'best[height<=720][ext=mp4]', # Best MP4 up to 720p (combined) 'best[height<=480][ext=mp4]', # Best MP4 up to 480p (combined) 'best' # Fallback to absolute best available (could be webm, etc., combined) ] class YouTubeDownloaderApp: """ A Tkinter-based GUI application for downloading and playing YouTube videos. It leverages the yt-dlp Python library for downloads and external players like smplayer for video playback. """ def __init__(self, root): self.root = root self.root.title("Defrag YouTube Video") self.root.geometry("1200x900") self.root.resizable(True, True) self.root.configure(bg=_WINDOW_BG) self.downloaded_video_path = None self.suggested_video_title = None # This will now store the actual YouTube title self.smplayer_process = None self.download_queue = [] self.current_download_index = -1 self.grid_rects = [] self.last_known_progress_percent = 0 self.yt_dlp_process = None # To store the yt-dlp subprocess self.yt_dlp_output_buffer = [] # Buffer to capture all yt-dlp output for error checking self.current_temp_dir = None # To store the temporary directory for the current download self.last_save_folder = self._load_last_save_folder() self.play_after_download_var = tk.BooleanVar(value=True) self.auto_save_after_download_var = tk.BooleanVar(value=False) self.auto_delete_after_play_var = tk.BooleanVar(value=False) self._create_widgets() self._check_dependencies() def _load_last_save_folder(self): """ Loads the last used save folder path from a JSON file. Returns an empty string if the file doesn't exist or is unreadable. """ if os.path.exists(LAST_SAVE_FOLDER_FILE): try: with open(LAST_SAVE_FOLDER_FILE, 'r') as f: data = json.load(f) return data.get("last_folder", "") except (json.JSONDecodeError, IOError) as e: self._log(f"Warning: Could not load last save folder: {e}", "orange") return "" def _save_last_save_folder(self, folder_path): """ Saves the given folder path as the last used save folder to a JSON file. """ try: with open(LAST_SAVE_FOLDER_FILE, 'w') as f: json.dump({"last_folder": folder_path}, f) self._log(f"Last save folder updated to: {folder_path}", "green") except IOError as e: self._log(f"Error saving last save folder: {e}", "red") def _check_dependencies(self): """ Checks for required external system dependencies (smplayer, ffmpeg) and optional ones (espeak-ng, aplay), and checks for yt-dlp updates. Informs the user about missing dependencies and exits if critical ones are absent. """ self._log("Checking system dependencies...", "blue") missing_critical = [] missing_optional = [] if subprocess.run(["which", "smplayer"], capture_output=True).returncode != 0: missing_critical.append("smplayer") else: self._log("smplayer: [OK]", "green") if subprocess.run(["which", "ffmpeg"], capture_output=True).returncode != 0: missing_critical.append("ffmpeg") else: self._log("ffmpeg: [OK]", "green") espeak_ng_ok = subprocess.run(["which", "espeak-ng"], capture_output=True).returncode == 0 aplay_ok = subprocess.run(["which", "aplay"], capture_output=True).returncode == 0 if not espeak_ng_ok: missing_optional.append("espeak-ng") if not aplay_ok: missing_optional.append("aplay") if espeak_ng_ok and aplay_ok: self._log("espeak-ng & aplay: [OK] (for audio test)", "green") elif missing_optional: self._log(f"Optional tools missing: {', '.join(missing_optional)}. Audio test will be skipped.", "orange") if missing_critical: message = (f"The following critical tools are missing and must be installed for this application to function:\n" f"{', '.join(missing_critical)}\n\n" f"Please install them using your system's package manager (e.g., 'sudo apt install {missing_critical[0]}' on Debian/Ubuntu).\n" f"The application will now exit.") messagebox.showerror("Missing Dependencies", message) self.root.destroy() else: self._log("All critical dependencies are installed.", "green") self._log("Dependency check complete.", "blue") self._log("Checking for yt-dlp updates...", "blue") try: installed_version = yt_dlp.version.__version__ self._log(f"Installed yt-dlp version: {installed_version}") pip_outdated_process = subprocess.run( [sys.executable, "-m", "pip", "list", "--outdated"], capture_output=True, text=True, check=True ) outdated_packages = pip_outdated_process.stdout if "yt-dlp" in outdated_packages: self._log("A newer version of yt-dlp is available!", "orange") if messagebox.askyesno("Update Available", "A newer version of yt-dlp is available. Would you like to update it now?\n\n" "The application will restart after the application."): self._log("Attempting to update yt-dlp...", "blue") try: update_command = [sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"] update_process = subprocess.run( update_command, capture_output=True, text=True, check=True ) self._log("yt-dlp updated successfully!", "green") messagebox.showinfo("Update Complete", "yt-dlp has been updated. Please restart the application to use the new version.") self.root.destroy() python = sys.executable os.execl(python, python, *sys.argv) except subprocess.CalledProcessError as update_e: error_message = update_e.stderr.strip() self._log(f"Error updating yt-dlp: {error_message}", "red") if "externally-managed-environment" in error_message: self._log("This error usually means your system prevents direct pip installs.", "orange") self._log("It is highly recommended to use a Python virtual environment to manage dependencies.", "orange") if messagebox.askyesno("System Package Conflict", "Your system prevents direct pip installs (externally-managed-environment).\n" "It is strongly recommended to use a Python virtual environment.\n\n" "Do you want to force the update using '--break-system-packages'?\n" "WARNING: This can potentially break your system's Python installation.", icon='warning'): self._log("Attempting to force update with --break-system-packages...", "orange") force_update_command = [sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp", "--break-system-packages"] try: force_update_process = subprocess.run( force_update_command, capture_output=True, text=True, check=True ) self._log("yt-dlp forced update successful!", "green") messagebox.showinfo("Update Complete (Forced)", "yt-dlp has been forced updated. Please restart the application to use the new version.") self.root.destroy() python = sys.executable os.execl(python, python, *sys.argv) except subprocess.CalledProcessError as force_e: self._log(f"Forced update failed: {force_e.stderr.strip()}", "red") messagebox.showerror("Forced Update Error", f"Forced update failed: {force_e.stderr.strip()}") except Exception as force_e: self._log(f"An unexpected error occurred during forced update: {force_e}", "red") messagebox.showerror("Forced Update Error", f"An unexpected error occurred during forced update: {force_e}") else: self._log("Forced update cancelled by user.", "blue") messagebox.showinfo("Update Cancelled", "Update cancelled. Please consider using a virtual environment for future updates.") else: messagebox.showerror("Update Error", f"Failed to update yt-dlp: {error_message}") except Exception as update_e: self._log(f"An unexpected error occurred during yt-dlp update: {update_e}", "red") messagebox.showerror("Update Error", f"An unexpected error occurred during yt-dlp update: {update_e}") else: self._log("Skipping yt-dlp update as requested by user.", "blue") else: self._log("yt-dlp is up to date.", "green") except subprocess.CalledProcessError as e: self._log(f"Warning: Could not check for yt-dlp updates (pip error: {e.stderr.strip()}).", "orange") except FileNotFoundError: self._log("Warning: 'pip' command not found. Cannot check for yt-dlp updates.", "orange") except AttributeError: self._log("Warning: Could not determine yt-dlp version. Ensure yt-dlp is correctly installed.", "orange") except Exception as e: self._log(f"An unexpected error occurred during yt-dlp update check: {e}", "orange") def _create_widgets(self): """ Creates and arranges all the GUI widgets within the main window. """ main_frame = tk.Frame(self.root, bg=_WINDOW_BG) main_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=15) header_label = tk.Label(main_frame, text="Defrag YouTube Video", font=_HEADER_FONT, bg=_WINDOW_BG, fg=_ACCENT_BLUE, relief=tk.RIDGE, borderwidth=2) header_label.pack(pady=10, fill=tk.X) self.url_label = tk.Label(main_frame, text="https://taksshack.com", foreground=_TEXT_FG, background=_WINDOW_BG, font=_MONOSPACE_FONT, cursor="") self.url_label.pack(pady=(0, 10)) self.url_label.bind("<Button-1>", lambda e: self._open_url("https://taksshack.com")) url_frame = tk.LabelFrame(main_frame, text="YouTube Video URL", bg=_WINDOW_BG, font=_GENERAL_FONT, relief=tk.GROOVE, borderwidth=2) url_frame.pack(fill=tk.X, pady=10, padx=5) self.url_entry = tk.Entry(url_frame, width=80, bg=_TEXT_BG, fg=_TEXT_FG, insertbackground=_TEXT_FG, relief=tk.SUNKEN, borderwidth=2, font=_MONOSPACE_FONT) self.url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 5), pady=5) self.url_entry.bind("<Return>", lambda event: self.start_process_single_link()) self.start_button = tk.Button(url_frame, text="START", command=self.start_process_single_link, width=10, font=_GENERAL_FONT, bg=_BUTTON_FACE, fg=_TEXT_FG, relief=tk.RAISED, borderwidth=2) self.start_button.pack(side=tk.LEFT, padx=(0, 5), pady=5) self.batch_download_button = tk.Button(url_frame, text="BATCH", command=self._open_batch_download_window, width=15, font=_GENERAL_FONT, bg=_BUTTON_FACE, fg=_TEXT_FG, relief=tk.RAISED, borderwidth=2) self.batch_download_button.pack(side=tk.RIGHT, padx=(0, 5), pady=5) checkbox_frame = tk.Frame(main_frame, bg=_WINDOW_BG) checkbox_frame.pack(pady=(5, 10), anchor=tk.W, padx=5) self.play_after_download_checkbox = tk.Checkbutton( checkbox_frame, text="Play video after download completes", variable=self.play_after_download_var, bg=_WINDOW_BG, fg=_TEXT_FG, selectcolor=_WINDOW_BG, font=_GENERAL_FONT ) self.play_after_download_checkbox.pack(side=tk.LEFT, padx=(0, 15)) self.auto_save_checkbox = tk.Checkbutton( checkbox_frame, text="Auto-save video to last used folder after download", variable=self.auto_save_after_download_var, bg=_WINDOW_BG, fg=_TEXT_FG, selectcolor=_WINDOW_BG, font=_GENERAL_FONT ) self.auto_save_checkbox.pack(side=tk.LEFT, padx=(0, 15)) self.auto_delete_checkbox = tk.Checkbutton( checkbox_frame, text="Auto-delete temporary video after playing", variable=self.auto_delete_after_play_var, bg=_WINDOW_BG, fg=_TEXT_FG, selectcolor=_WINDOW_BG, font=_GENERAL_FONT ) self.auto_delete_checkbox.pack(side=tk.LEFT, padx=(0, 15)) cookie_frame = tk.LabelFrame(main_frame, text="Cookie Options (Optional)", bg=_WINDOW_BG, font=_GENERAL_FONT, relief=tk.GROOVE, borderwidth=2) cookie_frame.pack(fill=tk.X, pady=10, padx=5) self.cookie_method = tk.StringVar(value="none") tk.Radiobutton(cookie_frame, text="No Cookies", variable=self.cookie_method, value="none", command=self._toggle_cookie_input, bg=_WINDOW_BG, fg=_TEXT_FG, selectcolor=_WINDOW_BG, font=_GENERAL_FONT).pack(side=tk.LEFT, padx=5, pady=5) tk.Radiobutton(cookie_frame, text="From Browser", variable=self.cookie_method, value="browser", command=self._toggle_cookie_input, bg=_WINDOW_BG, fg=_TEXT_FG, selectcolor=_WINDOW_BG, font=_GENERAL_FONT).pack(side=tk.LEFT, padx=5, pady=5) tk.Radiobutton(cookie_frame, text="From File", variable=self.cookie_method, value="file", command=self._toggle_cookie_input, bg=_WINDOW_BG, fg=_TEXT_FG, selectcolor=_WINDOW_BG, font=_GENERAL_FONT).pack(side=tk.LEFT, padx=5, pady=5) self.browser_var = tk.StringVar(value="firefox") self.browser_options = ["firefox", "chrome", "chromium", "brave", "edge", "opera", "safari", "vivaldi", "librewolf"] self.browser_menu = tk.OptionMenu(cookie_frame, self.browser_var, *self.browser_options) self.browser_menu.pack(side=tk.LEFT, padx=5, pady=5) self.browser_menu.config(state=tk.DISABLED, bg=_BUTTON_FACE, fg=_TEXT_FG, relief=tk.RAISED, borderwidth=2, highlightbackground=_WINDOW_BG, font=_GENERAL_FONT) self.browser_menu["menu"].config(bg=_BUTTON_FACE, fg=_TEXT_FG, font=_GENERAL_FONT) self.cookie_file_entry = tk.Entry(cookie_frame, width=40, bg=_TEXT_BG, fg=_TEXT_FG, insertbackground=_TEXT_FG, relief=tk.SUNKEN, borderwidth=2, font=_MONOSPACE_FONT) self.cookie_file_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5) self.cookie_file_entry.config(state=tk.DISABLED) self.browse_cookie_button = tk.Button(cookie_frame, text="BROWSE", command=self._browse_cookie_file, font=_GENERAL_FONT, bg=_BUTTON_FACE, fg=_TEXT_FG, relief=tk.RAISED, borderwidth=2) self.browse_cookie_button.pack(side=tk.LEFT, padx=5, pady=5) self.browse_cookie_button.config(state=tk.DISABLED) progress_section_frame = tk.Frame(main_frame, bg=_WINDOW_BG) progress_section_frame.pack(pady=10, padx=5, fill=tk.X) self.grid_canvas = tk.Canvas(progress_section_frame, bg=_NOT_DEFRAGMENTED_COLOR, relief=tk.SUNKEN, borderwidth=2, highlightbackground=_DARK_GRAY) self.grid_canvas.pack(pady=(0, 5), fill=tk.BOTH, expand=True) self.grid_canvas.bind("<Configure>", self._on_canvas_resize) self.grid_rects = [] self.progress_label = tk.Label(progress_section_frame, text="0% completed", bg=_WINDOW_BG, fg=_TEXT_FG, font=_GENERAL_FONT) self.progress_label.pack(pady=(5, 5)) self.small_progress_bar = tk.Canvas(progress_section_frame, height=10, bg=_WINDOW_BG, relief=tk.SUNKEN, borderwidth=1, highlightbackground=_DARK_GRAY) self.small_progress_bar.pack(pady=5, fill=tk.X, expand=True) self.small_progress_fill = self.small_progress_bar.create_rectangle(0, 0, 0, 10, fill=_ACCENT_BLUE, outline="") legend_frame = tk.Frame(progress_section_frame, bg=_WINDOW_BG) legend_frame.pack(pady=(5, 0), fill=tk.X) def create_legend_item(parent, color, text): item_frame = tk.Frame(parent, bg=_WINDOW_BG) item_frame.pack(side=tk.LEFT, padx=10) color_block = tk.Frame(item_frame, width=15, height=15, bg=color, relief=tk.SUNKEN, borderwidth=1) color_block.pack(side=tk.LEFT) label = tk.Label(item_frame, text=text, bg=_WINDOW_BG, fg=_TEXT_FG, font=_GENERAL_FONT) label.pack(side=tk.LEFT) create_legend_item(legend_frame, _NOT_DEFRAGMENTED_COLOR, "Not downloaded") create_legend_item(legend_frame, _IN_PROGRESS_COLOR, "In progress") create_legend_item(legend_frame, _DEFRAGMENTED_COLOR, "Downloaded") action_frame = tk.Frame(main_frame, bg=_WINDOW_BG) action_frame.pack(fill=tk.X, pady=10, padx=5) self.play_button = tk.Button(action_frame, text="PLAY VIDEO", command=self.play_video, state=tk.DISABLED, font=_GENERAL_FONT, bg=_BUTTON_FACE, fg=_TEXT_FG, relief=tk.RAISED, borderwidth=2) self.play_button.pack(side=tk.LEFT, expand=True, padx=5, pady=5) self.save_button = tk.Button(action_frame, text="SAVE VIDEO", command=self.save_video, state=tk.DISABLED, font=_GENERAL_FONT, bg=_BUTTON_FACE, fg=_TEXT_FG, relief=tk.RAISED, borderwidth=2) self.save_button.pack(side=tk.LEFT, expand=True, padx=5, pady=5) self.delete_button = tk.Button(action_frame, text="DELETE DOWNLOADED", command=self.delete_downloaded_video, state=tk.DISABLED, font=_GENERAL_FONT, bg=_BUTTON_FACE, fg=_TEXT_FG, relief=tk.RAISED, borderwidth=2) self.delete_button.pack(side=tk.LEFT, expand=True, padx=5, pady=5) log_frame = tk.LabelFrame(main_frame, text="Activity Log", bg=_WINDOW_BG, font=_GENERAL_FONT, relief=tk.GROOVE, borderwidth=2) log_frame.pack(fill=tk.BOTH, expand=True, pady=10, padx=5) self.log_text = tk.Text(log_frame, wrap=tk.WORD, state=tk.DISABLED, height=10, font=_MONOSPACE_FONT, bg=_LOG_BG, fg=_LOG_FG, relief=tk.SUNKEN, borderwidth=2, insertbackground=_LOG_FG) self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) log_scrollbar = tk.Scrollbar(log_frame, command=self.log_text.yview, relief=tk.FLAT, troughcolor=_WINDOW_BG, bg=_BUTTON_FACE) log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.log_text.config(yscrollcommand=log_scrollbar.set) self.log_text.tag_config("red", foreground="red") self.log_text.tag_config("green", foreground="green") self.log_text.tag_config("blue", foreground="blue") self.log_text.tag_config("orange", foreground="orange") self._toggle_cookie_input() def _on_canvas_resize(self, event): """ Callback function for the <Configure> event of the grid_canvas. Redraws the grid whenever the canvas is resized. """ self._draw_grid() self.small_progress_bar.coords(self.small_progress_fill, 0, 0, 0, 10) self.progress_label.config(text="0% completed") def _draw_grid(self): """ Clears the existing grid and redraws it to fit the current canvas size. Calculates block sizes dynamically to fill the canvas, ensuring no gaps. Blocks will be as close to square as possible given the canvas aspect ratio. """ self.grid_canvas.delete("all") self.grid_rects = [] canvas_width_actual = self.grid_canvas.winfo_width() canvas_height_actual = self.grid_canvas.winfo_height() if canvas_width_actual <= 0 or canvas_height_actual <= 0: self.root.update_idletasks() canvas_width_actual = self.grid_canvas.winfo_width() canvas_height_actual = self.grid_canvas.winfo_height() if canvas_width_actual <= 0 or canvas_height_actual <= 0: return block_width_float = canvas_width_actual / _GRID_COLS block_height_float = canvas_height_actual / _GRID_ROWS for r in range(_GRID_ROWS): for c in range(_GRID_COLS): x1 = int(c * block_width_float) y1 = int(r * block_height_float) x2 = int((c + 1) * block_width_float) if c == _GRID_COLS - 1: x2 = canvas_width_actual y2 = int((r + 1) * block_height_float) if r == _GRID_ROWS - 1: y2 = canvas_height_actual rect = self.grid_canvas.create_rectangle(x1, y1, x2, y2, fill=_NOT_DEFRAGMENTED_COLOR, outline="") self.grid_rects.append(rect) for r in range(_GRID_ROWS + 1): y = int(r * block_height_float) if r == _GRID_ROWS: y = canvas_height_actual self.grid_canvas.create_line(0, y, canvas_width_actual, y, fill=_DARK_GRAY, width=_GRID_LINE_THICKNESS) for c in range(_GRID_COLS + 1): x = int(c * block_width_float) if c == _GRID_COLS: x = canvas_width_actual self.grid_canvas.create_line(x, 0, x, canvas_height_actual, fill=_DARK_GRAY, width=_GRID_LINE_THICKNESS) def _open_url(self, url): """ Opens the given URL in the default web browser. """ try: webbrowser.open_new_tab(url) self._log(f"Opened URL: {url}", "blue") except Exception as e: self._log(f"Error opening URL {url}: {e}", "red") messagebox.showerror("Error", f"Failed to open URL: {e}") def _paste_url(self): """ Pastes the content from the clipboard into the URL entry field. """ try: clipboard_content = self.root.clipboard_get() self.url_entry.delete(0, tk.END) self.url_entry.insert(0, clipboard_content) self._log("Pasted URL from clipboard.", "blue") except tk.TclError: self._log("Clipboard is empty or inaccessible.", "orange") except Exception as e: self._log(f"An error occurred while pasting: {e}", "red") def start_process_single_link(self): """ Combines pasting the URL and starting the download process for a single link. This is the new command for the "START" button. """ self._paste_url() youtube_link = self.url_entry.get().strip() if youtube_link: self.download_queue = [youtube_link] self.current_download_index = -1 self._start_next_download_in_queue() else: messagebox.showwarning("Input Error", "Please enter a YouTube video link or paste from clipboard.") self._log("No URL to process for single download.", "red") def _open_batch_download_window(self): """ Opens a new Toplevel window to allow the user to input multiple YouTube links and configure batch-specific options. """ BatchDownloadWindow( self.root, self, self.play_after_download_var.get(), self.auto_save_after_download_var.get(), self.auto_delete_after_play_var.get() ) def start_batch_download(self, urls, play_after_download, auto_save, auto_delete): """ Sets the download queue and starts processing the first link. This method is called by the BatchDownloadWindow. It now also receives and applies the batch-specific checkbox settings. """ self.download_queue = [url.strip() for url in urls if url.strip()] self.current_download_index = -1 if not self.download_queue: messagebox.showwarning("Input Error", "No valid links provided for batch download.") self._log("No links in batch download queue.", "red") return self.play_after_download_var.set(play_after_download) self.auto_save_after_download_var.set(auto_save) self.auto_delete_after_play_var.set(auto_delete) self._log(f"Starting batch download for {len(self.download_queue)} videos...", "blue") self._log(f"Batch Options: Play after download: {play_after_download}, Auto-save: {auto_save}, Auto-delete: {auto_delete}", "blue") self.start_button.config(state=tk.DISABLED) self.batch_download_button.config(state=tk.DISABLED) self.play_button.config(state=tk.DISABLED) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) self._start_next_download_in_queue() def _start_next_download_in_queue(self): """ Processes the next video in the download queue. """ self.current_download_index += 1 if self.current_download_index < len(self.download_queue): youtube_link = self.download_queue[self.current_download_index] self.url_entry.delete(0, tk.END) self.url_entry.insert(0, youtube_link) self._log(f"\n--- Processing video {self.current_download_index + 1}/{len(self.download_queue)} ---", "blue") self._log(f"Downloading: {youtube_link}") self._draw_grid() self.small_progress_bar.coords(self.small_progress_fill, 0, 0, 0, 10) self.progress_label.config(text="0% completed") self.last_known_progress_percent = 0 download_thread = threading.Thread(target=self._download_video_thread, args=(youtube_link,)) download_thread.start() else: self._log("\n--- Batch download complete! ---", "green") if len(self.download_queue) > 1: messagebox.showinfo("Batch Download Complete", "All videos in the batch have been processed!") else: self._log("Video download and processing complete.", "green") self.start_button.config(state=tk.NORMAL) self.batch_download_button.config(state=tk.NORMAL) self.play_button.config(state=tk.DISABLED) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) self.download_queue = [] def _toggle_cookie_input(self): """ Enables or disables cookie input fields (browser menu, file entry, browse button) based on the selected cookie method radio button. """ method = self.cookie_method.get() if method == "browser": self.browser_menu.config(state=tk.NORMAL) for i in range(len(self.browser_options)): self.browser_menu["menu"].entryconfig(i, state=tk.NORMAL) self.cookie_file_entry.config(state=tk.DISABLED) self.browse_cookie_button.config(state=tk.DISABLED) elif method == "file": self.browser_menu.config(state=tk.DISABLED) for i in range(len(self.browser_options)): self.browser_menu["menu"].entryconfig(i, state=tk.DISABLED) self.cookie_file_entry.config(state=tk.NORMAL) self.browse_cookie_button.config(state=tk.NORMAL) else: # "none" selected self.browser_menu.config(state=tk.DISABLED) for i in range(len(self.browser_options)): self.browser_menu["menu"].entryconfig(i, state=tk.DISABLED) self.cookie_file_entry.config(state=tk.DISABLED) self.browse_cookie_button.config(state=tk.DISABLED) def _browse_cookie_file(self): """ Opens a file dialog for the user to select a cookies.txt file. Updates the cookie file entry field with the selected path. """ filepath = filedialog.askopenfilename( title="Select Cookies File", filetypes=[("Text files", "*.txt"), ("All files", "*.*")] ) if filepath: self.cookie_file_entry.delete(0, tk.END) self.cookie_file_entry.insert(0, filepath) def _log(self, message, color="black"): """ Appends a message to the activity log text area. Automatically scrolls to the end and applies a specified color. """ self.log_text.config(state=tk.NORMAL) self.log_text.insert(tk.END, message + "\n", color) self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) def _update_visual_progress(self, percent, status_message=""): """ Updates the progress bar, percentage label, and grid based on a given percentage. This function is called from the main thread. """ total_blocks = _GRID_ROWS * _GRID_COLS # Allow progress to go backward if yt-dlp reports it self.last_known_progress_percent = percent display_percent = self.last_known_progress_percent # Use the exact percentage self.progress_label.config(text=f"{display_percent:.0f}% completed{status_message}") canvas_width = self.small_progress_bar.winfo_width() if canvas_width <= 1: # Handle initial rendering or minimized window self.root.update_idletasks() canvas_width = self.small_progress_bar.winfo_width() if canvas_width <= 1: return fill_width = (display_percent / 100) * canvas_width self.small_progress_bar.coords(self.small_progress_fill, 0, 0, fill_width, 10) completed_blocks = int((display_percent / 100) * total_blocks) if not self.grid_rects: self._log("Warning: grid_rects is empty, cannot update progress visually.", "orange") return for i in range(total_blocks): if i < completed_blocks: self.grid_canvas.itemconfig(self.grid_rects[i], fill=_DEFRAGMENTED_COLOR) elif i == completed_blocks and display_percent < 100: # Only show 'in progress' if not yet 100% self.grid_canvas.itemconfig(self.grid_rects[i], fill=_IN_PROGRESS_COLOR) else: self.grid_canvas.itemconfig(self.grid_rects[i], fill=_NOT_DEFRAGMENTED_COLOR) self.root.update_idletasks() def _read_yt_dlp_output(self, pipe): """ Reads output from the yt-dlp subprocess pipe line by line, parses for progress, and updates the GUI. Also appends all lines to a buffer for later error checking (but no longer sets downloaded_video_path). Runs in a separate thread. """ # Regex to capture percentage from yt-dlp output lines progress_re = re.compile(r'\[download\]\s+(\d+\.?\d*)%') for line in iter(pipe.readline, ''): # Read strings from pipe directly line_str = line.strip() if not line_str: continue self.yt_dlp_output_buffer.append(line_str) # Always append to buffer # 1. Handle download percentage updates (visual only) match_progress = progress_re.search(line_str) if match_progress: try: percent = float(match_progress.group(1)) self.root.after(0, self._update_visual_progress, percent) except ValueError: self.root.after(0, self._log, f"Warning: Failed to parse percentage from: {line_str}", "orange") continue # Do not log percentage lines to activity log # 2. Filter out specific lines that should not be logged (debug, intermediate destination) # The final destination will be handled by the _download_video_thread after process completion if line_str.startswith('[debug]') or '[download] Destination:' in line_str: continue # Do not log debug lines or intermediate destination lines # 3. Log other important messages, applying color for errors/warnings if "[error]" in line_str.lower() or "http error 403" in line_str.lower(): self.root.after(0, self._log, line_str, "red") elif "[warning]" in line_str.lower(): self.root.after(0, self._log, line_str, "orange") else: self.root.after(0, self._log, line_str) time.sleep(0.01) pipe.close() # Close the pipe when done reading def _download_video_thread(self, youtube_link): """ The actual video download logic, executed in a separate thread. Uses the yt-dlp command line tool via subprocess. Implements a retry mechanism with different format strategies for 403 errors. """ self.downloaded_video_path = None self.suggested_video_title = None self.current_temp_dir = None # Reset for each new download youtube_regex = ( r'(https?://)?(www\.)?' r'(youtube|youtu|youtube-nocookie)\.(com|be)/' r'(watch\?v=|embed/|v/|.+\?v=|)' r'([a-zA-Z0-9_-]{11})' ) if not re.match(youtube_regex, youtube_link): self._log(f"Invalid URL for download: {youtube_link}. Skipping.", "red") self.root.after(0, lambda: messagebox.showwarning("Input Error", f"Invalid YouTube link: {youtube_link}. Skipping this video.")) self.root.after(0, self._start_next_download_in_queue) return download_successful = False final_error_message = "" # Use a temporary directory for downloads to ensure clean handling of files, # especially with dynamic naming and potential retries. try: temp_dir = os.path.join(os.getcwd(), "yt_dlp_temp_" + str(os.getpid())) os.makedirs(temp_dir, exist_ok=True) self.current_temp_dir = temp_dir # Store the temp directory self._log(f"Using temporary directory for download: {self.current_temp_dir}", "blue") for i, format_strategy in enumerate(FORMAT_STRATEGIES): self._log(f"Attempting download with format strategy {i+1}/{len(FORMAT_STRATEGIES)}: '{format_strategy}'", "blue") # Clear buffer for each attempt self.yt_dlp_output_buffer = [] self.downloaded_video_path = None # Reset path for each attempt # Construct the yt-dlp command using the dynamic output template command = [ sys.executable, '-m', 'yt_dlp', '--format', format_strategy, '--merge-output-format', 'mp4', '--output', os.path.join(self.current_temp_dir, OUTPUT_FILENAME_TEMPLATE), # Save to temp_dir '--no-warnings', '--verbose', youtube_link ] # Add cookie options cookie_method = self.cookie_method.get() if cookie_method == "browser": browser_name = self.browser_var.get() if browser_name: command.extend(['--cookies-from-browser', browser_name]) self._log(f"Using cookies from browser: {browser_name}", "blue") elif cookie_method == "file": cookie_file = self.cookie_file_entry.get().strip() expanded_cookie_file = os.path.expanduser(cookie_file) if cookie_file and os.path.exists(expanded_cookie_file): command.extend(['--cookie-file', expanded_cookie_file]) self._log(f"Using cookies from file: {expanded_cookie_file}", "blue") else: self._log("Warning: Cookie file not found or path invalid. Proceeding without cookies.", "orange") self.root.after(0, lambda: messagebox.showwarning("Cookie Error", "Cookie file not found or path invalid. Proceeding without cookies.")) self.root.after(0, lambda: self.cookie_method.set("none")) self.root.after(0, self._toggle_cookie_input) # Add SponsorBlock postprocessor command.extend(['--postprocessor-args', 'SponsorBlock:categories=sponsor']) try: self.yt_dlp_process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 ) output_reader_thread = threading.Thread(target=self._read_yt_dlp_output, args=(self.yt_dlp_process.stdout,)) output_reader_thread.daemon = True output_reader_thread.start() self.yt_dlp_process.wait() output_reader_thread.join() full_output_str = "\n".join(self.yt_dlp_output_buffer).lower() if self.yt_dlp_process.returncode == 0: self._log(f"yt-dlp command finished successfully for strategy '{format_strategy}'. Verifying downloaded file...", "blue") # --- ROBUST FILE VERIFICATION --- confirmed_file_path = None for attempt in range(20): # Try up to 20 times with 0.5s delay (max 10 seconds wait) # Look for any video/audio file in the temp directory found_files = glob.glob(os.path.join(self.current_temp_dir, "*")) for f in found_files: # Check for common video/audio extensions and a non-zero file size if os.path.isfile(f) and os.path.getsize(f) > 0 and (f.lower().endswith(('.mp4', '.webm', '.mkv', '.m4a', '.ogg', '.flv'))): confirmed_file_path = f break if confirmed_file_path: self._log(f"Confirmed downloaded file: {confirmed_file_path}", "green") break self._log(f"Attempt {attempt+1}/20: File not yet confirmed in temp directory. Waiting...", "orange") time.sleep(0.5) # Wait before retrying if confirmed_file_path: self.downloaded_video_path = confirmed_file_path download_successful = True break # Success, exit format strategy loop else: self._log(f"yt-dlp reported success but no valid file found in '{self.current_temp_dir}' after multiple checks. Trying next format.", "orange") final_error_message = "Download reported success but file not found or is empty after verification." # Continue to next format strategy else: if "http error 403: forbidden" in full_output_str: self._log(f"Download failed with 403 Forbidden for strategy '{format_strategy}'. Trying next format...", "orange") final_error_message = "Download failed due to 403 Forbidden. Trying other formats." else: final_error_message = f"Download process failed for strategy '{format_strategy}': yt-dlp exited with code {self.yt_dlp_process.returncode}. Check activity log for details." self._log(final_error_message, "red") break # Not a 403, so it's a different error, stop trying formats except FileNotFoundError: final_error_message = "'yt-dlp' command not found. Please ensure yt-dlp is installed and in your system's PATH." self._log(f"Error: {final_error_message}", "red") self.root.after(0, lambda: messagebox.showerror("Error", final_error_message)) break # Critical error, stop trying formats except Exception as e: final_error_message = f"An unexpected error occurred during download attempt: {e}" self._log(f"Error: {final_error_message}", "red") self.root.after(0, lambda: messagebox.showerror("Error", final_error_message)) break # Unexpected error, stop trying formats finally: self.yt_dlp_process = None # Clear process reference # Post-download processing based on success or final error if download_successful: self.root.after(0, self._update_visual_progress, 100.0, " - Finished") self.root.after(0, self._log, f"Video download process completed. Finalizing...", "green") # The downloaded_video_path should now be reliably set by the robust verification loop. if not self.downloaded_video_path or not os.path.exists(self.downloaded_video_path): error_msg = "Downloaded video file could not be found after successful yt-dlp execution and verification." self._log(f"Error: {error_msg}", "red") self.root.after(0, lambda: messagebox.showerror("File Not Found", error_msg)) self.root.after(0, self._start_next_download_in_queue) return # Exit this thread as file is not found # Extract suggested title from the final downloaded path base_name_with_id = os.path.basename(self.downloaded_video_path) # Remove the extension and the ID part (assuming -ID.ext) # This regex tries to remove the last hyphen followed by 11 alphanumeric chars and then the extension match_title_id_ext = re.match(r'(.*)-([a-zA-Z0-9_-]{11})\.(.+)', base_name_with_id) if match_title_id_ext: self.suggested_video_title = match_title_id_ext.group(1).strip() else: # Fallback if the ID pattern isn't found self.suggested_video_title = os.path.splitext(base_name_with_id)[0] # Basic sanitization for the title to be used in save dialog self.suggested_video_title = re.sub(r'[\\/:*?"<>|]', '_', self.suggested_video_title).strip() if not self.suggested_video_title: self.suggested_video_title = "youtube_video" self.root.after(0, self._log, f"Video '{self.suggested_video_title}' downloaded to: {self.downloaded_video_path}", "green") self.root.after(0, self._play_test_sound) self.root.after(0, self._initiate_post_download_flow) if self.current_download_index == len(self.download_queue) -1: self.root.after(0, lambda: self.play_button.config(state=tk.NORMAL)) self.root.after(0, lambda: self.save_button.config(state=tk.NORMAL)) self.root.after(0, lambda: self.delete_button.config(state=tk.NORMAL)) else: # If download was not successful after all attempts self.root.after(0, lambda: messagebox.showerror("Download Error", f"Failed to download video after multiple attempts: {final_error_message}")) self.root.after(0, self._start_next_download_in_queue) except Exception as e: self._log(f"An unhandled error occurred in download thread: {e}", "red") self.root.after(0, lambda: messagebox.showerror("Fatal Error", f"An unhandled error occurred: {e}")) self.root.after(0, self._start_next_download_in_queue) finally: self.yt_dlp_process = None # Clear process reference # Re-enable start button if not in batch mode if len(self.download_queue) <= 1: self.root.after(0, lambda: self.start_button.config(state=tk.NORMAL)) # IMPORTANT: The temporary directory is NOT removed here. # It will be removed by save_video or delete_downloaded_video, or cleanup_on_exit. def _play_test_sound(self): """ Generates a small test sound using espeak-ng and aplay, then plays it. This function is optional and will be skipped if the tools are not found. """ espeak_ng_ok = subprocess.run(["which", "espeak-ng"], capture_output=True).returncode == 0 aplay_ok = subprocess.run(["which", "aplay"], capture_output=True).returncode == 0 if not (espeak_ng_ok and aplay_ok): self._log("Skipping audio test: espeak-ng or aplay not found.", "orange") return self._log("Generating and playing audio test...", "blue") test_text = "Initiating video playback. Stand by." try: subprocess.run(["espeak-ng", "-w", TEST_SOUND_FILE, test_text], check=True, capture_output=True) subprocess.run(["aplay", TEST_SOUND_FILE], check=True, capture_output=True) self._log("Audio test played successfully.", "green") except (subprocess.CalledProcessError, FileNotFoundError) as e: self._log(f"Warning: Failed to generate or play test sound: {e}", "orange") finally: if os.path.exists(TEST_SOUND_FILE): os.remove(TEST_SOUND_FILE) def play_video(self): """ Plays the downloaded video using smplayer. It launches smplayer as a separate process. If auto-delete is enabled, it will delete the temporary file after playback. """ if not self.downloaded_video_path or not os.path.exists(self.downloaded_video_path): messagebox.showwarning("Playback Error", "No video downloaded or file not found to play.") self._log("No video to play.", "red") return self._log(f"Playing video: {self.downloaded_video_path}", "blue") try: self.smplayer_process = subprocess.Popen(["smplayer", self.downloaded_video_path]) self._log("smplayer launched. Check your desktop for the video player window.", "green") monitor_thread = threading.Thread(target=self._monitor_playback_and_cleanup) monitor_thread.daemon = True monitor_thread.start() except FileNotFoundError: messagebox.showerror("Playback Error", "smplayer not found. Please ensure it is installed and in your system's PATH.") self._log("smplayer not found. Cannot play video.", "red") self._start_next_download_in_queue() except Exception as e: messagebox.showerror("Playback Error", f"An error occurred during playback: {e}") self._log(f"Error during playback: {e}", "red") self._start_next_download_in_queue() def _monitor_playback_and_cleanup(self): """ Monitors the SMPlayer process. If auto-save or auto-delete is enabled, it will perform those actions after SMPlayer closes. This runs in a separate thread. """ if self.smplayer_process: self._log("Monitoring SMPlayer process for closure...", "blue") self.smplayer_process.wait() self.root.after(0, lambda: self._log("SMPlayer process closed.", "blue")) self.smplayer_process = None self.root.after(0, self._handle_post_playback_actions) else: self._log("No SMPlayer process to monitor.", "orange") def _initiate_post_download_flow(self): """ Determines the sequence of post-download actions (play, save, delete) based on checkbox states. This is called immediately after download. """ play_checked = self.play_after_download_var.get() save_checked = self.auto_save_after_download_var.get() delete_checked = self.auto_delete_after_play_var.get() if play_checked: self.play_video() else: if save_checked: self.save_video(auto_save=True) if delete_checked: if not play_checked and not save_checked: self.root.after(0, lambda: messagebox.showinfo("🤔 Question", "Are you okay❓ 😟 Why have you only download a video with the option to delete it after❓")) self.delete_downloaded_video(confirm=False) if not play_checked: self.root.after(0, self._start_next_download_in_queue) def _handle_post_playback_actions(self): """ Handles auto-save and auto-delete actions after video playback has finished. Called on the main thread. """ if self.auto_save_after_download_var.get(): self.save_video(auto_save=True) if self.auto_delete_after_play_var.get(): self.delete_downloaded_video(confirm=False) self.root.after(0, self._start_next_download_in_queue) def save_video(self, auto_save=False): """ Allows the user to save the downloaded video to a chosen location. Uses a file dialog for path selection and COPIES the file. The original temporary file remains in the temp directory. """ if not self.downloaded_video_path or not os.path.exists(self.downloaded_video_path): if auto_save: self._log("No video downloaded or file not found for auto-save.", "orange") else: messagebox.showwarning("Save Error", "No video downloaded or file not found to save.") return self._log("Initiating video save process...", "blue") original_ext = os.path.splitext(self.downloaded_video_path)[1] # Use the suggested_video_title (which is now the YouTube title) for the initial filename initial_filename = f"{self.suggested_video_title}{original_ext}" save_path = None # Determine initial directory for save dialog initial_dir = self.last_save_folder if os.path.isdir(self.last_save_folder) else os.path.expanduser("~") if auto_save: if self.last_save_folder and os.path.isdir(self.last_save_folder): save_path = os.path.join(self.last_save_folder, initial_filename) self._log(f"Attempting to auto-save (copy) to: {save_path}", "blue") try: shutil.copy2(self.downloaded_video_path, save_path) self._log(f"Video copied successfully to: {save_path}", "green") new_save_folder = os.path.dirname(save_path) self._save_last_save_folder(new_save_folder) except Exception as e: messagebox.showerror("Auto-Save Error", f"Failed to auto-save video: {e}") self._log(f"Error auto-saving video: {e}", "red") return # Auto-save is done, return. Manual save continues below. else: self._log("Auto-save failed: Last save folder not set or invalid. Opening manual save dialog.", "orange") # Fall through to manual save dialog if auto-save fails to find a valid folder # Manual save dialog save_path = filedialog.asksaveasfilename( initialdir=initial_dir, initialfile=initial_filename, defaultextension=original_ext, filetypes=[("Video files", "*.mp4 *.webm *.mkv"), ("All files", "*.*")] ) if save_path: try: target_dir = os.path.dirname(save_path) if not os.path.exists(target_dir): os.makedirs(target_dir) shutil.copy2(self.downloaded_video_path, save_path) # Always copy, not move self._log(f"Video saved successfully to: {save_path}", "green") # Do NOT set self.downloaded_video_path to None here, as the original temp file remains. # Do NOT call _delete_temp_file_and_folder here. Cleanup is separate. # Keep buttons enabled as the original temp file is still there self.play_button.config(state=tk.NORMAL) self.save_button.config(state=tk.NORMAL) self.delete_button.config(state=tk.NORMAL) new_save_folder = os.path.dirname(save_path) self._save_last_save_folder(new_save_folder) except Exception as e: messagebox.showerror("Save Error", f"Failed to save video: {e}") self._log(f"Error saving video: {e}", "red") else: self._log("Video save cancelled by user.", "blue") def delete_downloaded_video(self, confirm=True): """ Deletes the temporarily downloaded video file from the current directory. Asks for user confirmation before deletion, unless confirm=False. """ if not self.downloaded_video_path or not os.path.exists(self.downloaded_video_path): if not confirm: self._log("No video downloaded or file not found for auto-deletion.", "orange") else: messagebox.showwarning("Delete Error", "No video downloaded or file not found to delete.") return should_delete = True if confirm: should_delete = messagebox.askyesno("Confirm Deletion", f"Are you sure you want to delete '{os.path.basename(self.downloaded_video_path)}'?") if should_delete: self._delete_temp_file_and_folder(self.downloaded_video_path, self.current_temp_dir) self.downloaded_video_path = None self.suggested_video_title = None self.current_temp_dir = None # Ensure temp dir reference is cleared on successful deletion self.play_button.config(state=tk.DISABLED) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) elif confirm: self._log("Video deletion cancelled by user.", "blue") def _delete_temp_file_and_folder(self, file_path, temp_folder_path): """ Deletes the specified file and its parent temporary folder if it becomes empty. """ if file_path and os.path.exists(file_path): try: os.remove(file_path) self._log(f"Deleted temporary video file: {file_path}", "green") except Exception as e: self._log(f"Error deleting temporary video file {file_path}: {e}", "red") # After deleting the file, check if the temp_folder_path is empty and remove it if temp_folder_path and os.path.exists(temp_folder_path): try: # Check if the folder is empty before attempting to remove it if not os.listdir(temp_folder_path): os.rmdir(temp_folder_path) self._log(f"Removed empty temporary directory: {temp_folder_path}", "green") else: self._log(f"Temporary directory {temp_folder_path} is not empty, skipping removal.", "orange") except OSError as e: self._log(f"Error removing temporary directory {temp_folder_path}: {e}", "red") except Exception as e: self._log(f"An unexpected error occurred during temp directory cleanup: {e}", "red") def cleanup_on_exit(self): """ Performs final cleanup of any leftover temporary files (downloaded video, test sound file) and temporary directories when the application window is closed. """ self._log("Performing final cleanup...", "blue") # Clean up the current temporary video file and its directory if still present if self.downloaded_video_path and os.path.exists(self.downloaded_video_path): self._delete_temp_file_and_folder(self.downloaded_video_path, self.current_temp_dir) # Clean up any residual temporary sound file if os.path.exists(TEST_SOUND_FILE): try: os.remove(TEST_SOUND_FILE) self._log(f"Cleaned up temporary sound file: {TEST_SOUND_FILE}", "blue") except Exception as e: self._log(f"Warning: Could not clean up temporary sound file {TEST_SOUND_FILE}. Reason: {e}", "orange") # Clean up any other yt_dlp_temp_* directories that might have been left from crashes # This is a more general sweep. for item in os.listdir(os.getcwd()): if item.startswith("yt_dlp_temp_") and os.path.isdir(item): try: if not os.listdir(item): # Only remove if empty os.rmdir(item) self._log(f"Cleaned up residual empty temporary directory: {item}", "blue") else: self._log(f"Residual temporary directory {item} is not empty, skipping removal.", "orange") except OSError as e: self._log(f"Warning: Could not clean up residual temporary directory {item}. Reason: {e}", "orange") except Exception as e: self._log(f"An unexpected error occurred during residual temp directory cleanup: {e}", "red") self._log("Cleanup complete. Exiting application.", "blue") self.root.destroy() class BatchDownloadWindow(tk.Toplevel): def __init__(self, master, app_instance, current_play_state, current_save_state, current_delete_state): super().__init__(master) self.master = master self.app = app_instance self.title("Batch Download Links and Options") self.geometry("600x550") self.configure(bg=_WINDOW_BG) self.grab_set() self.batch_play_after_download_var = tk.BooleanVar(value=False) self.batch_auto_save_after_download_var = tk.BooleanVar(value=current_save_state) self.batch_auto_delete_after_play_var = tk.BooleanVar(value=current_delete_state) self._create_widgets() def _create_widgets(self): instruction_label = tk.Label(self, text="Enter YouTube video links below (one per line):", bg=_WINDOW_BG, fg=_TEXT_FG, font=_GENERAL_FONT) instruction_label.pack(pady=(10, 0), padx=10, anchor=tk.W) self.links_text = scrolledtext.ScrolledText(self, wrap=tk.WORD, width=70, height=15, bg=_TEXT_BG, fg=_TEXT_FG, relief=tk.SUNKEN, borderwidth=2, font=_MONOSPACE_FONT, insertbackground=_TEXT_FG) self.links_text.pack(pady=5, padx=10, fill=tk.BOTH, expand=True) options_frame = tk.LabelFrame(self, text="Batch Options", bg=_WINDOW_BG, font=_GENERAL_FONT, relief=tk.GROOVE, borderwidth=2) options_frame.pack(pady=10, padx=10, fill=tk.X) self.batch_play_checkbox = tk.Checkbutton( options_frame, text="Play video after download completes", variable=self.batch_play_after_download_var, bg=_WINDOW_BG, fg=_TEXT_FG, selectcolor=_WINDOW_BG, font=_GENERAL_FONT ) self.batch_play_checkbox.pack(anchor=tk.W, padx=10, pady=(5,0)) self.batch_auto_save_checkbox = tk.Checkbutton( options_frame, text="Auto-save video to last used folder after download (Required for batch)", variable=self.batch_auto_save_after_download_var, bg=_WINDOW_BG, fg=_TEXT_FG, selectcolor=_WINDOW_BG, font=_GENERAL_FONT ) self.batch_auto_save_checkbox.pack(anchor=tk.W, padx=10) self.batch_auto_delete_checkbox = tk.Checkbutton( options_frame, text="Auto-delete temporary video after playing (Required for batch)", variable=self.batch_auto_delete_after_play_var, bg=_WINDOW_BG, fg=_TEXT_FG, selectcolor=_WINDOW_BG, font=_GENERAL_FONT ) self.batch_auto_delete_checkbox.pack(anchor=tk.W, padx=10, pady=(0,5)) button_frame = tk.Frame(self, bg=_WINDOW_BG) button_frame.pack(pady=10, padx=10) start_button = tk.Button(button_frame, text="START BATCH DOWNLOAD", command=self._start_batch, font=_GENERAL_FONT, bg=_BUTTON_FACE, fg=_TEXT_FG, relief=tk.RAISED, borderwidth=2) start_button.pack(side=tk.LEFT, padx=5) cancel_button = tk.Button(button_frame, text="CANCEL", command=self.destroy, font=_GENERAL_FONT, bg=_BUTTON_FACE, fg=_TEXT_FG, relief=tk.RAISED, borderwidth=2) cancel_button.pack(side=tk.LEFT, padx=5) def _start_batch(self): raw_links = self.links_text.get("1.0", tk.END).strip() links = [link.strip() for link in raw_links.split('\n') if link.strip()] if not links: messagebox.showwarning("No Links", "Please enter at least one YouTube link.") return if not (self.batch_auto_save_after_download_var.get() or self.batch_auto_delete_after_play_var.get()): messagebox.showerror("Batch Options Required", "For batch downloads, you must select either 'Auto-save video' or 'Auto-delete temporary video', or both, to prevent issues.") return if self.batch_auto_delete_after_play_var.get() and not self.batch_play_after_download_var.get(): messagebox.showerror("Option Conflict", "If 'Auto-delete temporary video after playing' is selected, you must also select 'Play video after download completes'.") return if self.batch_play_after_download_var.get() and \ not (self.batch_auto_save_after_download_var.get() or self.batch_auto_delete_after_play_var.get()): messagebox.showerror("Option Conflict", "If 'Play video after download completes' is selected for a batch, you must also select either 'Auto-save video' or 'Auto-delete temporary video'.") return self.destroy() self.app.start_batch_download( links, self.batch_play_after_download_var.get(), self.batch_auto_save_after_download_var.get(), self.batch_auto_delete_after_play_var.get() ) if __name__ == "__main__": root = tk.Tk() app = YouTubeDownloaderApp(root) root.protocol("WM_DELETE_WINDOW", app.cleanup_on_exit) root.mainloop()
August 21, 2025 at 1:58 pm #8191thumbtak
ModeratorMy app is having issues and I can’t get it working at this time. I found an app that should work.
$ flatpak install flathub io.github.aandrew_me.ytdn $ flatpak install flathub com.github.tchx84.Flatseal $ flatpak run com.github.tchx84.Flatseal
Turn on this setting.
In the settings of ytDownloader change the download folder. You might get a warning in the app, but if you can set the folder, you are fine. -
AuthorPosts
- You must be logged in to reply to this topic.