loader image

How to Stop Ad Blocker Detection on Websites Like YouTube

What makes us different from other similar websites? Forums Tech How to Stop Ad Blocker Detection on Websites Like YouTube

Viewing 2 posts - 16 through 17 (of 17 total)
  • Author
    Posts
  • #8154
    thumbtak
    Moderator

    Batch 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()
    #8191
    thumbtak
    Moderator

    My 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.

Viewing 2 posts - 16 through 17 (of 17 total)
  • You must be logged in to reply to this topic.
TAKs Shack