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 12 posts - 16 through 27 (of 27 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.

    #8206
    thumbtak
    Moderator

    Here is an updated and working version of the app. Save it as yt-dlp.sh and run it with bash yt-dlp.sh.

    #!/bin/bash
    
    # Function to type text character by character for a cool visual effect
    type_text() {
    text="$1"
    for ((i=0; i<${#text}; i++)); do
    echo -n "${text:$i:1}"
    sleep 0.05 # Adjust this value to change the typing speed
    done
    echo "" # Add a newline at the end
    }
    
    # --- yt-dlp Update Check ---
    
    update_yt_dlp() {
    if command -v "yt-dlp" &> /dev/null; then
    read -p "A new version of yt-dlp may be available. Do you want to check for updates? (y/n) " update_choice
    if [[ "$update_choice" =~ ^[Yy]$ ]]; then
    echo "Checking for and installing updates..."
    # Check if yt-dlp was installed via pip
    if command -v "pip" &> /dev/null && pip freeze | grep "yt-dlp" &> /dev/null; then
    pip install -U yt-dlp
    else
    # Fallback to the self-update command
    yt-dlp -U
    fi
    
    if [ $? -eq 0 ]; then
    echo "yt-dlp updated successfully!"
    else
    echo "Failed to update yt-dlp."
    fi
    fi
    fi
    }
    
    # --- Dependency Checks with Installation Prompts ---
    
    # Function to safely install a tool
    install_tool() {
    local tool_name="$1"
    local install_cmd="$2"
    local snap_install="$3"
    
    # First, check if the tool is already installed
    if command -v "$tool_name" &> /dev/null; then
    echo "'$tool_name' is already installed."
    return 0
    fi
    
    # If not, prompt the user for installation
    echo "The '$tool_name' tool is required for this script."
    read -p "Do you want to install it now? (y/n) " install_choice
    
    if [[ "$install_choice" =~ ^[Yy]$ ]]; then
    echo "Installing $tool_name..."
    if [ -n "$snap_install" ]; then
    sudo snap install "$snap_install"
    else
    sudo $install_cmd
    fi
    
    # Check if the installation was successful
    if [ $? -eq 0 ]; then
    echo "'$tool_name' installed successfully!"
    # Add a small delay and re-check to ensure the shell updates
    sleep 1
    if ! command -v "$tool_name" &> /dev/null; then
    echo "Warning: '$tool_name' was installed but not found in PATH. Please open a new terminal or run 'source ~/.bashrc'."
    return 1
    fi
    return 0
    else
    echo "Failed to install '$tool_name'. Please install it manually."
    return 1
    fi
    else
    echo "Skipping '$tool_name' installation. Some features may not work."
    return 1
    fi
    }
    
    # --- Main Script Logic and Dependency Management ---
    
    # Run update check first
    update_yt_dlp
    
    echo "Checking for required dependencies..."
    if ! command -v "snap" &> /dev/null; then
    install_tool "snapd" "apt install snapd"
    else
    echo "'snapd' is already installed."
    fi
    install_tool "figlet" "apt install figlet"
    install_tool "lolcat" "" "lolcat"
    install_tool "yt-dlp" "apt install yt-dlp"
    install_tool "smplayer" "apt-get install -y smplayer" "smplayer"
    
    # At this point, all required tools should be installed or the user declined.
    for cmd in yt-dlp figlet lolcat smplayer; do
    if ! command -v "$cmd" &> /dev/null; then
    type_text "Error: '$cmd' is not installed. Exiting."
    exit 1
    fi
    done
    
    # --- Script Configuration ---
    SEPARATOR="---------------------------------------------------"
    echo "$SEPARATOR" | lolcat
    figlet "YTDLP" | lolcat
    echo "$SEPARATOR" | lolcat
    
    # Define a configuration file to save settings
    CONFIG_FILE=".yt-dlp_config"
    
    # --- New Function: Download and Play from a Playlist File ---
    
    download_and_play_playlist() {
    read -p "Enter the name of the text file with the links: " file_name
    
    if [ ! -f "$file_name" ]; then
    echo "Error: File '$file_name' not found."
    return
    fi
    
    # Create the playlist folder if it doesn't exist
    mkdir -p playlist
    
    echo "Starting download and play session from '$file_name'..."
    
    # Read each URL and process it one by one
    while IFS= read -r url; do
    if [ -n "$url" ]; then
    echo "$SEPARATOR" | lolcat
    echo "Processing video from URL: $url"
    
    # Use yt-dlp to download the video
    yt-dlp "$url" -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' -o "playlist/%(title)s.%(ext)s"
    
    if [ $? -eq 0 ]; then
    echo "Download successful."
    
    # Find the downloaded file
    video_file=$(find playlist -name "*.*" -printf '%T@ %p\n' | sort -n | tail -1 | cut -d' ' -f2-)
    
    if [ -n "$video_file" ]; then
    echo "Playing: $(basename "$video_file")"
    # Use smplayer to play the video and then exit when it's done.
    smplayer -close-after-media-ended "$video_file"
    
    # After playing, prompt for deletion and removal
    read -p "Finished playing. Do you want to delete this video and remove the link from the file? (y/n) " delete_choice
    if [[ "$delete_choice" =~ ^[Yy]$ ]]; then
    rm "$video_file"
    sed -i.bak "/$url/d" "$file_name"
    echo "Deleted video and removed link from $file_name."
    else
    echo "Skipped deletion and link removal."
    fi
    else
    echo "Could not find the downloaded video file."
    fi
    else
    echo "Download failed for URL: $url"
    fi
    fi
    done < "$file_name"
    
    echo "$SEPARATOR" | lolcat
    echo "Playlist session complete."
    }
    
    # --- Function to add a URL to a playlist file without downloading ---
    add_url_to_playlist() {
    read -p "Please paste the YouTube URL you want to add to a playlist: " url
    if [[ -z "$url" ]]; then
    type_text "No URL provided. Exiting."
    return
    fi
    
    LAST_PLAYLIST_FILE="playlist.txt"
    if [ -f "$CONFIG_FILE" ]; then
    LAST_PLAYLIST_FILE=$(grep "^LAST_PLAYLIST_FILE=" "$CONFIG_FILE" | cut -d'=' -f2-)
    if [ -z "$LAST_PLAYLIST_FILE" ]; then
    LAST_PLAYLIST_FILE="playlist.txt"
    fi
    fi
    
    read -p "Enter the name of the playlist file to add the link to (default: $LAST_PLAYLIST_FILE): " playlist_file_input
    
    if [ -z "$playlist_file_input" ]; then
    playlist_file="$LAST_PLAYLIST_FILE"
    else
    playlist_file="$playlist_file_input"
    fi
    
    echo "$url" >> "$playlist_file"
    type_text "URL added to $playlist_file"
    
    echo "LAST_PLAYLIST_FILE=$playlist_file" > "$CONFIG_FILE"
    }
    
    # --- Main Script Logic ---
    
    type_text "Welcome to the yt-dlp interactive downloader!"
    echo "https://taksshack.com"
    echo ""
    
    echo "Please choose an option:"
    echo " [a] Add a single video to a playlist file (after downloading)"
    echo " [p] Run and play a list of videos from a file"
    echo " [s] Download a single video and ask to play/save it"
    echo " [d] Add a single URL to a playlist file (without downloading)"
    read -p "Your choice (a/p/s/d): " main_choice
    
    if [[ "$main_choice" =~ ^[Pp]$ ]]; then
    download_and_play_playlist
    elif [[ "$main_choice" =~ ^[Aa]$ ]]; then
    read -p "Please paste the YouTube URL you want to add: " url
    
    original_filename=$(yt-dlp --get-filename -o '%(title)s.%(ext)s' "$url")
    
    read -p "Do you want to use a browser for authentication? (y/n) " use_cookies_choice
    YTDLP_CMD="yt-dlp \"$url\""
    if [[ "$use_cookies_choice" =~ ^[Yy]$ ]]; then
    echo "Select a browser:"
    options=("Chrome" "Firefox" "Brave" "Edge")
    select selected_browser in "${options[@]}"; do
    if [[ -n "$selected_browser" ]]; then
    break
    else
    echo "Invalid selection. Please choose a number from the list."
    fi
    done
    
    read -p "Enter profile name (e.g., 'Default') or leave blank: " profile_name
    
    if [[ -n "$profile_name" ]]; then
    YTDLP_CMD="$YTDLP_CMD --cookies-from-browser \"${selected_browser,,}:$profile_name\""
    else
    YTDLP_CMD="$YTDLP_CMD --cookies-from-browser \"${selected_browser,,}\""
    fi
    fi
    
    YTDLP_CMD="$YTDLP_CMD -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' -o 'playlist.mp4'"
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Your final download command is ready:"
    echo "$YTDLP_CMD"
    echo "$SEPARATOR" | lolcat
    echo ""
    
    read -p "Ready to download and add to playlist? (y/n) " execute_choice
    if [[ "$execute_choice" =~ ^[Yy]$ ]]; then
    echo "Starting download..."
    eval "$YTDLP_CMD"
    
    if [[ $? -eq 0 ]]; then
    echo "Download finished!"
    
    smplayer "playlist.mp4"
    
    read -p "Finished playing. Do you want to delete the video? (y/n) " delete_video_choice
    if [[ "$delete_video_choice" =~ ^[Yy]$ ]]; then
    rm "playlist.mp4"
    echo "Deleted playlist.mp4"
    else
    mv "playlist.mp4" "$original_filename"
    echo "Video renamed to '$original_filename'"
    fi
    
    LAST_PLAYLIST_FILE="playlist.txt"
    if [ -f "$CONFIG_FILE" ]; then
    LAST_PLAYLIST_FILE=$(grep "^LAST_PLAYLIST_FILE=" "$CONFIG_FILE" | cut -d'=' -f2-)
    if [ -z "$LAST_PLAYLIST_FILE" ]; then
    LAST_PLAYLIST_FILE="playlist.txt"
    fi
    fi
    
    read -p "Enter the name of the playlist file to add the link to (default: $LAST_PLAYLIST_FILE): " playlist_file_input
    
    if [ -z "$playlist_file_input" ]; then
    playlist_file="$LAST_PLAYLIST_FILE"
    else
    playlist_file="$playlist_file_input"
    fi
    
    echo "$url" >> "$playlist_file"
    echo "URL added to $playlist_file"
    
    echo "LAST_PLAYLIST_FILE=$playlist_file" > "$CONFIG_FILE"
    else
    echo "An error occurred during the download. The video will not be played or added to a playlist."
    fi
    
    echo "$SEPARATOR" | lolcat
    else
    echo "Download cancelled."
    fi
    
    elif [[ "$main_choice" =~ ^[Dd]$ ]]; then
    add_url_to_playlist
    else
    read -p "Please paste the YouTube URL you want to download: " url
    
    original_filename=$(yt-dlp --get-filename -o '%(title)s.%(ext)s' "$url")
    temp_filename="taksshack.com.mp4"
    
    read -p "Do you want to use a browser for authentication? (y/n) " use_cookies_choice
    YTDLP_CMD="yt-dlp \"$url\""
    if [[ "$use_cookies_choice" =~ ^[Yy]$ ]]; then
    echo "Select a browser:"
    options=("Chrome" "Firefox" "Brave" "Edge")
    select selected_browser in "${options[@]}"; do
    if [[ -n "$selected_browser" ]]; then
    break
    else
    echo "Invalid selection. Please choose a number from the list."
    fi
    done
    
    read -p "Enter profile name (e.g., 'Default') or leave blank: " profile_name
    
    if [[ -n "$profile_name" ]]; then
    YTDLP_CMD="$YTDLP_CMD --cookies-from-browser \"${selected_browser,,}:$profile_name\""
    else
    YTDLP_CMD="$YTDLP_CMD --cookies-from-browser \"${selected_browser,,}\""
    fi
    fi
    
    read -p "Do you want to download just the audio? (y/n) " download_audio_choice
    if [[ "$download_audio_choice" =~ ^[Yy]$ ]]; then
    YTDLP_CMD="$YTDLP_CMD -x --audio-format mp3 -o '$temp_filename'"
    echo "Got it! We'll download the audio in MP3 format."
    else
    YTDLP_CMD="$YTDLP_CMD -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' -o '$temp_filename'"
    echo "Downloading the best quality video (MP4 preferred)."
    fi
    
    read -p "Any other yt-dlp options (e.g., --verbose)? " extra_options
    if [[ "$extra_options" != "n" && -n "$extra_options" ]]; then
    YTDLP_CMD="$YTDLP_CMD $extra_options"
    fi
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Your final command is ready:"
    echo "$YTDLP_CMD"
    echo "$SEPARATOR" | lolcat
    echo ""
    
    read -p "Ready to download? (y/n) " execute_choice
    if [[ "$execute_choice" =~ ^[Yy]$ ]]; then
    echo "Starting download..."
    eval "$YTDLP_CMD"
    
    if [[ $? -eq 0 ]]; then
    echo "Download finished!"
    
    read -p "Do you want to play the downloaded video? (y/n) " play_choice
    if [[ "$play_choice" =~ ^[Yy]$ ]]; then
    smplayer "$temp_filename"
    fi
    
    read -p "Do you want to save the video as '$original_filename'? (y/n) " save_choice
    if [[ "$save_choice" =~ ^[Yy]$ ]]; then
    mv "$temp_filename" "$original_filename"
    echo "Video saved as '$original_filename'"
    else
    rm "$temp_filename"
    echo "Video deleted."
    fi
    else
    echo "An error occurred during the download. The video was not processed."
    fi
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Operation complete."
    echo "$SEPARATOR" | lolcat
    else
    echo "Download cancelled."
    fi
    fi
    #8214
    thumbtak
    Moderator

    Script update to fix an issue with it sometimes not downloading videos:

    #!/bin/bash
    
    # Function to type text character by character for a cool visual effect
    type_text() {
    text="$1"
    for ((i=0; i<${#text}; i++)); do echo -n "${text:$i:1}" sleep 0.05 # Adjust this value to change the typing speed done echo "" # Add a newline at the end } # ---------------------------------------------------------------------- # --- yt-dlp Update Check --- # ---------------------------------------------------------------------- update_yt_dlp() { if command -v "yt-dlp" &> /dev/null; then
    read -p "A new version of yt-dlp may be available. Do you want to check for updates? (y/n) " update_choice
    if [[ "$update_choice" =~ ^[Yy]$ ]]; then
    echo "Checking for and installing updates..."
    # Check if yt-dlp was installed via pip
    if command -v "pip" &> /dev/null && pip freeze | grep "yt-dlp" &> /dev/null; then
    pip install -U yt-dlp
    else
    # Fallback to the self-update command
    yt-dlp -U
    fi
    
    if [ $? -eq 0 ]; then
    echo "yt-dlp updated successfully!"
    else
    echo "Failed to update yt-dlp."
    fi
    fi
    fi
    }
    
    # ----------------------------------------------------------------------
    # --- Dependency Checks with Installation Prompts ---
    # ----------------------------------------------------------------------
    
    # Function to safely install a tool
    install_tool() {
    local tool_name="$1"
    local install_cmd="$2"
    local snap_install="$3"
    
    # First, check if the tool is already installed
    if command -v "$tool_name" &> /dev/null; then
    echo "'$tool_name' is already installed."
    return 0
    fi
    
    # If not, prompt the user for installation
    echo "The '$tool_name' tool is required for this script."
    read -p "Do you want to install it now? (y/n) " install_choice
    
    if [[ "$install_choice" =~ ^[Yy]$ ]]; then
    echo "Installing $tool_name..."
    if [ -n "$snap_install" ]; then
    sudo snap install "$snap_install"
    else
    sudo $install_cmd
    fi
    
    # Check if the installation was successful
    if [ $? -eq 0 ]; then
    echo "'$tool_name' installed successfully!"
    # Add a small delay and re-check to ensure the shell updates
    sleep 1
    if ! command -v "$tool_name" &> /dev/null; then
    echo "Warning: '$tool_name' was installed but not found in PATH. Please open a new terminal or run 'source ~/.bashrc'."
    return 1
    fi
    return 0
    else
    echo "Failed to install '$tool_name'. Please install it manually."
    return 1
    fi
    else
    echo "Skipping '$tool_name' installation. Some features may not work."
    return 1
    fi
    }
    
    # ----------------------------------------------------------------------
    # --- yt-dlp Command Builder Function (FIXED) ---
    # FIX: Removed --retry-all-fragments which caused the "no such option" error.
    # ----------------------------------------------------------------------
    build_yt_dlp_command() {
    local url="$1"
    local output_template="$2"
    local use_cookies="$3" # 'y' or 'n'
    local download_audio="$4" # 'y' or 'n'
    local extra_options="$5"
    
    local YTDLP_CMD="yt-dlp \"$url\""
    
    if [[ "$use_cookies" =~ ^[Yy]$ ]]; then
    echo "Select a browser for cookies:" >&2 # Output to stderr
    local options=("Chrome" "Firefox" "Brave" "Edge")
    select selected_browser in "${options[@]}"; do
    if [[ -n "$selected_browser" ]]; then
    break
    else
    echo "Invalid selection. Please choose a number from the list." >&2 # Output to stderr
    fi
    done
    
    read -p "Enter profile name (e.g., 'Default') or leave blank: " profile_name
    
    if [[ -n "$profile_name" ]]; then
    YTDLP_CMD="$YTDLP_CMD --cookies-from-browser \"${selected_browser,,}:$profile_name\""
    else
    YTDLP_CMD="$YTDLP_CMD --cookies-from-browser \"${selected_browser,,}\""
    fi
    fi
    
    # ALWAYS add general robustness flags. Removed --retry-all-fragments.
    YTDLP_CMD="$YTDLP_CMD --no-check-certificate"
    
    if [[ "$download_audio" =~ ^[Yy]$ ]]; then
    YTDLP_CMD="$YTDLP_CMD -x --audio-format mp3 -o '$output_template'"
    echo "Got it! We'll download the audio in MP3 format." >&2 # Output to stderr
    else
    # Default to best video/audio combination
    YTDLP_CMD="$YTDLP_CMD -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' -o '$output_template'"
    echo "Downloading the best quality video (MP4 preferred)." >&2 # Output to stderr
    fi
    
    if [[ -n "$extra_options" ]]; then
    YTDLP_CMD="$YTDLP_CMD $extra_options"
    fi
    
    echo "$YTDLP_CMD" # Only this line is printed to stdout and captured by $()
    }
    
    # ----------------------------------------------------------------------
    # --- Main Script Logic and Dependency Management ---
    # ----------------------------------------------------------------------
    
    # Run update check first
    update_yt_dlp
    
    echo "Checking for required dependencies..."
    if ! command -v "snap" &> /dev/null; then
    install_tool "snapd" "apt install snapd"
    else
    echo "'snapd' is already installed."
    fi
    install_tool "figlet" "apt install figlet"
    install_tool "lolcat" "" "lolcat"
    install_tool "yt-dlp" "apt install yt-dlp"
    install_tool "smplayer" "apt-get install -y smplayer" "smplayer"
    
    # At this point, all required tools should be installed or the user declined.
    for cmd in yt-dlp figlet lolcat smplayer; do
    if ! command -v "$cmd" &> /dev/null; then
    type_text "Error: '$cmd' is not installed. Exiting."
    exit 1
    fi
    done
    
    # --- Script Configuration ---
    SEPARATOR="---------------------------------------------------"
    echo "$SEPARATOR" | lolcat
    figlet "YTDLP" | lolcat
    echo "$SEPARATOR" | lolcat
    
    # Define a configuration file to save settings
    CONFIG_FILE=".yt-dlp_config"
    
    # ----------------------------------------------------------------------
    # --- Function: Download and Play from a Playlist File ---
    # ----------------------------------------------------------------------
    
    download_and_play_playlist() {
    read -p "Enter the name of the text file with the links: " file_name
    
    if [ ! -f "$file_name" ]; then
    echo "Error: File '$file_name' not found."
    return
    fi
    
    # Check for cookies ONCE
    read -p "Do these videos require browser authentication (cookies)? (y/n) " use_cookies_choice
    
    # Create the playlist folder if it doesn't exist
    mkdir -p playlist
    
    echo "Starting download and play session from '$file_name'..."
    
    # Read each URL and process it one by one
    while IFS= read -r url; do
    if [ -n "$url" ]; then
    echo "$SEPARATOR" | lolcat
    echo "Processing video from URL: $url"
    
    # Use the new builder function for consistency
    YTDLP_CMD=$(build_yt_dlp_command "$url" "playlist/%(title)s.%(ext)s" "$use_cookies_choice" "n" "")
    
    echo "Executing: $YTDLP_CMD"
    eval "$YTDLP_CMD"
    
    if [ $? -eq 0 ]; then
    echo "Download successful."
    
    # Find the downloaded file (improved find logic)
    # Find the most recently modified file in the playlist directory
    video_file=$(find playlist -type f -mtime -1m -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1 | cut -d' ' -f2-)
    
    if [ -n "$video_file" ] && [ -f "$video_file" ]; then
    echo "Playing: $(basename "$video_file")"
    # Use smplayer to play the video and then exit when it's done.
    smplayer -close-after-media-ended "$video_file"
    
    # After playing, prompt for deletion and removal
    read -p "Finished playing. Do you want to delete this video and remove the link from the file? (y/n) " delete_choice
    if [[ "$delete_choice" =~ ^[Yy]$ ]]; then
    rm "$video_file"
    # Use sed to remove the URL only if it's the exact line (safer regex)
    sed -i.bak "\@^$url$@d" "$file_name"
    echo "Deleted video and removed link from $file_name."
    else
    echo "Skipped deletion and link removal."
    fi
    else
    echo "Could not reliably find the downloaded video file."
    fi
    else
    echo "Download failed for URL: $url"
    fi
    fi
    done < "$file_name" echo "$SEPARATOR" | lolcat echo "Playlist session complete." } # ---------------------------------------------------------------------- # --- Function to add a URL to a playlist file without downloading --- # ---------------------------------------------------------------------- add_url_to_playlist() { read -p "Please paste the YouTube URL you want to add to a playlist: " url if [[ -z "$url" ]]; then type_text "No URL provided. Exiting." return fi LAST_PLAYLIST_FILE="playlist.txt" if [ -f "$CONFIG_FILE" ]; then LAST_PLAYLIST_FILE=$(grep "^LAST_PLAYLIST_FILE=" "$CONFIG_FILE" | cut -d'=' -f2-) if [ -z "$LAST_PLAYLIST_FILE" ]; then LAST_PLAYLIST_FILE="playlist.txt" fi fi read -p "Enter the name of the playlist file to add the link to (default: $LAST_PLAYLIST_FILE): " playlist_file_input if [ -z "$playlist_file_input" ]; then playlist_file="$LAST_PLAYLIST_FILE" else playlist_file="$playlist_file_input" fi echo "$url" >> "$playlist_file"
    type_text "URL added to $playlist_file"
    
    echo "LAST_PLAYLIST_FILE=$playlist_file" > "$CONFIG_FILE"
    }
    
    # ----------------------------------------------------------------------
    # --- Main Script Execution ---
    # ----------------------------------------------------------------------
    
    type_text "Welcome to the yt-dlp interactive downloader!"
    echo "https://taksshack.com"
    echo ""
    
    echo "Please choose an option:"
    echo " [a] Add a single video to a playlist file (after downloading)"
    echo " [p] Run and play a list of videos from a file"
    echo " [s] Download a single video and ask to play/save it"
    echo " [d] Add a single URL to a playlist file (without downloading)"
    read -p "Your choice (a/p/s/d): " main_choice
    
    if [[ "$main_choice" =~ ^[Pp]$ ]]; then
    download_and_play_playlist
    elif [[ "$main_choice" =~ ^[Aa]$ ]]; then
    read -p "Please paste the YouTube URL you want to add: " url
    
    # Get original filename early
    original_filename=$(yt-dlp --get-filename -o '%(title)s.%(ext)s' "$url")
    temp_output_file="playlist.mp4"
    
    read -p "Do you want to use a browser for authentication? (y/n) " use_cookies_choice
    
    # Build the command using the new function
    YTDLP_CMD=$(build_yt_dlp_command "$url" "$temp_output_file" "$use_cookies_choice" "n" "")
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Your final download command is ready:"
    echo "$YTDLP_CMD"
    echo "$SEPARATOR" | lolcat
    echo ""
    
    read -p "Ready to download and add to playlist? (y/n) " execute_choice
    if [[ "$execute_choice" =~ ^[Yy]$ ]]; then
    echo "Starting download..."
    eval "$YTDLP_CMD"
    
    if [[ $? -eq 0 ]]; then
    echo "Download finished!"
    
    smplayer "$temp_output_file"
    
    read -p "Finished playing. Do you want to delete the video? (y/n) " delete_video_choice
    if [[ "$delete_video_choice" =~ ^[Yy]$ ]]; then
    rm "$temp_output_file"
    echo "Deleted $temp_output_file"
    else
    mv "$temp_output_file" "$original_filename"
    echo "Video renamed to '$original_filename'"
    fi
    
    LAST_PLAYLIST_FILE="playlist.txt"
    if [ -f "$CONFIG_FILE" ]; then
    LAST_PLAYLIST_FILE=$(grep "^LAST_PLAYLIST_FILE=" "$CONFIG_FILE" | cut -d'=' -f2-)
    if [ -z "$LAST_PLAYLIST_FILE" ]; then
    LAST_PLAYLIST_FILE="playlist.txt"
    fi
    fi
    
    read -p "Enter the name of the playlist file to add the link to (default: $LAST_PLAYLIST_FILE): " playlist_file_input
    
    if [ -z "$playlist_file_input" ]; then
    playlist_file="$LAST_PLAYLIST_FILE"
    else
    playlist_file="$playlist_file_input"
    fi
    
    echo "$url" >> "$playlist_file"
    echo "URL added to $playlist_file"
    
    echo "LAST_PLAYLIST_FILE=$playlist_file" > "$CONFIG_FILE"
    else
    echo "An error occurred during the download. The video will not be played or added to a playlist."
    fi
    
    echo "$SEPARATOR" | lolcat
    else
    echo "Download cancelled."
    fi
    
    elif [[ "$main_choice" =~ ^[Dd]$ ]]; then
    add_url_to_playlist
    elif [[ "$main_choice" =~ ^[Ss]$ ]]; then
    read -p "Please paste the YouTube URL you want to download: " url
    
    # Get original filename early
    original_filename=$(yt-dlp --get-filename -o '%(title)s.%(ext)s' "$url")
    temp_filename="taksshack.com.mp4" # Use a consistent temp filename
    
    read -p "Do you want to use a browser for authentication? (y/n) " use_cookies_choice
    
    read -p "Do you want to download just the audio? (y/n) " download_audio_choice
    
    read -p "Any other yt-dlp options (e.g., --verbose)? " extra_options
    if [[ "$extra_options" == "n" ]]; then
    extra_options=""
    fi
    
    # Build the command using the new function
    YTDLP_CMD=$(build_yt_dlp_command "$url" "$temp_filename" "$use_cookies_choice" "$download_audio_choice" "$extra_options")
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Your final command is ready:"
    echo "$YTDLP_CMD"
    echo "$SEPARATOR" | lolcat
    echo ""
    
    read -p "Ready to download? (y/n) " execute_choice
    if [[ "$execute_choice" =~ ^[Yy]$ ]]; then
    echo "Starting download..."
    eval "$YTDLP_CMD"
    
    if [[ $? -eq 0 ]]; then
    echo "Download finished!"
    
    read -p "Do you want to play the downloaded media? (y/n) " play_choice
    if [[ "$play_choice" =~ ^[Yy]$ ]]; then
    smplayer "$temp_filename"
    fi
    
    # Check the actual extension of the downloaded file
    if [[ "$download_audio_choice" =~ ^[Yy]$ ]]; then
    original_filename="${original_filename%.*}.mp3" # Ensure .mp3 extension if audio was downloaded
    fi
    
    read -p "Do you want to save the file as '$original_filename'? (y/n) " save_choice
    if [[ "$save_choice" =~ ^[Yy]$ ]]; then
    mv "$temp_filename" "$original_filename"
    echo "File saved as '$original_filename'"
    else
    rm "$temp_filename"
    echo "File deleted."
    fi
    else
    echo "An error occurred during the download. The file was not processed."
    fi
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Operation complete."
    echo "$SEPARATOR" | lolcat
    else
    echo "Download cancelled."
    fi
    fi
    #8215
    thumbtak
    Moderator

    Updated code that fixes the error “ERROR: unable to download video data: HTTP Error 403: Forbidden“:

    #!/bin/bash
    
    # Function to type text character by character for a cool visual effect
    type_text() {
    text="$1"
    for ((i=0; i<${#text}; i++)); do
    echo -n "${text:$i:1}"
    sleep 0.05 # Adjust this value to change the typing speed
    done
    echo "" # Add a newline at the end
    }
    
    # ----------------------------------------------------------------------
    # --- yt-dlp Update Check ---
    # ----------------------------------------------------------------------
    
    update_yt_dlp() {
    if command -v "yt-dlp" &> /dev/null; then
    read -p "A new version of yt-dlp may be available. Do you want to check for updates? (y/n) " update_choice
    if [[ "$update_choice" =~ ^[Yy]$ ]]; then
    echo "Checking for and installing updates..."
    # Check if yt-dlp was installed via pip
    if command -v "pip" &> /dev/null && pip freeze | grep "yt-dlp" &> /dev/null; then
    pip install -U yt-dlp
    else
    # Fallback to the self-update command
    yt-dlp -U
    fi
    
    if [ $? -eq 0 ]; then
    echo "yt-dlp updated successfully!"
    else
    echo "Failed to update yt-dlp."
    fi
    fi
    fi
    }
    
    # ----------------------------------------------------------------------
    # --- Dependency Checks with Installation Prompts ---
    # ----------------------------------------------------------------------
    
    # Function to safely install a tool
    install_tool() {
    local tool_name="$1"
    local install_cmd="$2"
    local snap_install="$3"
    
    # First, check if the tool is already installed
    if command -v "$tool_name" &> /dev/null; then
    echo "'$tool_name' is already installed."
    return 0
    fi
    
    # If not, prompt the user for installation
    echo "The '$tool_name' tool is required for this script."
    read -p "Do you want to install it now? (y/n) " install_choice
    
    if [[ "$install_choice" =~ ^[Yy]$ ]]; then
    echo "Installing $tool_name..."
    if [ -n "$snap_install" ]; then
    sudo snap install "$snap_install"
    else
    sudo $install_cmd
    fi
    
    # Check if the installation was successful
    if [ $? -eq 0 ]; then
    echo "'$tool_name' installed successfully!"
    # Add a small delay and re-check to ensure the shell updates
    sleep 1
    if ! command -v "$tool_name" &> /dev/null; then
    echo "Warning: '$tool_name' was installed but not found in PATH. Please open a new terminal or run 'source ~/.bashrc'."
    return 1
    fi
    return 0
    else
    echo "Failed to install '$tool_name'. Please install it manually."
    return 1
    fi
    else
    echo "Skipping '$tool_name' installation. Some features may not work."
    return 1
    fi
    }
    
    # ----------------------------------------------------------------------
    # --- yt-dlp Command Builder Function (FIXED) ---
    # ----------------------------------------------------------------------
    build_yt_dlp_command() {
    local url="$1"
    local output_template="$2"
    local use_cookies="$3" # 'y' or 'n'
    local download_audio="$4" # 'y' or 'n'
    local extra_options="$5"
    
    local YTDLP_CMD="yt-dlp \"$url\""
    
    if [[ "$use_cookies" =~ ^[Yy]$ ]]; then
    echo "Select a browser for cookies:" >&2 # Output to stderr
    local options=("Chrome" "Firefox" "Brave" "Edge")
    select selected_browser in "${options[@]}"; do
    if [[ -n "$selected_browser" ]]; then
    break
    else
    echo "Invalid selection. Please choose a number from the list." >&2 # Output to stderr
    fi
    done
    
    read -p "Enter profile name (e.g., 'Default') or leave blank: " profile_name
    
    if [[ -n "$profile_name" ]]; then
    YTDLP_CMD="$YTDLP_CMD --cookies-from-browser \"${selected_browser,,}:$profile_name\""
    else
    YTDLP_CMD="$YTDLP_CMD --cookies-from-browser \"${selected_browser,,}\""
    fi
    fi
    
    # ALWAYS add general robustness flags.
    YTDLP_CMD="$YTDLP_CMD --no-check-certificate"
    
    if [[ "$download_audio" =~ ^[Yy]$ ]]; then
    YTDLP_CMD="$YTDLP_CMD -x --audio-format mp3 -o '$output_template'"
    echo "Got it! We'll download the audio in MP3 format." >&2 # Output to stderr
    else
    # Default to best video/audio combination
    YTDLP_CMD="$YTDLP_CMD -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' -o '$output_template'"
    echo "Downloading the best quality video (MP4 preferred)." >&2 # Output to stderr
    fi
    
    if [[ -n "$extra_options" ]]; then
    YTDLP_CMD="$YTDLP_CMD $extra_options"
    fi
    
    echo "$YTDLP_CMD" # Only this line is printed to stdout and captured by $()
    }
    
    # ----------------------------------------------------------------------
    # --- Main Script Logic and Dependency Management ---
    # ----------------------------------------------------------------------
    
    # Run update check first
    update_yt_dlp
    
    echo "Checking for required dependencies..."
    if ! command -v "snap" &> /dev/null; then
    install_tool "snapd" "apt install snapd"
    else
    echo "'snapd' is already installed."
    fi
    install_tool "figlet" "apt install figlet"
    install_tool "lolcat" "" "lolcat"
    install_tool "yt-dlp" "apt install yt-dlp"
    install_tool "smplayer" "apt-get install -y smplayer" "smplayer"
    install_tool "ffmpeg" "apt install ffmpeg" # <--- NEW DEPENDENCY FOR SCREEN CAPTURE
    
    # At this point, all required tools should be installed or the user declined.
    for cmd in yt-dlp figlet lolcat smplayer; do
    if ! command -v "$cmd" &> /dev/null; then
    type_text "Error: '$cmd' is not installed. Exiting."
    exit 1
    fi
    done
    
    # --- Script Configuration ---
    SEPARATOR="---------------------------------------------------"
    echo "$SEPARATOR" | lolcat
    figlet "YTDLP" | lolcat
    echo "$SEPARATOR" | lolcat
    
    # Define a configuration file to save settings
    CONFIG_FILE=".yt-dlp_config"
    
    # ----------------------------------------------------------------------
    # --- Function: Download and Play from a Playlist File ---
    # ----------------------------------------------------------------------
    
    download_and_play_playlist() {
    read -p "Enter the name of the text file with the links: " file_name
    
    if [ ! -f "$file_name" ]; then
    echo "Error: File '$file_name' not found."
    return
    fi
    
    # Check for cookies ONCE
    read -p "Do these videos require browser authentication (cookies)? (y/n) " use_cookies_choice
    
    # Create the playlist folder if it doesn't exist
    mkdir -p playlist
    
    echo "Starting download and play session from '$file_name'..."
    
    # Read each URL and process it one by one
    while IFS= read -r url; do
    if [ -n "$url" ]; then
    echo "$SEPARATOR" | lolcat
    echo "Processing video from URL: $url"
    
    # Use the new builder function for consistency
    YTDLP_CMD=$(build_yt_dlp_command "$url" "playlist/%(title)s.%(ext)s" "$use_cookies_choice" "n" "")
    
    echo "Executing: $YTDLP_CMD"
    eval "$YTDLP_CMD"
    
    if [ $? -eq 0 ]; then
    echo "Download successful."
    
    # Find the downloaded file (improved find logic)
    # Find the most recently modified file in the playlist directory
    video_file=$(find playlist -type f -mtime -1m -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1 | cut -d' ' -f2-)
    
    if [ -n "$video_file" ] && [ -f "$video_file" ]; then
    echo "Playing: $(basename "$video_file")"
    # Use smplayer to play the video and then exit when it's done.
    smplayer -close-after-media-ended "$video_file"
    
    # After playing, prompt for deletion and removal
    read -p "Finished playing. Do you want to delete this video and remove the link from the file? (y/n) " delete_choice
    if [[ "$delete_choice" =~ ^[Yy]$ ]]; then
    rm "$video_file"
    # Use sed to remove the URL only if it's the exact line (safer regex)
    sed -i.bak "\@^$url$@d" "$file_name"
    echo "Deleted video and removed link from $file_name."
    else
    echo "Skipped deletion and link removal."
    fi
    else
    echo "Could not reliably find the downloaded video file."
    fi
    else
    echo "Download failed for URL: $url"
    fi
    fi
    done < "$file_name"
    
    echo "$SEPARATOR" | lolcat
    echo "Playlist session complete."
    }
    
    # ----------------------------------------------------------------------
    # --- Function to add a URL to a playlist file without downloading ---
    # ----------------------------------------------------------------------
    add_url_to_playlist() {
    read -p "Please paste the YouTube URL you want to add to a playlist: " url
    if [[ -z "$url" ]]; then
    type_text "No URL provided. Exiting."
    return
    fi
    
    LAST_PLAYLIST_FILE="playlist.txt"
    if [ -f "$CONFIG_FILE" ]; then
    LAST_PLAYLIST_FILE=$(grep "^LAST_PLAYLIST_FILE=" "$CONFIG_FILE" | cut -d'=' -f2-)
    if [ -z "$LAST_PLAYLIST_FILE" ]; then
    LAST_PLAYLIST_FILE="playlist.txt"
    fi
    fi
    
    read -p "Enter the name of the playlist file to add the link to (default: $LAST_PLAYLIST_FILE): " playlist_file_input
    
    if [ -z "$playlist_file_input" ]; then
    playlist_file="$LAST_PLAYLIST_FILE"
    else
    playlist_file="$playlist_file_input"
    fi
    
    echo "$url" >> "$playlist_file"
    type_text "URL added to $playlist_file"
    
    echo "LAST_PLAYLIST_FILE=$playlist_file" > "$CONFIG_FILE"
    }
    
    # ----------------------------------------------------------------------
    # --- Main Script Execution ---
    # ----------------------------------------------------------------------
    
    type_text "Welcome to the yt-dlp interactive downloader!"
    echo "https://taksshack.com"
    echo ""
    
    echo "Please choose an option:"
    echo " [a] Add a single video to a playlist file (after downloading)"
    echo " [p] Run and play a list of videos from a file"
    echo " [s] Download a single video and ask to play/save it"
    echo " [d] Add a single URL to a playlist file (without downloading)"
    echo " [l] Live Stream/Play without downloading"
    read -p "Your choice (a/p/s/d/l): " main_choice
    
    if [[ "$main_choice" =~ ^[Pp]$ ]]; then
    download_and_play_playlist
    elif [[ "$main_choice" =~ ^[Aa]$ ]]; then
    read -p "Please paste the YouTube URL you want to add: " url
    
    # Get original filename early
    original_filename=$(yt-dlp --get-filename -o '%(title)s.%(ext)s' "$url")
    temp_output_file="playlist.mp4"
    
    read -p "Do you want to use a browser for authentication? (y/n) " use_cookies_choice
    
    # Build the command using the new function
    YTDLP_CMD=$(build_yt_dlp_command "$url" "$temp_output_file" "$use_cookies_choice" "n" "")
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Your final download command is ready:"
    echo "$YTDLP_CMD"
    echo "$SEPARATOR" | lolcat
    echo ""
    
    read -p "Ready to download and add to playlist? (y/n) " execute_choice
    if [[ "$execute_choice" =~ ^[Yy]$ ]]; then
    echo "Starting download..."
    eval "$YTDLP_CMD"
    
    if [[ $? -eq 0 ]]; then
    echo "Download finished!"
    
    smplayer "$temp_output_file"
    
    read -p "Finished playing. Do you want to delete the video? (y/n) " delete_video_choice
    if [[ "$delete_video_choice" =~ ^[Yy]$ ]]; then
    rm "$temp_output_file"
    echo "Deleted $temp_output_file"
    else
    mv "$temp_output_file" "$original_filename"
    echo "Video renamed to '$original_filename'"
    fi
    
    LAST_PLAYLIST_FILE="playlist.txt"
    if [ -f "$CONFIG_FILE" ]; then
    LAST_PLAYLIST_FILE=$(grep "^LAST_PLAYLIST_FILE=" "$CONFIG_FILE" | cut -d'=' -f2-)
    if [ -z "$LAST_PLAYLIST_FILE" ]; then
    LAST_PLAYLIST_FILE="playlist.txt"
    fi
    fi
    
    read -p "Enter the name of the playlist file to add the link to (default: $LAST_PLAYLIST_FILE): " playlist_file_input
    
    if [ -z "$playlist_file_input" ]; then
    playlist_file="$LAST_PLAYLIST_FILE"
    else
    playlist_file="$playlist_file_input"
    fi
    
    echo "$url" >> "$playlist_file"
    echo "URL added to $playlist_file"
    
    echo "LAST_PLAYLIST_FILE=$playlist_file" > "$CONFIG_FILE"
    else
    echo "--- DOWNLOAD FAILED ---"
    read -p "The primary download failed. Do you want to try screen capturing as a backup? (y/n) " capture_choice
    
    if [[ "$capture_choice" =~ ^[Yy]$ ]]; then
    if command -v "ffmpeg" &> /dev/null; then
    type_text "FFmpeg is installed. To screen record the video (Last Resort):"
    echo "1. Play the video in a full-screen browser or player now."
    echo "2. Open a NEW terminal and run the command below. (You may need to adjust '1920x1080' and ':0.0')."
    echo "3. Press 'q' in the new terminal to stop recording."
    
    # Provide the generic, common FFmpeg command for Linux (X11)
    echo ""
    echo "--- BACKUP COMMAND ---" | lolcat
    echo "ffmpeg -f x11grab -s 1920x1080 -i :0.0 -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p 'Screen_Capture_Backup_$(date +%Y%m%d_%H%M%S).mp4'" | lolcat
    echo "----------------------" | lolcat
    type_text "Recording will be saved in your current directory."
    else
    echo "FFmpeg (the best command-line screen recorder) is not installed."
    echo "Please install it (e.g., 'sudo apt install ffmpeg') or use a graphical screen capture tool like OBS Studio."
    fi
    else
    echo "Backup capture skipped."
    fi
    fi
    
    echo "$SEPARATOR" | lolcat
    else
    echo "Download cancelled."
    fi
    
    elif [[ "$main_choice" =~ ^[Dd]$ ]]; then
    add_url_to_playlist
    elif [[ "$main_choice" =~ ^[Ss]$ ]]; then
    read -p "Please paste the YouTube URL you want to download: " url
    
    # Get original filename early
    original_filename=$(yt-dlp --get-filename -o '%(title)s.%(ext)s' "$url")
    temp_filename="taksshack.com.mp4" # Use a consistent temp filename
    
    read -p "Do you want to use a browser for authentication? (y/n) " use_cookies_choice
    
    read -p "Do you want to download just the audio? (y/n) " download_audio_choice
    
    read -p "Any other yt-dlp options (e.g., --verbose)? " extra_options
    if [[ "$extra_options" == "n" ]]; then
    extra_options=""
    fi
    
    # Build the command using the new function
    YTDLP_CMD=$(build_yt_dlp_command "$url" "$temp_filename" "$use_cookies_choice" "$download_audio_choice" "$extra_options")
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Your final command is ready:"
    echo "$YTDLP_CMD"
    echo "$SEPARATOR" | lolcat
    echo ""
    
    read -p "Ready to download? (y/n) " execute_choice
    if [[ "$execute_choice" =~ ^[Yy]$ ]]; then
    echo "Starting download..."
    eval "$YTDLP_CMD"
    
    if [[ $? -eq 0 ]]; then
    echo "Download finished!"
    
    read -p "Do you want to play the downloaded media? (y/n) " play_choice
    if [[ "$play_choice" =~ ^[Yy]$ ]]; then
    smplayer "$temp_filename"
    fi
    
    # Check the actual extension of the downloaded file
    if [[ "$download_audio_choice" =~ ^[Yy]$ ]]; then
    original_filename="${original_filename%.*}.mp3" # Ensure .mp3 extension if audio was downloaded
    fi
    
    read -p "Do you want to save the file as '$original_filename'? (y/n) " save_choice
    if [[ "$save_choice" =~ ^[Yy]$ ]]; then
    mv "$temp_filename" "$original_filename"
    echo "File saved as '$original_filename'"
    else
    rm "$temp_filename"
    echo "File deleted."
    fi
    else
    echo "--- DOWNLOAD FAILED ---"
    read -p "The primary download failed. Do you want to try screen capturing as a backup? (y/n) " capture_choice
    
    if [[ "$capture_choice" =~ ^[Yy]$ ]]; then
    if command -v "ffmpeg" &> /dev/null; then
    type_text "FFmpeg is installed. To screen record the video (Last Resort):"
    echo "1. Play the video in a full-screen browser or player now."
    echo "2. Open a NEW terminal and run the command below. (You may need to adjust '1920x1080' and ':0.0')."
    echo "3. Press 'q' in the new terminal to stop recording."
    
    # Provide the generic, common FFmpeg command for Linux (X11)
    echo ""
    echo "--- BACKUP COMMAND ---" | lolcat
    echo "ffmpeg -f x11grab -s 1920x1080 -i :0.0 -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p 'Screen_Capture_Backup_$(date +%Y%m%d_%H%M%S).mp4'" | lolcat
    echo "----------------------" | lolcat
    type_text "Recording will be saved in your current directory."
    else
    echo "FFmpeg (the best command-line screen recorder) is not installed."
    echo "Please install it (e.g., 'sudo apt install ffmpeg') or use a graphical screen capture tool like OBS Studio."
    fi
    else
    echo "Backup capture skipped."
    fi
    fi
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Operation complete."
    echo "$SEPARATOR" | lolcat
    else
    echo "Download cancelled."
    fi
    
    elif [[ "$main_choice" =~ ^[Ll]$ ]]; then
    read -p "Please paste the YouTube URL you want to stream live: " url
    
    read -p "Do you want to use a browser for authentication? (y/n) " use_cookies_choice
    
    # Set up the base command for piping to STDOUT
    STREAM_CMD="yt-dlp \"$url\" -f 'best' -o - --no-check-certificate"
    
    if [[ "$use_cookies_choice" =~ ^[Yy]$ ]]; then
    echo "Select a browser for cookies:"
    options=("Chrome" "Firefox" "Brave" "Edge")
    select selected_browser in "${options[@]}"; do
    if [[ -n "$selected_browser" ]]; then
    break
    else
    echo "Invalid selection. Please choose a number from the list."
    fi
    done
    
    read -p "Enter profile name (e.g., 'Default') or leave blank: " profile_name
    
    if [[ -n "$profile_name" ]]; then
    STREAM_CMD="$STREAM_CMD --cookies-from-browser \"${selected_browser,,}:$profile_name\""
    else
    STREAM_CMD="$STREAM_CMD --cookies-from-browser \"${selected_browser,,}\""
    fi
    fi
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Starting live stream. Press Ctrl+C to stop playback."
    echo "Executing: $STREAM_CMD | smplayer -"
    echo "$SEPARATOR" | lolcat
    
    # Execute the command and pipe its raw output into smplayer
    eval "$STREAM_CMD | smplayer -"
    
    echo "$SEPARATOR" | lolcat
    echo "Live stream complete."
    fi
    #8216
    thumbtak
    Moderator

    Option H added, and some other fixes, that blocked downloads.

    #!/bin/bash
    
    # Function to type text character by character for a cool visual effect
    type_text() {
    text="$1"
    for ((i=0; i<${#text}; i++)); do
    echo -n "${text:$i:1}"
    sleep 0.05 # Adjust this value to change the typing speed
    done
    echo "" # Add a newline at the end
    }
    
    # ----------------------------------------------------------------------
    # --- yt-dlp Update Check ---
    # ----------------------------------------------------------------------
    
    update_yt_dlp() {
    if command -v "yt-dlp" &> /dev/null; then
    read -p "A new version of yt-dlp may be available. Do you want to check for updates? (y/n) " update_choice
    if [[ "$update_choice" =~ ^[Yy]$ ]]; then
    echo "Checking for and installing updates..."
    # Check if yt-dlp was installed via pip
    if command -v "pip" &> /dev/null && pip freeze | grep "yt-dlp" &> /dev/null; then
    pip install -U yt-dlp
    else
    # Fallback to the self-update command
    yt-dlp -U
    fi
    
    if [ $? -eq 0 ]; then
    echo "yt-dlp updated successfully!"
    else
    echo "Failed to update yt-dlp."
    fi
    fi
    fi
    }
    
    # ----------------------------------------------------------------------
    # --- Dependency Checks with Installation Prompts ---
    # ----------------------------------------------------------------------
    
    # Function to safely install a tool
    install_tool() {
    local tool_name="$1"
    local install_cmd="$2"
    local snap_install="$3"
    
    # First, check if the tool is already installed
    if command -v "$tool_name" &> /dev/null; then
    echo "'$tool_name' is already installed."
    return 0
    fi # <--- FIX: Corrected syntax from '}' to 'fi'
    
    # If not, prompt the user for installation
    echo "The '$tool_name' tool is required for this script."
    read -p "Do you want to install it now? (y/n) " install_choice
    
    if [[ "$install_choice" =~ ^[Yy]$ ]]; then
    echo "Installing $tool_name..."
    if [ -n "$snap_install" ]; then
    sudo snap install "$snap_install"
    else
    sudo $install_cmd
    fi
    
    # Check if the installation was successful
    if [ $? -eq 0 ]; then
    echo "'$tool_name' installed successfully!"
    # Add a small delay and re-check to ensure the shell updates
    sleep 1
    if ! command -v "$tool_name" &> /dev/null; then
    echo "Warning: '$tool_name' was installed but not found in PATH. Please open a new terminal or run 'source ~/.bashrc'."
    return 1
    fi
    return 0
    else
    echo "Failed to install '$tool_name'. Please install it manually."
    return 1
    fi
    else
    echo "Skipping '$tool_name' installation. Some features may not work."
    return 1
    fi
    }
    
    # ----------------------------------------------------------------------
    # --- Cookie Flag Builder Function ---
    # ----------------------------------------------------------------------
    # Helper to generate the cookie flags string based on user input
    get_cookie_flags() {
    local use_cookies_choice="$1"
    local browser_name="$2"
    local profile_name="$3"
    local cookie_flags=""
    
    if [[ "$use_cookies_choice" =~ ^[Yy]$ ]]; then
    # Aggressively clean and lowercase the browser name
    local clean_browser_name
    clean_browser_name=$(echo "$browser_name" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]')
    
    if [[ -n "$profile_name" ]]; then
    # FIX: Removed internal quotes to prevent passing literal quotes to yt-dlp
    cookie_flags="--cookies-from-browser ${clean_browser_name}:${profile_name}"
    else
    # FIX: Removed internal quotes to prevent passing literal quotes to yt-dlp
    cookie_flags="--cookies-from-browser ${clean_browser_name}"
    fi
    fi
    echo "$cookie_flags"
    }
    
    # ----------------------------------------------------------------------
    # --- yt-dlp Command Builder Function (Now expects pre-built cookie flags) ---
    # ----------------------------------------------------------------------
    build_yt_dlp_command() {
    local url="$1"
    local output_template="$2"
    local download_audio="$3" # 'y' or 'n'
    local extra_options="$4"
    local print_filename_flag="$5" # '--print filename' or ''
    local cookie_flags="$6" # New: Pre-built cookie flags
    
    # The command now includes the cookie flags at the start
    local YTDLP_CMD="yt-dlp $cookie_flags \"$url\""
    
    # RESTORING USER-AGENT and other robustness flags to bypass 403 errors.
    YTDLP_CMD="$YTDLP_CMD --user-agent \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36\" --no-check-certificate"
    
    if [[ "$download_audio" =~ ^[Yy]$ ]]; then
    YTDLP_CMD="$YTDLP_CMD -x --audio-format mp3 -o '$output_template'"
    echo "Got it! We'll download the audio in MP3 format." >&2 # Output to stderr
    else
    # Default to best video/audio combination
    YTDLP_CMD="$YTDLP_CMD -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' -o '$output_template'"
    echo "Downloading the best quality video (MP4 preferred)." >&2 # Output to stderr
    fi
    
    if [[ -n "$extra_options" ]]; then
    YTDLP_CMD="$YTDLP_CMD $extra_options"
    fi
    
    # Append the filename print flag if requested
    if [[ -n "$print_filename_flag" ]]; then
    YTDLP_CMD="$YTDLP_CMD $print_filename_flag"
    fi
    
    echo "$YTDLP_CMD" # Only this line is printed to stdout and captured by $()
    }
    
    # ----------------------------------------------------------------------
    # --- CORE DOWNLOAD FUNCTION (Updated to accept cookie flags) ---
    # ----------------------------------------------------------------------
    run_single_download() {
    local url="$1"
    local temp_output_template="$2"
    local download_audio_choice="$3"
    local interactive_mode="$4" # 'y' for [s], 'n' for [h]
    local extra_options="$5" # Passed in
    local cookie_flags="$6" # Passed in
    
    # Check if we need the complex print filename logic (only if template contains %(ext)s)
    local print_filename_flag=""
    if [[ "$temp_output_template" =~ "%(ext)s" ]]; then
    print_filename_flag="--print filename"
    fi
    
    # Build the command string (passing cookie flags)
    YTDLP_CMD=$(build_yt_dlp_command "$url" "$temp_output_template" "$download_audio_choice" "$extra_options" "$print_filename_flag" "$cookie_flags")
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Your final command is ready:"
    echo "$YTDLP_CMD"
    echo "$SEPARATOR" | lolcat
    echo ""
    
    if [[ "$interactive_mode" == "y" ]]; then
    read -p "Ready to download? (y/n) " execute_choice
    if [[ ! "$execute_choice" =~ ^[Yy]$ ]]; then
    return 1 # Download cancelled
    fi
    fi
    
    echo "Starting download..."
    
    # Simple execution for fixed names (Used in [s] and [h])
    # $YTDLP_CMD contains the cookie flags which expand to two arguments
    eval "$YTDLP_CMD"
    export FINAL_DOWNLOAD_STATUS=$?
    export FINAL_DOWNLOAD_FILENAME="$temp_output_template"
    
    if [ $FINAL_DOWNLOAD_STATUS -ne 0 ]; then
    echo "An error occurred during the download."
    # Clean up partial files using the known fixed name
    rm -f "${temp_output_template}.part"
    return 1
    fi
    
    echo "Download finished!"
    return 0 # Download successful
    }
    
    # ----------------------------------------------------------------------
    # --- Main Script Logic and Dependency Management ---
    # ----------------------------------------------------------------------
    
    # Run update check first
    update_yt_dlp
    
    echo "Checking for required dependencies..."
    if ! command -v "snap" &> /dev/null; then
    install_tool "snapd" "apt install snapd"
    else
    echo "'snapd' is already installed."
    fi
    install_tool "figlet" "apt install figlet"
    install_tool "lolcat" "" "lolcat"
    install_tool "yt-dlp" "apt install yt-dlp"
    install_tool "mpv" "apt-get install -y mpv" # Install mpv for Option [h]
    install_tool "smplayer" "apt-get install -y smplayer" "smplayer" # Keep smplayer for Option [s]
    
    # At this point, all required tools should be installed or the user declined.
    for cmd in yt-dlp figlet lolcat; do
    if ! command -v "$cmd" &> /dev/null; then
    type_text "Error: '$cmd' is not installed. Exiting."
    exit 1
    fi
    done
    
    # Check for at least one media player
    if ! command -v "smplayer" &> /dev/null && ! command -v "mpv" &> /dev/null; then
    type_text "Error: Neither 'smplayer' nor 'mpv' is installed. Please install at least one. Exiting."
    exit 1
    fi
    
    # --- Script Configuration ---
    SEPARATOR="---------------------------------------------------"
    echo "$SEPARATOR" | lolcat
    figlet "YTDLP" | lolcat
    echo "$SEPARATOR" | lolcat
    
    # Define a configuration file to save settings
    CONFIG_FILE=".yt-dlp_config"
    
    # ----------------------------------------------------------------------
    # --- Function: Download and Play from a Playlist File (P/A/D logic) ---
    # ----------------------------------------------------------------------
    
    download_and_play_playlist() {
    read -p "Enter the name of the text file with the links: " file_name
    
    if [ ! -f "$file_name" ]; then
    echo "Error: File '$file_name' not found."
    return
    fi
    
    local use_cookies_choice
    local browser_name=""
    local profile_name=""
    local cookie_flags=""
    
    read -p "Do these videos require browser authentication (cookies)? (y/n) " use_cookies_choice
    
    # --- COOKIE INTERACTION FOR [p] (Playlist mode) ---
    if [[ "$use_cookies_choice" =~ ^[Yy]$ ]]; then
    echo "Select a browser for cookies:"
    local options=("Chrome" "Firefox" "Brave" "Edge")
    select browser_name_temp in "${options[@]}"; do
    if [[ -n "$browser_name_temp" ]]; then
    browser_name="$browser_name_temp"
    break
    else
    echo "Invalid selection. Please choose a number from the list."
    fi
    done
    read -p "Enter profile name (e.g., 'Default') or leave blank: " profile_name
    cookie_flags=$(get_cookie_flags "$use_cookies_choice" "$browser_name" "$profile_name")
    fi
    # --------------------------------------------------
    
    mkdir -p playlist
    
    echo "Starting download and play session from '$file_name'..."
    
    while IFS= read -r url; do
    if [ -n "$url" ]; then
    echo "$SEPARATOR" | lolcat
    echo "Processing video from URL: $url"
    
    # Use the command builder, passing cookie flags
    YTDLP_CMD=$(build_yt_dlp_command "$url" "playlist/%(title)s.%(ext)s" "n" "" "" "$cookie_flags")
    
    echo "Executing: $YTDLP_CMD"
    eval "$YTDLP_CMD"
    
    if [ $? -eq 0 ]; then
    echo "Download successful."
    
    # Finding the file by last modified time is the most reliable way when using %(title)s.%(ext)s
    video_file=$(find playlist -type f -mtime -1m -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1 | cut -d' ' -f2-)
    
    if [ -n "$video_file" ] && [ -f "$video_file" ]; then
    echo "Playing: $(basename "$video_file")"
    # Default to smplayer here, as this is the standard playlist mode
    if command -v "smplayer" &> /dev/null; then
    smplayer -close-after-media-ended "$video_file"
    elif command -v "mpv" &> /dev/null; then
    mpv "$video_file"
    fi
    
    read -p "Finished playing. Do you want to delete this video and remove the link from the file? (y/n) " delete_choice
    if [[ "$delete_choice" =~ ^[Yy]$ ]]; then
    rm "$video_file"
    sed -i.bak "\@^$url$@d" "$file_name"
    echo "Deleted video and removed link from $file_name."
    else
    echo "Skipped deletion and link removal."
    fi
    else
    echo "Could not reliably find the downloaded video file."
    fi
    else
    echo "Download failed for URL: $url"
    fi
    fi
    done < "$file_name"
    
    echo "$SEPARATOR" | lolcat
    echo "Playlist session complete."
    }
    
    add_url_to_playlist() {
    read -p "Please paste the YouTube URL you want to add to a playlist: " url
    if [[ -z "$url" ]]; then
    type_text "No URL provided. Exiting."
    return
    fi
    
    LAST_PLAYLIST_FILE="playlist.txt"
    if [ -f "$CONFIG_FILE" ]; then
    LAST_PLAYLIST_FILE=$(grep "^LAST_PLAYLIST_FILE=" "$CONFIG_FILE" | cut -d'=' -f2-)
    if [ -z "$LAST_PLAYLIST_FILE" ]; then
    LAST_PLAYLIST_FILE="playlist.txt"
    fi
    fi
    
    read -p "Enter the name of the playlist file to add the link to (default: $LAST_PLAYLIST_FILE): " playlist_file_input
    
    if [ -z "$playlist_file_input" ]; then
    playlist_file="$LAST_PLAYLIST_FILE"
    else
    playlist_file="$playlist_file_input"
    fi
    
    echo "$url" >> "$playlist_file"
    type_text "URL added to $playlist_file"
    
    echo "LAST_PLAYLIST_FILE=$playlist_file" > "$CONFIG_FILE"
    }
    
    # ----------------------------------------------------------------------
    # --- Function: Download, Auto-Play, and Delete Loop ([h] Logic) ---
    # ----------------------------------------------------------------------
    hybrid_stream_loop() {
    # The temporary fixed filename placeholder
    local temp_filename="taksshack.com.mp4"
    local MAX_RETRIES=10
    local RETRY_DELAY=10
    
    # Ensure mpv is installed, since this option relies on it for direct playback
    if ! command -v "mpv" &> /dev/null; then
    echo "Error: The [h] option requires 'mpv'. Please install it."
    return 1
    fi
    
    while true; do
    echo "$SEPARATOR" | lolcat
    read -p "Please paste the YouTube URL you want to download and play (or type 'q' to exit): " url
    
    if [[ "$url" =~ ^[Qq]$ ]]; then
    break
    elif [[ -z "$url" ]]; then
    echo "No URL provided. Please try again."
    continue
    fi
    
    # --- INTERACTION FOR [h] (Cookie and Extra Options) ---
    local use_cookies_choice
    local browser_name=""
    local profile_name=""
    local cookie_flags=""
    
    read -p "Do you want to use a browser for authentication (cookies)? (y/n) " use_cookies_choice
    
    # Follow-up questions for cookie details
    if [[ "$use_cookies_choice" =~ ^[Yy]$ ]]; then
    echo "Select a browser for cookies:"
    local options=("Chrome" "Firefox" "Brave" "Edge")
    select browser_name_temp in "${options[@]}"; do
    if [[ -n "$browser_name_temp" ]]; then
    browser_name="$browser_name_temp"
    break
    else
    echo "Invalid selection. Please choose a number from the list."
    fi
    done
    read -p "Enter profile name (e.g., 'Default') or leave blank: " profile_name
    # Build the cookie flags immediately
    cookie_flags=$(get_cookie_flags "$use_cookies_choice" "$browser_name" "$profile_name")
    fi
    
    read -p "Any other yt-dlp options (e.g., --verbose)? " extra_options
    if [[ "$extra_options" == "n" ]]; then
    extra_options=""
    fi
    # -------------------------------
    
    # 1. Get the actual final filename, which includes the correct extension
    # FIX: Now includes $cookie_flags to authenticate the filename lookup
    local original_filename
    # $cookie_flags is unquoted here, correctly expanding to "--cookies-from-browser chrome" (two arguments)
    original_filename=$(yt-dlp --get-filename -o '%(title)s.%(ext)s' $cookie_flags "$url" 2>/dev/tty)
    local status=$?
    
    # Trim leading/trailing whitespace just in case (important if yt-dlp spits out newlines)
    original_filename=$(echo "$original_filename" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
    
    if [ $status -ne 0 ] || [ -z "$original_filename" ]; then
    echo "Error: Could not determine the original filename/extension for the URL (yt-dlp exit code: $status). Aborting URL."
    continue
    fi
    
    # 2. Download Loop with Retries
    local RETRY_COUNT=1
    local DOWNLOAD_SUCCESS=0
    
    while [ $RETRY_COUNT -le $MAX_RETRIES ]; do
    echo "$SEPARATOR" | lolcat
    echo "--- Download Attempt $RETRY_COUNT of $MAX_RETRIES ---"
    
    # Run the core download function (passing cookie flags)
    run_single_download "$url" "$temp_filename" "n" "n" "$extra_options" "$cookie_flags"
    
    if [ $? -eq 0 ]; then
    DOWNLOAD_SUCCESS=1
    break # Exit the retry loop on success
    else
    echo "Download failed. Waiting $RETRY_DELAY seconds before retrying..."
    sleep $RETRY_DELAY
    RETRY_COUNT=$((RETRY_COUNT + 1))
    fi
    done
    
    if [ $DOWNLOAD_SUCCESS -eq 1 ]; then
    # 3. Download was successful. Check if the placeholder file exists.
    if [ -f "$temp_filename" ]; then
    
    # 4. ATOMIC RENAME: Rename the placeholder to the actual name/extension.
    local final_video_file="$original_filename"
    mv "$temp_filename" "$final_video_file"
    
    echo "$SEPARATOR" | lolcat
    echo "Launching player (mpv) fullscreen and stretched for: $final_video_file"
    
    # 5. Play the correctly named file using direct mpv with full screen and forced stretch
    mpv --fullscreen --no-keepaspect --no-keepaspect-window "$final_video_file"
    
    echo "Playback session ended. Auto-deleting file."
    
    # 6. Auto-delete the final file
    rm -f "$final_video_file"
    echo "Temporary file deleted: $final_video_file"
    else
    echo "Error: Download was reported successful but the final file could not be located at the expected placeholder ($temp_filename)."
    rm -f "${temp_filename}.part"
    fi
    else
    echo "$SEPARATOR" | lolcat
    echo "Download failed after $MAX_RETRIES attempts. Moving to next URL or exiting loop."
    fi
    
    echo "$SEPARATOR" | lolcat
    echo "Operation complete."
    echo "$SEPARATOR" | lolcat
    
    read -p "Do you want to process another URL? (y/n) " play_again_choice
    if [[ ! "$play_again_choice" =~ ^[Yy]$ ]]; then
    break
    fi
    done
    }
    
    # ----------------------------------------------------------------------
    # --- Single Download and Play/Save ([s] Logic) ---
    # ----------------------------------------------------------------------
    single_download_and_save() {
    read -p "Please paste the YouTube URL you want to download: " url
    
    # --- INTERACTION FOR [s] ---
    local use_cookies_choice
    local browser_name=""
    local profile_name=""
    local cookie_flags=""
    
    read -p "Do you want to use a browser for authentication? (y/n) " use_cookies_choice
    
    # Follow-up questions for cookie details
    if [[ "$use_cookies_choice" =~ ^[Yy]$ ]]; then
    echo "Select a browser for cookies:"
    local options=("Chrome" "Firefox" "Brave" "Edge")
    select browser_name_temp in "${options[@]}"; do
    if [[ -n "$browser_name_temp" ]]; then
    browser_name="$browser_name_temp"
    break
    else
    echo "Invalid selection. Please choose a number from the list."
    fi
    done
    read -p "Enter profile name (e.g., 'Default') or leave blank: " profile_name
    # Build the cookie flags immediately
    cookie_flags=$(get_cookie_flags "$use_cookies_choice" "$browser_name" "$profile_name")
    fi
    
    read -p "Any other yt-dlp options (e.g., --verbose)? " extra_options
    if [[ "$extra_options" == "n" ]]; then
    extra_options=""
    fi
    # ---------------------------
    
    # Prompt for audio
    read -p "Do you want to download just the audio? (y/n) " download_audio_choice
    
    # Get original filename early to use for saving later (using cookie flags for authentication)
    local original_filename
    original_filename=$(yt-dlp --get-filename -o '%(title)s.%(ext)s' $cookie_flags "$url")
    local temp_filename="taksshack.com.mp4"
    
    # Run the core download function in interactive mode ("y"), passing cookie details
    run_single_download "$url" "$temp_filename" "$download_audio_choice" "y" "$extra_options" "$cookie_flags"
    
    if [ $? -eq 0 ]; then
    # Download was successful
    
    read -p "Do you want to play the downloaded media? (y/n) " play_choice
    if [[ "$play_choice" =~ ^[Yy]$ ]]; then
    if command -v "smplayer" &> /dev/null; then
    smplayer "$temp_filename"
    elif command -v "mpv" &> /dev/null; then
    mpv "$temp_filename"
    else
    echo "Cannot play: Neither smplayer nor mpv found."
    fi
    fi
    
    # Check the actual extension for saving
    if [[ "$download_audio_choice" =~ ^[Yy]$ ]]; then
    # If audio was downloaded, the final file is mp3, rename the original_filename variable
    original_filename="${original_filename%.*}.mp3"
    else
    # For video, rename the placeholder to the original name/extension
    local final_video_file="$original_filename"
    mv "$temp_filename" "$final_video_file"
    temp_filename="$final_video_file" # Update the reference for cleanup
    
    # Reset original_filename to the correct name/extension for the prompt
    original_filename="$final_video_file"
    fi
    
    read -p "Do you want to save the file as '$original_filename'? (y/n) " save_choice
    if [[ "$save_choice" =~ ^[Yy]$ ]]; then
    # If video, it's already renamed and saved. If audio, mv from temp_filename (which is taksshack.com.mp4)
    if [[ "$download_audio_choice" =~ ^[Yy]$ ]]; then
    mv "taksshack.com.mp4" "$original_filename"
    fi
    echo "File saved as '$original_filename'"
    else
    rm "$temp_filename"
    echo "File deleted."
    fi
    fi
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Operation complete."
    echo "$SEPARATOR" | lolcat
    }
    
    # ----------------------------------------------------------------------
    # --- Main Script Execution ---
    # ----------------------------------------------------------------------
    
    type_text "Welcome to the yt-dlp interactive downloader!"
    echo "https://taksshack.com"
    echo ""
    
    echo "Please choose an option:"
    echo " [a] Add a single video to a playlist file (after downloading)"
    echo " [p] Run and play a list of videos from a file"
    echo " [s] Download a single video and ask to play/save it"
    echo " [d] Add a single URL to a playlist file (without downloading)"
    echo " [h] Download, Auto-Play, and Delete (Uses full download progress as buffer) ⭐ NEW"
    read -p "Your choice (a/p/s/d/h): " main_choice
    
    if [[ "$main_choice" =~ ^[Pp]$ ]]; then
    download_and_play_playlist
    elif [[ "$main_choice" =~ ^[Aa]$ ]]; then
    add_url_to_playlist
    elif [[ "$main_choice" =~ ^[Dd]$ ]]; then
    add_url_to_playlist
    elif [[ "$main_choice" =~ ^[Ss]$ ]]; then
    single_download_and_save
    elif [[ "$main_choice" =~ ^[Hh]$ ]]; then
    hybrid_stream_loop
    fi
    • This reply was modified 3 months, 3 weeks ago by thumbtak. Reason: Retry added to option H if you get a 403 error
    • This reply was modified 3 months, 3 weeks ago by thumbtak. Reason: Fixed logic related to option H and using cookies
    #8221
    thumbtak
    Moderator

    A 403 error often means YouTube is blocking your IP address or rejecting your existing connection/cookies. I’ve updated the script to include two aggressive flags to help bypass this issue:

    1. --force-ipv4: Instructs yt-dlp to only use IPv4, which can sometimes bypass IPv6-related blocks.
    2. Aria2c Downloader: I’ve integrated checks for the faster external downloader, Aria2c, and set it to use 16 connections (--max-connection-per-server=16). This helps with multi-part downloads and often improves connection resilience, which can mitigate 403 issues during a download.

    Full Script with the 403 Bypass Fixes Applied

    #!/bin/bash
    
    # Function to type text character by character for a cool visual effect
    type_text() {
    text="$1"
    for ((i=0; i<${#text}; i++)); do
    echo -n "${text:$i:1}"
    sleep 0.05 # Adjust this value to change the typing speed
    done
    echo "" # Add a newline at the end
    }
    
    # ----------------------------------------------------------------------
    # --- yt-dlp Update Check ---
    # ----------------------------------------------------------------------
    
    update_yt_dlp() {
    if command -v "yt-dlp" &> /dev/null; then
    read -p "A new version of yt-dlp may be available. Do you want to check for updates? (y/n) " update_choice
    if [[ "$update_choice" =~ ^[Yy]$ ]]; then
    echo "Checking for and installing updates..."
    # Check if yt-dlp was installed via pip
    if command -v "pip" &> /dev/null && pip freeze | grep "yt-dlp" &> /dev/null; then
    pip install -U yt-dlp
    else
    # Fallback to the self-update command
    yt-dlp -U
    fi
    
    if [ $? -eq 0 ]; then
    echo "yt-dlp updated successfully!"
    else
    echo "Failed to update yt-dlp."
    fi
    fi
    fi
    }
    
    # ----------------------------------------------------------------------
    # --- Dependency Checks with Installation Prompts ---
    # ----------------------------------------------------------------------
    
    # Function to safely install a tool (FIXED SYNTAX)
    install_tool() {
    local tool_name="$1"
    local install_cmd="$2"
    local snap_install="$3"
    
    # First, check if the tool is already installed
    if command -v "$tool_name" &> /dev/null; then
    echo "'$tool_name' is already installed."
    return 0
    fi # <--- CORRECTED: Closing the 'if' statement with 'fi'
    
    # If not, prompt the user for installation
    echo "The '$tool_name' tool is required for this script."
    read -p "Do you want to install it now? (y/n) " install_choice
    
    if [[ "$install_choice" =~ ^[Yy]$ ]]; then
    echo "Installing $tool_name..."
    if [ -n "$snap_install" ]; then
    sudo snap install "$snap_install"
    else
    # Try apt-get if it's the primary install method
    if command -v "apt-get" &> /dev/null; then
    sudo apt-get update && sudo apt-get install -y "$tool_name"
    elif command -v "apt" &> /dev/null; then
    sudo apt update && sudo apt install -y "$tool_name"
    else
    sudo $install_cmd
    fi
    fi
    
    # Check if the installation was successful
    if [ $? -eq 0 ]; then
    echo "'$tool_name' installed successfully!"
    sleep 1
    if ! command -v "$tool_name" &> /dev/null; then
    echo "Warning: '$tool_name' was installed but not found in PATH. Please open a new terminal or run 'source ~/.bashrc'."
    return 1
    fi
    return 0
    else
    echo "Failed to install '$tool_name'. Please install it manually."
    return 1
    fi
    else
    echo "Skipping '$tool_name' installation. Some features may not work."
    return 1
    fi
    }
    
    # ----------------------------------------------------------------------
    # --- Tor Management Functions ---
    # ----------------------------------------------------------------------
    
    # Global variables
    TOR_STARTED_BY_SCRIPT=0
    TOR_PID=""
    TOR_AGGRESSIVE_MODE="n" # Global to track if we skip the Android spoof for max quality
    
    start_tor() {
    if ! command -v "tor" &> /dev/null; then
    echo "Tor is not installed. Skipping Tor proxy."
    return 1
    fi
    
    read -p "Do you want to use the Tor network for this operation (anonymity/bypassing geo-blocks)? (y/n) " use_tor_choice
    
    if [[ "$use_tor_choice" =~ ^[Yy]$ ]]; then
    echo ""
    echo "Tor Quality Mode Selection:"
    echo " [1] High-Quality (Aggressive): Tries for 4K/1080p, but risks the '403 Forbidden' error."
    echo " [2] Reliable (Safe): Guarantees bypass of '403 Forbidden', but often limits quality to 720p/480p."
    read -p "Select mode (1 or 2): " mode_choice
    
    if [[ "$mode_choice" == "1" ]]; then
    TOR_AGGRESSIVE_MODE="y"
    echo "Mode: High-Quality (Aggressive) selected. Proceeding to start Tor..."
    else
    TOR_AGGRESSIVE_MODE="n"
    echo "Mode: Reliable (Safe) selected. Proceeding to start Tor..."
    fi
    
    echo "Attempting to start Tor in the background (SOCKS5 on 127.0.0.1:9050)..."
    # Check if Tor is already running (e.g., via system service)
    if pgrep -x "tor" > /dev/null; then
    echo "Tor is already running. Will use existing instance."
    TOR_STARTED_BY_SCRIPT=0
    return 0
    fi
    
    # Start Tor as a background process, redirecting output to /dev/null
    tor &
    TOR_PID=$!
    sleep 5 # Wait for Tor to bootstrap
    
    if kill -0 $TOR_PID 2>/dev/null; then
    echo "Tor started successfully (PID: $TOR_PID)."
    TOR_STARTED_BY_SCRIPT=1
    return 0
    else
    echo "Failed to start Tor. Falling back to direct connection."
    TOR_STARTED_BY_SCRIPT=0
    return 1
    fi
    fi
    # If Tor is not used, reset aggressive mode just in case
    TOR_AGGRESSIVE_MODE="n"
    return 1
    }
    
    stop_tor() {
    if [ "$TOR_STARTED_BY_SCRIPT" -eq 1 ] && [ -n "$TOR_PID" ]; then
    echo "Stopping Tor process (PID: $TOR_PID)..."
    kill "$TOR_PID" 2>/dev/null
    if [ $? -eq 0 ]; then
    echo "Tor stopped."
    else
    echo "Could not stop Tor gracefully. It may have already exited."
    fi
    fi
    # Reset mode after stopping
    TOR_AGGRESSIVE_MODE="n"
    }
    
    # ----------------------------------------------------------------------
    # --- Cookie Flag Builder Function ---
    # ----------------------------------------------------------------------
    # Helper to generate the cookie flags string based on user input
    get_cookie_flags() {
    local use_cookies_choice="$1"
    local browser_name="$2"
    local profile_name="$3"
    local cookie_flags=""
    
    if [[ "$use_cookies_choice" =~ ^[Yy]$ ]]; then
    # Aggressively clean and lowercase the browser name
    local clean_browser_name
    clean_browser_name=$(echo "$browser_name" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]')
    
    if [[ -n "$profile_name" ]]; then
    cookie_flags="--cookies-from-browser ${clean_browser_name}:${profile_name}"
    else
    cookie_flags="--cookies-from-browser ${clean_browser_name}"
    fi
    fi
    echo "$cookie_flags"
    }
    
    # ----------------------------------------------------------------------
    # --- CONSOLIDATED INTERACTION FUNCTION ---
    # ----------------------------------------------------------------------
    # Prompts for cookies, Tor, and extra options in a single block.
    # Outputs:
    # - cookie_flags (string)
    # - extra_options (string)
    # - tor_enabled (global TOR_STARTED_BY_SCRIPT is set)
    # - tor_aggressive (global TOR_AGGRESSIVE_MODE is set)
    get_user_options() {
    local __cookie_flags_out=$1
    local __extra_options_out=$2
    
    local use_cookies_choice="n"
    local browser_name=""
    local profile_name=""
    local cookie_flags=""
    local extra_options=""
    
    # 1. COOKIE INTERACTION
    read -p "Do you want to use a browser for authentication (cookies)? (y/n) " use_cookies_choice
    
    if [[ "$use_cookies_choice" =~ ^[Yy]$ ]]; then
    echo "Select a browser for cookies:"
    local options=("Chrome" "Firefox" "Brave" "Edge")
    select browser_name_temp in "${options[@]}"; do
    if [[ -n "$browser_name_temp" ]]; then
    browser_name="$browser_name_temp"
    break
    else
    echo "Invalid selection. Please choose a number from the list."
    fi
    done
    read -p "Enter profile name (e.g., 'Default') or leave blank: " profile_name
    cookie_flags=$(get_cookie_flags "$use_cookies_choice" "$browser_name" "$profile_name")
    fi
    
    # 2. EXTRA OPTIONS INTERACTION
    read -p "Any other yt-dlp options (e.g., --verbose)? " extra_options_input
    if [[ "$extra_options_input" != "n" ]]; then
    extra_options="$extra_options_input"
    fi
    
    # 3. TOR INTERACTION (Sets globals TOR_STARTED_BY_SCRIPT and TOR_AGGRESSIVE_MODE)
    start_tor # This function handles the prompting and starting of Tor
    
    # Set the output variables using nameref/indirect reference
    eval "$__cookie_flags_out=\"$cookie_flags\""
    eval "$__extra_options_out=\"$extra_options\""
    }
    
    # ----------------------------------------------------------------------
    # --- yt-dlp Command Builder Function ---
    # ----------------------------------------------------------------------
    build_yt_dlp_command() {
    local url="$1"
    local output_template="$2"
    local download_audio="$3" # 'y' or 'n'
    local extra_options="$4"
    local print_filename_flag="$5" # '--print filename' or ''
    local cookie_flags="$6"
    local tor_enabled="$7" # 'y' or 'n'
    local tor_aggressive="$8" # 'y' or 'n'
    
    local YTDLP_CMD="yt-dlp"
    
    # --- Tor Proxy and 403/Quality Fix Integration ---
    if [[ "$tor_enabled" =~ ^[Yy]$ ]]; then
    YTDLP_CMD="$YTDLP_CMD --proxy socks5://127.0.0.1:9050"
    
    if [[ "$tor_aggressive" == "n" ]]; then
    # Reliable Mode: Use the Android spoof to bypass the 403 check, but quality is limited
    YTDLP_CMD="$YTDLP_CMD --extractor-args \"youtube:player-client=android\""
    echo "Tor proxy (Reliable Mode: Android spoof active)." >&2
    else
    # Aggressive Mode: Do NOT use the Android spoof to maximize quality
    echo "Tor proxy (Aggressive Mode: Max Quality attempted)." >&2
    fi
    fi
    # -----------------------------------------
    
    # Add Cookies and URL
    YTDLP_CMD="$YTDLP_CMD $cookie_flags \"$url\""
    
    # --- Robustness Flags: Referer, Retries, User-Agent ---
    YTDLP_CMD="$YTDLP_CMD --user-agent \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36\""
    YTDLP_CMD="$YTDLP_CMD --no-check-certificate"
    YTDLP_CMD="$YTDLP_CMD --extractor-retries 5"
    YTDLP_CMD="$YTDLP_CMD --referer https://www.youtube.com/"
    # --------------------------------------------------------
    
    # --- Download Engine Logic ---
    if [[ -n "$extra_options" ]] && command -v "aria2c" &> /dev/null; then
    YTDLP_CMD="$YTDLP_CMD --force-ipv4 --external-downloader aria2c --external-downloader-args \"aria2c:--max-connection-per-server=16\""
    echo "Using aria2c for download robustness." >&2
    else
    YTDLP_CMD="$YTDLP_CMD --force-ipv4"
    echo "Using internal yt-dlp download for stability." >&2
    fi
    # -----------------------------
    
    if [[ "$download_audio" =~ ^[Yy]$ ]]; then
    # Audio download
    YTDLP_CMD="$YTDLP_CMD -x --audio-format mp3 -o '$output_template'"
    echo "Downloading audio in MP3 format." >&2
    else
    # Video download: Highest Quality Resilient String
    YTDLP_CMD="$YTDLP_CMD -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio/best[ext=mp4]/best' -k -o '$output_template'"
    echo "Downloading the HIGHEST reliable quality video and audio (MP4 preferred)." >&2
    fi
    
    if [[ -n "$extra_options" ]]; then
    YTDLP_CMD="$YTDLP_CMD $extra_options"
    fi
    
    if [[ -n "$print_filename_flag" ]]; then
    YTDLP_CMD="$YTDLP_CMD $print_filename_flag"
    fi
    
    echo "$YTDLP_CMD"
    }
    
    # ----------------------------------------------------------------------
    # --- CORE DOWNLOAD FUNCTION ---
    # ----------------------------------------------------------------------
    run_single_download() {
    local url="$1"
    local temp_output_template="$2"
    local download_audio_choice="$3"
    local interactive_mode="$4" # 'y' for [s], 'n' for others
    local extra_options="$5"
    local cookie_flags="$6"
    local tor_enabled="$7" # Passed in
    local tor_aggressive="$8" # Passed in
    
    local print_filename_flag=""
    if [[ "$temp_output_template" =~ "%(ext)s" ]]; then
    print_filename_flag="--print filename"
    fi
    
    # Build the command string (passing all flags)
    YTDLP_CMD=$(build_yt_dlp_command "$url" "$temp_output_template" "$download_audio_choice" "$extra_options" "$print_filename_flag" "$cookie_flags" "$tor_enabled" "$tor_aggressive")
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Your final command is ready:"
    echo "$YTDLP_CMD"
    echo "$SEPARATOR" | lolcat
    echo ""
    
    if [[ "$interactive_mode" =~ ^[Yy]$ ]]; then
    read -p "Ready to download? (y/n) " execute_choice
    if [[ ! "$execute_choice" =~ ^[Yy]$ ]]; then
    return 1 # Download cancelled
    fi
    fi
    
    echo "Starting download..."
    
    # Execute the command
    eval "$YTDLP_CMD"
    export FINAL_DOWNLOAD_STATUS=$?
    export FINAL_DOWNLOAD_FILENAME="$temp_output_template"
    
    if [ $FINAL_DOWNLOAD_STATUS -ne 0 ]; then
    echo "An error occurred during the download (Exit Code: $FINAL_DOWNLOAD_STATUS)."
    # Clean up temporary part files
    rm -f "${temp_output_template}.part"
    return 1
    fi
    
    echo "Download finished!"
    return 0 # Download successful
    }
    
    # ----------------------------------------------------------------------
    # --- Main Script Logic and Dependency Management ---
    # ----------------------------------------------------------------------
    
    # Run update check first
    update_yt_dlp
    
    echo "Checking for required dependencies..."
    if ! command -v "snap" &> /dev/null; then
    install_tool "snapd" "apt install snapd"
    else
    echo "'snapd' is already installed."
    fi
    install_tool "figlet" "apt install figlet"
    install_tool "lolcat" "" "lolcat"
    install_tool "yt-dlp" "apt install yt-dlp"
    install_tool "mpv" "apt-get install -y mpv"
    install_tool "smplayer" "apt-get install -y smplayer" "smplayer"
    install_tool "aria2c" "apt-get install -y aria2"
    install_tool "tor" "apt-get install -y tor"
    
    # Check for essential tools
    for cmd in yt-dlp figlet lolcat; do
    if ! command -v "$cmd" &> /dev/null; then
    type_text "Error: '$cmd' is not installed. Exiting."
    exit 1
    fi
    done
    
    # Check for at least one media player
    if ! command -v "smplayer" &> /dev/null && ! command -v "mpv" &> /dev/null; then
    type_text "Error: Neither 'smplayer' nor 'mpv' is installed. Please install at least one. Exiting."
    exit 1
    fi
    
    # --- Script Configuration ---
    SEPARATOR="---------------------------------------------------"
    echo "$SEPARATOR" | lolcat
    figlet "YTDLP" | lolcat
    echo "$SEPARATOR" | lolcat
    
    # Define a configuration file to save settings
    CONFIG_FILE=".yt-dlp_config"
    
    # ----------------------------------------------------------------------
    # --- FUNCTION: Get Filename (Direct Connection) ---
    # ----------------------------------------------------------------------
    # This function is run BEFORE Tor is started to get the best info
    get_info_direct() {
    local url="$1"
    local cookie_flags="$2"
    local download_audio_choice="$3"
    
    local filename_template=""
    local format_string=""
    
    if [[ "$download_audio_choice" =~ ^[Yy]$ ]]; then
    # For audio, we look for the best audio stream and set filename to mp3
    filename_template='%(title)s.mp3'
    format_string="bestaudio[ext=m4a]/bestaudio"
    else
    # For video, we look for the best video/audio streams and set filename to mp4
    filename_template='%(title)s.mp4'
    # Resilient format string used to determine the final filename extension
    format_string="bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best"
    fi
    
    local original_filename
    # Use yt-dlp to output the determined filename using the best format string
    original_filename=$(yt-dlp --get-filename -o "$filename_template" -f "$format_string" $cookie_flags "$url" 2>/dev/null)
    original_filename=$(echo "$original_filename" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
    
    if [ -z "$original_filename" ]; then
    echo "Error: Could not determine original filename via direct connection." >&2
    return 1
    fi
    
    # Returning only the filename. The download command uses the static, resilient format string.
    echo "$original_filename"
    return 0
    }
    # ----------------------------------------------------------------------
    
    # ----------------------------------------------------------------------
    # --- Function: Download and Play from a Playlist File ([l] logic) ---
    # ----------------------------------------------------------------------
    
    download_and_play_playlist() {
    read -p "Enter the name of the text file with the links: " file_name
    
    if [ ! -f "$file_name" ]; then
    echo "Error: File '$file_name' not found."
    return
    fi
    
    local cookie_flags=""
    local extra_options=""
    
    echo "$SEPARATOR" | lolcat
    
    # Use consolidated option getter (Tor is global, will be available)
    get_user_options "cookie_flags" "extra_options"
    
    local tor_enabled="n"
    local tor_aggressive="n"
    if [ "$TOR_STARTED_BY_SCRIPT" -eq 1 ] || pgrep -x "tor" > /dev/null; then
    tor_enabled="y"
    tor_aggressive="$TOR_AGGRESSIVE_MODE"
    fi
    # -----------------------------------
    
    mkdir -p playlist
    
    echo "Starting download and play session from '$file_name'..."
    
    while IFS= read -r url; do
    if [ -n "$url" ]; then
    echo "$SEPARATOR" | lolcat
    echo "Processing video from URL: $url"
    
    # 1. Get info directly
    local info
    info=$(get_info_direct "$url" "$cookie_flags" "n")
    local status=$?
    
    if [ $status -ne 0 ]; then
    echo "Warning: Could not determine final filename. Skipping URL."
    continue
    fi
    
    # Use the command builder, passing all options
    YTDLP_CMD=$(build_yt_dlp_command "$url" "playlist/%(title)s.%(ext)s" "n" "$extra_options" "" "$cookie_flags" "$tor_enabled" "$tor_aggressive")
    
    echo "Executing: $YTDLP_CMD"
    eval "$YTDLP_CMD"
    
    if [ $? -eq 0 ]; then
    echo "Download successful."
    
    # Finding the downloaded file based on modification time
    video_file=$(find playlist -type f -mtime -1m -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1 | cut -d' ' -f2-)
    
    if [ -n "$video_file" ] && [ -f "$video_file" ]; then
    echo "Playing: $(basename "$video_file")"
    if command -v "smplayer" &> /dev/null; then
    smplayer -close-after-media-ended "$video_file"
    elif command -v "mpv" &> /dev/null; then
    mpv "$video_file"
    fi
    
    read -p "Finished playing. Do you want to delete this video and remove the link from the file? (y/n) " delete_choice
    if [[ "$delete_choice" =~ ^[Yy]$ ]]; then
    rm "$video_file"
    # Safely remove the URL line from the file
    sed -i.bak "\@^$url$@d" "$file_name"
    echo "Deleted video and removed link from $file_name."
    else
    echo "Skipped deletion and link removal."
    fi
    else
    echo "Could not reliably find the downloaded video file."
    fi
    else
    echo "Download failed for URL: $url"
    fi
    fi
    done < "$file_name"
    
    # Stop Tor after the playlist finishes
    stop_tor
    
    echo "$SEPARATOR" | lolcat
    echo "Playlist session complete."
    }
    
    add_url_to_playlist() {
    read -p "Please paste the YouTube URL you want to add to a playlist: " url
    if [[ -z "$url" ]]; then
    type_text "No URL provided. Exiting."
    return
    fi
    
    LAST_PLAYLIST_FILE="playlist.txt"
    if [ -f "$CONFIG_FILE" ]; then
    LAST_PLAYLIST_FILE=$(grep "^LAST_PLAYLIST_FILE=" "$CONFIG_FILE" | cut -d'=' -f2-)
    if [ -z "$LAST_PLAYLIST_FILE" ]; then
    LAST_PLAYLIST_FILE="playlist.txt"
    fi
    fi
    
    read -p "Enter the name of the playlist file to add the link to (default: $LAST_PLAYLIST_FILE): " playlist_file_input
    
    if [ -z "$playlist_file_input" ]; then
    playlist_file="$LAST_PLAYLIST_FILE"
    else
    playlist_file="$playlist_file_input"
    fi
    
    echo "$url" >> "$playlist_file"
    type_text "URL added to $playlist_file"
    
    echo "LAST_PLAYLIST_FILE=$playlist_file" > "$CONFIG_FILE"
    }
    
    # ----------------------------------------------------------------------
    # --- Download, Play, and Add to Playlist ([a] Logic) ---
    # ----------------------------------------------------------------------
    download_play_and_add_to_playlist() {
    local temp_filename="taksshack.com.mp4"
    local MAX_RETRIES=10
    local RETRY_DELAY=10
    
    if ! command -v "mpv" &> /dev/null; then
    echo "Error: This option requires 'mpv' for playback. Please install it."
    return 1
    fi # <-- FIXED: This was the missing 'fi'
    
    # --- INPUT/COOKIE/TOR SECTION ---
    echo "$SEPARATOR" | lolcat
    read -p "Please paste the YouTube URL you want to download and add to a playlist: " url
    
    if [[ -z "$url" ]]; then
    echo "No URL provided. Aborting."
    return
    fi
    
    local cookie_flags=""
    local extra_options=""
    
    # Use consolidated option getter
    get_user_options "cookie_flags" "extra_options"
    
    local tor_enabled="n"
    local tor_aggressive="n"
    if [ "$TOR_STARTED_BY_SCRIPT" -eq 1 ] || pgrep -x "tor" > /dev/null; then
    tor_enabled="y"
    tor_aggressive="$TOR_AGGRESSIVE_MODE"
    fi
    # -------------------------------
    
    # 1. Get info directly
    local info
    info=$(get_info_direct "$url" "$cookie_flags" "n")
    local status=$?
    local original_filename=""
    
    if [ $status -eq 0 ]; then
    # original_filename is retrieved
    original_filename="$info"
    else
    echo "Error: Could not determine original filename. Aborting URL."
    stop_tor
    return
    fi
    
    # 2. Download Loop
    local RETRY_COUNT=1
    local DOWNLOAD_SUCCESS=0
    
    while [ $RETRY_COUNT -le $MAX_RETRIES ]; do
    echo "$SEPARATOR" | lolcat
    echo "--- Download Attempt $RETRY_COUNT of $MAX_RETRIES (Tor: $tor_enabled) ---"
    
    # Run the core download function, passing Tor flag and aggressive mode
    run_single_download "$url" "$temp_filename" "n" "n" "$extra_options" "$cookie_flags" "$tor_enabled" "$tor_aggressive"
    
    if [ $? -eq 0 ]; then
    DOWNLOAD_SUCCESS=1
    break
    else
    echo "Download failed. Waiting $RETRY_DELAY seconds before retrying..."
    sleep $RETRY_DELAY
    RETRY_COUNT=$((RETRY_COUNT + 1))
    fi
    done
    
    # Stop Tor once download attempts are complete
    stop_tor
    
    if [ $DOWNLOAD_SUCCESS -eq 1 ]; then
    # 3. Download successful: Rename, Play, and Add to Playlist
    
    if [ -f "$temp_filename" ]; then
    local final_video_file="$original_filename"
    
    mv "$temp_filename" "$final_video_file"
    
    echo "$SEPARATOR" | lolcat
    echo "Launching player (mpv) fullscreen and stretched for: $final_video_file"
    
    mpv --fullscreen --no-keepaspect --no-keepaspect-window "$final_video_file"
    
    echo "Playback session ended."
    
    # --- Add to Playlist Logic ---
    LAST_PLAYLIST_FILE="playlist.txt"
    if [ -f "$CONFIG_FILE" ]; then
    LAST_PLAYLIST_FILE=$(grep "^LAST_PLAYLIST_FILE=" "$CONFIG_FILE" | cut -d'=' -f2-)
    if [ -z "$LAST_PLAYLIST_FILE" ]; then
    LAST_PLAYLIST_FILE="playlist.txt"
    fi
    fi
    
    read -p "Enter the name of the playlist file to add the link to (default: $LAST_PLAYLIST_FILE): " playlist_file_input
    
    if [ -z "$playlist_file_input" ]; then
    playlist_file="$LAST_PLAYLIST_FILE"
    else
    playlist_file="$playlist_file_input"
    fi
    
    echo "$url" >> "$playlist_file"
    type_text "URL added to $playlist_file"
    echo "LAST_PLAYLIST_FILE=$playlist_file" > "$CONFIG_FILE"
    # -----------------------------
    
    read -p "Do you want to delete the downloaded file '$final_video_file' now? (y/n) " delete_choice
    if [[ "$delete_choice" =~ ^[Yy]$ ]]; then
    rm -f "$final_video_file"
    echo "File deleted."
    else
    echo "File saved as '$final_video_file'."
    fi
    
    else
    echo "Error: Download was reported successful but the final file could not be located."
    fi
    else
    echo "$SEPARATOR" | lolcat
    echo "Download failed after $MAX_RETRIES attempts. Cannot proceed to play or add to playlist."
    fi
    
    echo "$SEPARATOR" | lolcat
    echo "Operation complete."
    echo "$SEPARATOR" | lolcat
    }
    # ----------------------------------------------------------------------
    
    # ----------------------------------------------------------------------
    # --- Quick View / Live Stream Logic ([p] and [t]) ---
    # ----------------------------------------------------------------------
    # This function is now used for both [p] (quick D/P/D) and [t] (streaming)
    hybrid_stream_loop() {
    local temp_filename="taksshack.com.mp4"
    local MAX_RETRIES=10
    local RETRY_DELAY=10
    local requested_action="$1" # 'stream' or 'download'
    
    if ! command -v "mpv" &> /dev/null; then
    echo "Error: This option requires 'mpv' for playback. Please install it."
    return 1
    fi
    
    while true; do
    echo "$SEPARATOR" | lolcat
    read -p "Please paste the YouTube URL you want to process (or type 'q' to exit): " url
    
    if [[ "$url" =~ ^[Qq]$ ]]; then
    break
    elif [[ -z "$url" ]]; then
    echo "No URL provided. Please try again."
    continue
    fi
    
    # --- INPUT/COOKIE/TOR SECTION ---
    local cookie_flags=""
    local extra_options=""
    
    get_user_options "cookie_flags" "extra_options"
    
    local tor_enabled="n"
    local tor_aggressive="n"
    if [ "$TOR_STARTED_BY_SCRIPT" -eq 1 ] || pgrep -x "tor" > /dev/null; then
    tor_enabled="y"
    tor_aggressive="$TOR_AGGRESSIVE_MODE"
    fi
    # -------------------------------
    
    # ======================================================================
    # --- LIVE STREAMING LOGIC ([t] option) ---
    # ======================================================================
    if [[ "$requested_action" == "stream" ]]; then
    echo "Attempting to extract streaming URL and pipe directly to mpv (Bypassing download)..."
    
    # Use the Tor/Cookie flags if enabled. Use -f 'best' and -g to get the stream URL.
    local STREAM_CMD="yt-dlp -f 'best' -g $cookie_flags \"$url\" $extra_options"
    
    # Add Tor/Proxy if enabled.
    if [[ "$tor_enabled" =~ ^[Yy]$ ]]; then
    STREAM_CMD="$STREAM_CMD --proxy socks5://127.0.0.1:9050"
    
    if [[ "$tor_aggressive" == "n" ]]; then
    STREAM_CMD="$STREAM_CMD --extractor-args \"youtube:player-client=android\""
    echo "Using Reliable (Safe) mode for streaming (may limit quality)." >&2
    else
    echo "Using Aggressive (High-Quality) mode for streaming." >&2
    fi
    fi
    
    echo "Extraction Command: $STREAM_CMD"
    
    # Execute yt-dlp to get the URL, and pipe it directly to MPV
    local FINAL_STREAM_CMD="mpv --fullscreen --no-keepaspect --no-keepaspect-window \"\$($STREAM_CMD)\""
    
    echo "Launching player: $FINAL_STREAM_CMD"
    eval "$FINAL_STREAM_CMD"
    
    if [ $? -ne 0 ]; then
    echo "Error: Failed to extract stream URL or MPV failed to play the stream."
    fi
    
    stop_tor # Stop Tor after streaming is complete
    
    # ======================================================================
    # --- QUICK VIEW DOWNLOAD LOGIC ([p] option) ---
    # ======================================================================
    elif [[ "$requested_action" == "download" ]]; then
    echo "Starting download/play/delete cycle..."
    
    # 1. Get info directly
    local info
    info=$(get_info_direct "$url" "$cookie_flags" "n")
    local status=$?
    local original_filename=""
    
    if [ $status -eq 0 ]; then
    original_filename="$info"
    else
    echo "Error: Could not determine original filename. Aborting URL."
    stop_tor
    continue
    fi
    
    # 2. Download Loop with Retries
    local RETRY_COUNT=1
    local DOWNLOAD_SUCCESS=0
    
    while [ $RETRY_COUNT -le $MAX_RETRIES ]; do
    echo "$SEPARATOR" | lolcat
    echo "--- Download Attempt $RETRY_COUNT of $MAX_RETRIES (Tor: $tor_enabled) ---"
    
    # Run the core download function, passing Tor flag and aggressive mode
    run_single_download "$url" "$temp_filename" "n" "n" "$extra_options" "$cookie_flags" "$tor_enabled" "$tor_aggressive"
    
    if [ $? -eq 0 ]; then
    DOWNLOAD_SUCCESS=1
    break # Exit the retry loop on success
    else
    echo "Download failed. Waiting $RETRY_DELAY seconds before retrying..."
    sleep $RETRY_DELAY
    RETRY_COUNT=$((RETRY_COUNT + 1))
    fi
    done
    
    stop_tor # Stop Tor once download attempts are complete
    
    if [ $DOWNLOAD_SUCCESS -eq 1 ]; then
    # 3. Play and Delete
    if [ -f "$temp_filename" ]; then
    local final_video_file="$original_filename"
    mv "$temp_filename" "$final_video_file"
    
    echo "$SEPARATOR" | lolcat
    echo "Launching player (mpv) for: $final_video_file"
    mpv --fullscreen --no-keepaspect --no-keepaspect-window "$final_video_file"
    
    echo "Playback session ended. Auto-deleting file."
    rm -f "$final_video_file"
    echo "Temporary file deleted: $final_video_file"
    else
    echo "Error: Download was reported successful but the final file could not be located."
    rm -f "${temp_filename}.part"
    fi
    else
    echo "$SEPARATOR" | lolcat
    echo "Download failed after $MAX_RETRIES attempts."
    fi
    fi # End of Stream/Download choice
    
    echo "$SEPARATOR" | lolcat
    echo "Operation complete."
    echo "$SEPARATOR" | lolcat
    
    read -p "Do you want to process another URL? (y/n) " play_again_choice
    if [[ ! "$play_again_choice" =~ ^[Yy]$ ]]; then
    break
    fi
    done
    }
    
    # ----------------------------------------------------------------------
    # --- Single Download and Play/Save ([s] Logic) ---
    # ----------------------------------------------------------------------
    single_download_and_save() {
    read -p "Please paste the YouTube URL you want to download: " url
    
    # --- INPUT/COOKIE/TOR SECTION ---
    local cookie_flags=""
    local extra_options=""
    
    get_user_options "cookie_flags" "extra_options"
    
    local tor_enabled="n"
    local tor_aggressive="n"
    if [ "$TOR_STARTED_BY_SCRIPT" -eq 1 ] || pgrep -x "tor" > /dev/null; then
    tor_enabled="y"
    tor_aggressive="$TOR_AGGRESSIVE_MODE"
    fi
    # -------------------------------
    
    read -p "Do you want to download just the audio? (y/n) " download_audio_choice
    
    # 1. Get info directly
    local info
    info=$(get_info_direct "$url" "$cookie_flags" "$download_audio_choice")
    local status=$?
    local original_filename=""
    
    if [ $status -eq 0 ]; then
    original_filename="$info"
    else
    echo "Error: Could not determine original filename. Aborting URL."
    stop_tor
    return
    fi
    
    local temp_filename="taksshack.com.mp4"
    
    # Run the core download function in interactive mode ("y"), passing Tor flag and aggressive mode
    run_single_download "$url" "$temp_filename" "$download_audio_choice" "y" "$extra_options" "$cookie_flags" "$tor_enabled" "$tor_aggressive"
    
    # Stop Tor once download is complete
    stop_tor
    
    if [ $? -eq 0 ]; then
    # Download was successful
    
    read -p "Do you want to play the downloaded media? (y/n) " play_choice
    if [[ "$play_choice" =~ ^[Yy]$ ]]; then
    if command -v "smplayer" &> /dev/null; then
    smplayer "$temp_filename"
    elif command -v "mpv" &> /dev/null; then
    mpv "$temp_filename"
    else
    echo "Cannot play: Neither smplayer nor mpv found."
    fi
    fi
    
    # Rename the placeholder to the actual name/extension
    local final_file_name="$original_filename"
    mv "$temp_filename" "$final_file_name"
    temp_filename="$final_file_name" # Update the reference for cleanup
    
    read -p "Do you want to save the file as '$final_file_name'? (y/n) " save_choice
    if [[ "$save_choice" =~ ^[Yy]$ ]]; then
    echo "File saved as '$final_file_name'"
    else
    rm "$temp_filename"
    echo "File deleted."
    fi
    fi
    
    echo ""
    echo "$SEPARATOR" | lolcat
    echo "Operation complete."
    echo "$SEPARATOR" | lolcat
    }
    
    # ----------------------------------------------------------------------
    # --- Main Script Execution (FINAL MENU) ---
    # ----------------------------------------------------------------------
    
    type_text "Welcome to the yt-dlp interactive downloader!"
    echo "https://taksshack.com"
    echo ""
    
    echo "Please choose a single URL mode (Tor/Cookies optional for all):"
    echo " [s] Standard Download: Download, Play, and Choose to Save (Interactive)"
    echo " [a] Add to Queue: Download, Play, and Auto-Save URL to Playlist File"
    echo " [p] Quick View: Download, Auto-Play, and Auto-Delete"
    echo " [t] Live Stream: Stream directly with MPV (No Download/Save)"
    
    echo ""
    echo "Or choose a list/management mode:"
    echo " [l] Process List: Download, Play, and Manage URLs from a File (Playlist Mode)"
    echo " [d] Add URL: Quickly add a URL to a Playlist File (No Download)"
    
    read -p "Your choice (s/a/p/t/l/d): " main_choice
    
    if [[ "$main_choice" =~ ^[Ll]$ ]]; then
    download_and_play_playlist
    elif [[ "$main_choice" =~ ^[Aa]$ ]]; then
    download_play_and_add_to_playlist
    elif [[ "$main_choice" =~ ^[Pp]$ ]]; then
    hybrid_stream_loop "download"
    elif [[ "$main_choice" =~ ^[Tt]$ ]]; then
    hybrid_stream_loop "stream"
    elif [[ "$main_choice" =~ ^[Dd]$ ]]; then
    add_url_to_playlist
    elif [[ "$main_choice" =~ ^[Ss]$ ]]; then
    single_download_and_save
    fi
    • This reply was modified 3 months, 3 weeks ago by thumbtak. Reason: Fixed option A
    • This reply was modified 3 months, 3 weeks ago by thumbtak. Reason: Tor Option added
    • This reply was modified 3 months, 3 weeks ago by thumbtak. Reason: Stream option added and improved tor network video quality, by adding a new option
    • This reply was modified 3 months, 3 weeks ago by thumbtak. Reason: Improved menu structure
    #8262
    thumbtak
    Moderator

    We went back to a She Bang file as it is better for this task. The Python file was causing issues. Below is the updated script.

    Script update: Save as an SH file and open with $ bash file.sh

    #!/bin/bash
    
    # ASCII Art Functions
    # Function to print the main ASCII art banner for the script.
    print_banner() {
    echo "+---------------------------------+"
    echo "|===========TAKS SHACK============|"
    echo "|======https://taksshack.com======|"
    echo "+---------------------------------+"
    
    }
    
    # Function to print a section header with ASCII art.
    # Takes the section title as an argument.
    print_section_header() {
    echo "---=[ $@ ]=---------------------------------------------------"
    echo ""
    }
    
    # --- Configuration ---
    # URL to download the latest yt-dlp binary (Linux/macOS)
    YTDLP_URL="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp"
    # Local path where yt-dlp will be saved and executed from
    YTDLP_BIN="./yt-dlp"
    # Name of the temporary Python script that will handle the download, play, and delete logic
    PYTHON_SCRIPT="yt_dlp_player.py"
    # Base name for the downloaded video file (e.g., "downloaded_video.mp4")
    # yt-dlp will append the correct extension.
    OUTPUT_BASENAME="downloaded_video"
    # File to store the last used save folder
    LAST_SAVE_FOLDER_FILE=".last_save_folder"
    # Flag file to indicate if audio tools were installed by this script
    AUDIO_TOOLS_INSTALLED_FLAG=".audio_tools_installed_by_script_flag"
    # Flag file to indicate if ffmpeg was installed by this script
    FFMPEG_INSTALLED_FLAG=".ffmpeg_installed_by_script_flag"
    # File for the playlist
    PLAYLIST_FILE="video_playlist.txt"
    
    # --- Main Script Execution ---
    
    print_banner
    print_section_header "SYSTEM SETUP"
    
    # Step 1: Download yt-dlp if it doesn't exist or isn't executable
    if [ ! -f "$YTDLP_BIN" ] || [ ! -x "$YTDLP_BIN" ]; then
    echo " yt-dlp binary not found or not executable. Attempting to download..."
    if command -v curl &> /dev/null; then
    echo " Using 'curl' to download yt-dlp..."
    curl -L "$YTDLP_URL" -o "$YTDLP_BIN"
    elif command -v wget &> /dev/null; then
    echo " Using 'wget' to download yt-dlp..."
    wget -O "$YTDLP_BIN" "$YTDLP_URL"
    else
    echo " Error: Neither 'curl' nor 'wget' found. Please install one of them to download yt-dlp."
    echo " Exiting script."
    exit 1
    fi
    
    if [ $? -eq 0 ]; then
    chmod +x "$YTDLP_BIN"
    echo " yt-dlp downloaded and made executable."
    else
    echo " Error: Failed to download yt-dlp. Please check your internet connection or the URL."
    echo " Exiting script."
    exit 1
    fi
    else
    echo " yt-dlp binary already exists and is executable. Skipping download."
    fi
    
    # Step 2: Check and install espeak-ng, aplay, and ffmpeg if not present
    ESPEAK_NG_INSTALLED=false
    APLAY_INSTALLED=false
    FFMPEG_INSTALLED=false
    
    if command -v espeak-ng &> /dev/null; then
    ESPEAK_NG_INSTALLED=true
    echo " espeak-ng is already installed."
    else
    echo " espeak-ng is NOT found."
    fi
    
    if command -v aplay &> /dev/null; then
    APLAY_INSTALLED=true
    echo " aplay is already installed."
    else
    echo " aplay is NOT found."
    fi
    
    if command -v ffmpeg &> /dev/null; then
    FFMPEG_INSTALLED=true
    echo " ffmpeg is already installed."
    else
    echo " ffmpeg is NOT found. It is required for merging video and audio."
    fi
    
    # If any critical tool is missing, offer to install
    if [ "$ESPEAK_NG_INSTALLED" = false ] || [ "$APLAY_INSTALLED" = false ] || [ "$FFMPEG_INSTALLED" = false ]; then
    read -p " Some required tools (espeak-ng, aplay, ffmpeg) are missing. Do you want to install them? (y/n): " install_tools_choice
    if [[ "$install_tools_choice" =~ ^[Yy]$ ]]; then
    echo " Attempting to install required tools..."
    INSTALL_CMD=""
    if command -v apt &> /dev/null; then
    INSTALL_CMD="sudo apt install -y espeak-ng alsa-utils ffmpeg"
    elif command -v dnf &> /dev/null; then
    INSTALL_CMD="sudo dnf install -y espeak-ng alsa-utils ffmpeg"
    elif command -v pacman &> /dev/null; then
    INSTALL_CMD="sudo pacman -S --noconfirm espeak-ng alsa-utils ffmpeg"
    else
    echo " Error: No supported package manager (apt, dnf, pacman) found for installing tools."
    echo " Please install espeak-ng, alsa-utils, and ffmpeg manually."
    fi
    
    if [ -n "$INSTALL_CMD" ]; then
    if eval "$INSTALL_CMD"; then
    echo " Required tools installed successfully."
    touch "$AUDIO_TOOLS_INSTALLED_FLAG"
    touch "$FFMPEG_INSTALLED_FLAG"
    else
    echo " Error: Failed to install required tools. Please check permissions or internet connection."
    fi
    fi
    else
    echo " Skipping installation of missing tools. Script functionality may be limited or fail."
    # If ffmpeg wasn't installed, exit because it's critical for merging.
    if [ "$FFMPEG_INSTALLED" = false ]; then
    echo " ffmpeg is critical for downloading videos with sound. Exiting."
    exit 1
    fi
    fi
    fi
    
    echo ""
    
    print_section_header "PYTHON SCRIPT CREATION"
    # Step 3: Create the Python script dynamically
    echo " Creating temporary Python script: $PYTHON_SCRIPT"
    cat <<'EOF' > "$PYTHON_SCRIPT"
    import subprocess
    import os
    import sys
    import glob
    import re
    import time
    import shutil
    
    # Path to the downloaded yt-dlp binary (relative to where the shell script runs)
    YTDLP_PATH = "./yt-dlp"
    # Base name for the downloaded video file
    OUTPUT_BASENAME = "downloaded_video"
    # File to store the last used save folder (Python will now read/write this directly)
    LAST_SAVE_FOLDER_FILE = ".last_save_folder"
    # File for the playlist
    PLAYLIST_FILE = "video_playlist.txt"
    # Temporary WAV file for the test sound
    TEST_SOUND_FILE = "taks_shack_test_sound.wav"
    
    # Exit code signaling to the bash script to uninstall audio tools and clean up last save folder
    UNINSTALL_AUDIO_TOOLS_EXIT_CODE = 5
    
    # Regex to find percentage in yt-dlp download lines
    PROGRESS_RE = re.compile(r'\[download\]\s+(\d+\.?\d*)%')
    
    # --- Utility Functions ---
    
    def print_ascii_line(char='-', length=60):
    """Prints a line of ASCII characters."""
    print(char * length)
    
    def print_ascii_header(text, char='='):
    """Prints a header with ASCII art."""
    print_ascii_line(char)
    print(f" {text}")
    print_ascii_line(char)
    print("") # Add a newline for spacing
    
    def draw_ascii_progress_bar(percentage, bar_length=40):
    """
    Draws an ASCII progress bar for the download.
    Updates the same line in the terminal using carriage return.
    """
    filled_len = int(bar_length * percentage // 100)
    bar = '#' * filled_len + '-' * (bar_length - filled_len)
    sys.stdout.write(f'\rDownloading: [ {bar} ] {percentage:6.2f}%') # Fixed width for percentage
    sys.stdout.flush()
    
    def play_test_sound():
    """
    Generates and plays a small test sound using espeak-ng and aplay.
    """
    print_ascii_header("AUDIO TEST", '-')
    test_text = "Initiating video playback. Stand by."
    
    # Check if espeak-ng and aplay are available before attempting to play
    if not (subprocess.run(["which", "espeak-ng"], capture_output=True).returncode == 0 and \
    subprocess.run(["which", "aplay"], capture_output=True).returncode == 0):
    print(" Skipping audio test: espeak-ng or aplay not found (or not in PATH).")
    print_ascii_line('=')
    return
    
    try:
    # Generate the WAV file
    print(f" Generating test sound: '{test_text}'...")
    subprocess.run(["espeak-ng", "-w", TEST_SOUND_FILE, test_text], check=True, capture_output=True)
    
    # Play the WAV file
    print(f" Playing test sound from {TEST_SOUND_FILE}...")
    subprocess.run(["aplay", TEST_SOUND_FILE], check=True, capture_output=True)
    print(" Test sound played successfully.")
    except FileNotFoundError as e:
    print(f" Warning: Audio test tools not found. {e.strerror}: '{e.filename}'.")
    print(" This should have been caught by the main bash script. Audio wake-up may be unavailable.")
    except subprocess.CalledProcessError as e:
    print(f" Warning: Failed to generate or play test sound. Error: {e.stderr.decode().strip()}")
    except Exception as e:
    print(f" An unexpected error occurred during audio test: {e}")
    finally:
    # Clean up the temporary sound file
    if os.path.exists(TEST_SOUND_FILE):
    os.remove(TEST_SOUND_FILE)
    print_ascii_line('=') # Separator line
    
    def get_playlist_links():
    """Reads the playlist file and returns a list of video links."""
    links = []
    if os.path.exists(PLAYLIST_FILE):
    try:
    with open(PLAYLIST_FILE, 'r') as f:
    # Use strip to clean up whitespace and ensure no empty lines are added
    links = [line.strip() for line in f if line.strip()]
    except Exception as e:
    print(f"Warning: Could not read playlist file '{PLAYLIST_FILE}': {e}")
    return links
    
    def update_playlist_file(links):
    """Writes the current list of links back to the playlist file."""
    try:
    # 'w' mode truncates the file and writes the new content
    with open(PLAYLIST_FILE, 'w') as f:
    f.write('\n'.join(links) + '\n')
    return True
    except Exception as e:
    print(f"Error: Could not rewrite playlist file '{PLAYLIST_FILE}': {e}")
    return False
    
    def add_to_playlist(youtube_link):
    """Checks for duplicates and appends a link to the playlist file if unique."""
    links = get_playlist_links()
    
    # Check for duplicates
    if youtube_link in links:
    print(f"\n Link already exists in the playlist. Skipping addition.")
    return False
    
    # Append the new link
    links.append(youtube_link)
    
    if update_playlist_file(links):
    print(f"\n Link successfully added to the end of the playlist: {PLAYLIST_FILE}")
    return True
    return False
    
    def remove_first_from_playlist():
    """Removes the first link from the playlist file and re-writes the file."""
    links = get_playlist_links()
    
    if not links:
    return None
    
    # Remove the first item
    removed_link = links.pop(0)
    
    if update_playlist_file(links):
    print(f"\n Link successfully removed from the top of the playlist.")
    return removed_link
    return None # Indicate failure
    
    def run_yt_dlp(youtube_link, cookie_option=None, is_browser_option=False):
    """
    Attempts to download a video using yt-dlp with optional cookies.
    Prints download progress to stdout using a custom ASCII bar.
    Returns (success_boolean, stderr_output, video_title_suggestion).
    """
    # Use --get-filename to preview the filename yt-dlp would use
    # Also use --get-title to get the actual title from YouTube
    info_command = [
    YTDLP_PATH,
    '--get-title',
    '--print', '%(title)s', # Get title
    '--print', '%(id)s.%(ext)s', # Get filename suggestion
    youtube_link
    ]
    if cookie_option:
    if is_browser_option:
    info_command.extend(['--cookies-from-browser', cookie_option])
    else:
    expanded_cookies_path = os.path.expanduser(cookie_option)
    info_command.extend(['--cookies', expanded_cookies_path])
    
    video_title = None
    suggested_filename = None
    try:
    info_process = subprocess.run(info_command, capture_output=True, text=True, check=True)
    # Assuming yt-dlp prints title on first line, filename on second
    info_lines = info_process.stdout.strip().split('\n')
    if len(info_lines) >= 2:
    video_title = info_lines[0].strip()
    # Sanitize the title for use as a filename
    suggested_filename = re.sub(r'[\\/:*?"<>|]', '_', video_title)
    # Remove leading/trailing spaces, and ensure it's not empty
    suggested_filename = suggested_filename.strip()
    if not suggested_filename:
    suggested_filename = "youtube_video" # Fallback if title is empty after sanitization
    
    # Append a generic extension for the prompt, actual extension will be handled by yt-dlp
    suggested_filename += ".mp4"
    else:
    print(f"Warning: Could not get full video info. Output: {info_process.stdout.strip()}")
    except subprocess.CalledProcessError as e:
    print(f"Error getting video info: {e.stderr}")
    except Exception as e:
    print(f"An unexpected error occurred while getting video info: {e}")
    
    download_command = [
    YTDLP_PATH,
    # Prioritize separate best video and audio, then merge. Fallback to best overall mp4, then just best.
    '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
    '--merge-output-format', 'mp4', # Merge audio and video into an MP4 container.
    '--output', f"{OUTPUT_BASENAME}.%(ext)s",
    '--sponsorblock-remove', 'sponsor',
    youtube_link
    ]
    
    # Add cookies option based on whether it's a browser or a file path
    if cookie_option:
    if is_browser_option:
    download_command.extend(['--cookies-from-browser', cookie_option])
    else:
    # Expand user's home directory (e.g., '~/.config/cookies.txt')
    expanded_cookies_path = os.path.expanduser(cookie_option)
    download_command.extend(['--cookies', expanded_cookies_path])
    
    stderr_output = ""
    try:
    # Use subprocess.Popen to stream output in real-time
    process = subprocess.Popen(
    download_command,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT, # Direct stderr to stdout for real-time reading
    text=True, # Decode stdout/stderr as text
    bufsize=1 # Line-buffered output for real-time printing
    )
    
    is_download_progress_active = False
    # Read stdout line by line
    for line in iter(process.stdout.readline, ''):
    # Check if the line is a progress update from yt-dlp
    match = PROGRESS_RE.search(line)
    if match:
    is_download_progress_active = True
    percentage = float(match.group(1))
    draw_ascii_progress_bar(percentage)
    else:
    if is_download_progress_active:
    sys.stdout.write('\n') # Move to next line after progress bar
    is_download_progress_active = False # Reset flag after progress bar is finalized by a non-progress line
    sys.stdout.write(line) # Print other yt-dlp output directly
    sys.stdout.flush()
    
    # After the loop, if a progress bar was the last thing printed, ensure a newline
    if is_download_progress_active:
    sys.stdout.write('\n')
    
    # Wait for the subprocess to complete and get its return code
    return_code = process.wait()
    return return_code == 0, stderr_output, suggested_filename
    except FileNotFoundError:
    return False, f"Error: yt-dlp binary not found at '{YTDLP_PATH}'. Ensure it's downloaded and executable.", suggested_filename
    except Exception as e:
    return False, f"An unexpected error occurred during yt-dlp execution: {e}", suggested_filename
    
    def find_package_manager_install_command(package_name):
    """
    Tries to find a supported package manager and returns the installation command list.
    """
    if subprocess.run(["which", "apt"], capture_output=True).returncode == 0:
    return ["sudo", "apt", "install", "-y", package_name]
    elif subprocess.run(["which", "dnf"], capture_output=True).returncode == 0:
    return ["sudo", "dnf", "install", "-y", package_name]
    elif subprocess.run(["which", "pacman"], capture_output=True).returncode == 0:
    return ["sudo", "pacman", "-S", "--noconfirm", package_name]
    return None
    
    def _get_player_and_folder():
    """Performs pre-checks and returns the determined media player and last save folder."""
    
    # 1. Read last_save_folder
    last_save_folder = ""
    if os.path.exists(LAST_SAVE_FOLDER_FILE):
    try:
    with open(LAST_SAVE_FOLDER_FILE, 'r') as f:
    last_save_folder = f.read().strip()
    except Exception as e:
    print(f"Warning: Could not read last save folder file '{LAST_SAVE_FOLDER_FILE}': {e}")
    last_save_folder = "" # Reset if read fails
    
    print_ascii_header("PYTHON DEPENDENCY CHECKS", '-')
    
    # 2. Check for a suitable media player
    media_player = None
    mpv_available = subprocess.run(["which", "mpv"], capture_output=True).returncode == 0
    vlc_available = subprocess.run(["which", "vlc"], capture_output=True).returncode == 0
    smplayer_available = subprocess.run(["which", "smplayer"], capture_output=True).returncode == 0
    
    sys.stdout.write(" Checking for media player (mpv, vlc, smplayer)...")
    sys.stdout.flush()
    
    if mpv_available:
    media_player = "mpv"
    print(f" [OK: Using {media_player}]")
    elif vlc_available or smplayer_available:
    print(" [FAILED to find mpv, alternatives found]")
    print_ascii_line('=')
    
    print_ascii_header("MPV MISSING - ACTION REQUIRED", '#')
    
    alt_options = []
    if vlc_available: alt_options.append("'v' for VLC")
    if smplayer_available: alt_options.append("'s' for SMPlayer")
    
    choice_prompt = (
    " The preferred player 'mpv' was not found.\n"
    " Do you want to try installing 'mpv' now (requires sudo) or use an alternative player?\n"
    f" (Type 'i' for install, {' or '.join(alt_options)}, 'e' to exit): "
    )
    
    install_choice = input(choice_prompt).lower().strip()
    
    if install_choice == 'i':
    install_cmd = find_package_manager_install_command("mpv")
    
    if install_cmd:
    try:
    print(f" Attempting to run: {' '.join(install_cmd)}")
    subprocess.run(install_cmd, check=True)
    print(" mpv installed successfully. Using mpv.")
    media_player = "mpv"
    except subprocess.CalledProcessError:
    print(" Failed to install mpv. Falling back to alternative player.")
    except FileNotFoundError:
    print(" Failed to run installation command (sudo not found or similar). Falling back.")
    else:
    print(" No supported package manager (apt, dnf, pacman) found for installation. Falling back to alternative player.")
    
    if not media_player:
    if install_choice == 'v' and vlc_available:
    media_player = "vlc"
    elif install_choice == 's' and smplayer_available:
    media_player = "smplayer"
    else:
    if vlc_available: media_player = "vlc"
    elif smplayer_available: media_player = "smplayer"
    
    if not media_player:
    print(" No valid player selected or available. Exiting.")
    sys.exit(1)
    
    print(f" Using player: {media_player}")
    print_ascii_line('=')
    
    else:
    # NO players are available (mpv, vlc, or smplayer)
    print(" [FAILED]")
    print(" Error: No compatible media player ('mpv', 'vlc', or 'smplayer') found in your PATH.")
    print(" Please install one of these players to proceed.")
    sys.exit(1) # Exit if no player is found
    
    # 3. Check for yt-dlp binary
    sys.stdout.write(f" Checking for yt-dlp at '{YTDLP_PATH}'...")
    sys.stdout.flush()
    if not os.path.exists(YTDLP_PATH) or not os.access(YTDLP_PATH, os.X_OK):
    print(" [FAILED]")
    print(f" Error: yt-dlp not found or not executable at '{YTDLP_PATH}'.")
    sys.exit(1)
    print(" [OK]")
    
    # 4. Check for ffmpeg.
    sys.stdout.write(" Checking for ffmpeg...")
    sys.stdout.flush()
    if subprocess.run(["which", "ffmpeg"], capture_output=True).returncode != 0:
    print(" [FAILED]")
    print(" Error: 'ffmpeg' is not found in your system's PATH.")
    sys.exit(1)
    print(" [OK]")
    
    print_ascii_line('=')
    return media_player, last_save_folder
    
    def _process_link_workflow(youtube_link, media_player, last_save_folder, ontop_choice='n', monitor_choice=None):
    """
    Handles the download, play, and optional save/delete for a single video link.
    Returns True if successfully downloaded/played/handled, False otherwise.
    """
    final_video_file = None
    suggested_filename_for_save = None
    
    try:
    print_ascii_header(f"PROCESSING: {youtube_link}", '=')
    
    download_attempt_successful = False
    
    # --- Video Download Attempt (with optional cookie retry) ---
    print("\n Attempting to download video with best video and separate audio streams...")
    success, _, suggested_filename_for_save = run_yt_dlp(youtube_link)
    
    if success:
    download_attempt_successful = True
    print("\n Video and audio downloaded and merged successfully.")
    else:
    print("\n Initial video download failed.")
    
    # --- Cookie Retry Logic ---
    retry_choice = input(
    "\n The download failed. This can happen if the video is private or age-restricted. "
    "Do you want to try again with browser cookies? (y/n): "
    ).lower()
    
    if retry_choice == 'y':
    cookie_method_choice = input(
    " How do you want to provide cookies?\n"
    " 1. From a browser (e.g., firefox, chrome)\n"
    " 2. From a cookies.txt file\n"
    " Enter 1 or 2: "
    ).strip()
    
    cookie_option_value = None
    is_browser = False
    
    if cookie_method_choice == '1':
    browser_options = {
    1: "firefox", 2: "chrome", 3: "chromium",
    4: "brave", 5: "edge", 6: "opera",
    7: "safari", 8: "vivaldi", 9: "librewolf"
    }
    print("\n Select a browser for cookies:")
    for num, browser in browser_options.items():
    print(f" {num}. {browser.capitalize()}")
    
    browser_selection_input = input(" Enter the number of your preferred browser: ").strip()
    try:
    browser_selection_num = int(browser_selection_input)
    if browser_selection_num in browser_options:
    browser_name = browser_options[browser_selection_num]
    cookie_option_value = browser_name
    is_browser = True
    print(f"\n Attempting to download video using cookies from {browser_name}...")
    else:
    print(" Invalid browser number. Falling back to cookies.txt file option.")
    cookie_method_choice = '2'
    except ValueError:
    print(" Invalid input. Please enter a number. Falling back to cookies.txt file option.")
    cookie_method_choice = '2'
    
    if cookie_method_choice == '2' or (cookie_method_choice == '1' and not is_browser):
    cookies_file_path = input(
    " Please enter the path to the cookies file "
    "(e.g., ~/.config/yt-dlp/cookies.txt or cookies.txt in current directory): "
    ).strip()
    if cookies_file_path:
    cookie_option_value = cookies_file_path
    is_browser = False
    print("\n Attempting to download video with cookies from file...")
    else:
    print(" No cookies file path provided. Cannot retry with cookies.")
    
    if cookie_option_value:
    success_retry, _, suggested_filename_for_save = run_yt_dlp(youtube_link, cookie_option_value, is_browser)
    
    if success_retry:
    download_attempt_successful = True
    print("\n Video and audio downloaded and merged successfully with cookies.")
    else:
    print(" Video download failed even with cookies.")
    else:
    print(" No valid cookie option provided. Cannot retry with cookies.")
    else:
    print(" Not retrying. Returning to main menu.")
    
    if not download_attempt_successful:
    print("\n Failed to download video. Returning to main menu.")
    return False
    
    # --- Find Downloaded File ---
    print_ascii_header("LOCATING VIDEO", '-')
    sys.stdout.write(" Searching for downloaded video file...")
    sys.stdout.flush()
    downloaded_files = glob.glob(f"{OUTPUT_BASENAME}.*")
    
    for f in downloaded_files:
    if f.startswith(OUTPUT_BASENAME) and (f.endswith(".mp4") or f.endswith(".webm") or f.endswith(".mkv")):
    final_video_file = f
    break
    
    if not final_video_file:
    print(" [NOT FOUND]")
    print(f" Error: Could not find a video file matching '{OUTPUT_BASENAME}.*' after download and merge.")
    return False
    
    print(" [FOUND]")
    print(f" Identified downloaded video file: {final_video_file}")
    print_ascii_line('=')
    
    # --- Play Test Sound before Video ---
    play_test_sound()
    
    # --- Play Video ---
    print_ascii_header("PLAYING VIDEO", '-')
    player_command = [media_player, final_video_file]
    
    if media_player == "mpv":
    # Base command for mpv: fullscreen and ignore aspect ratio
    player_command = [media_player, '--fs', '--no-keepaspect', final_video_file]
    
    if ontop_choice == 'y':
    # Insert --ontop flag after the player name
    player_command.insert(1, '--ontop')
    
    # NEW: Add monitor selection if provided
    if monitor_choice is not None:
    player_command.insert(1, f'--screen={monitor_choice}')
    print(f" MPV will attempt to play on monitor index: {monitor_choice}")
    
    elif media_player == "vlc":
    # VLC fullscreen/aspect ratio control is less common via CLI, but can be done.
    # Sticking to simple play command for cross-platform robustness.
    player_command = [media_player, '--fullscreen', final_video_file]
    if ontop_choice == 'y':
    # VLC has no direct --ontop flag from CLI, need to rely on settings or other methods.
    print(" Note: VLC does not support '--ontop' from command line; ignoring option.")
    if monitor_choice is not None:
    # VLC has no direct --screen flag from CLI, ignoring option.
    print(" Note: VLC does not support '--screen' from command line; ignoring option.")
    
    elif media_player == "smplayer":
    # SMPlayer handles its own settings for full screen/always on top
    player_command = [media_player, final_video_file]
    if ontop_choice == 'y':
    # SMPlayer also doesn't have a standardized CLI ontop flag, relying on its internal settings.
    print(" Note: SMPlayer does not support '--ontop' from command line; ignoring option.")
    if monitor_choice is not None:
    # SMPlayer also doesn't have a standardized CLI screen flag, ignoring option.
    print(" Note: SMPlayer does not support monitor selection from command line; ignoring option.")
    
    print(f" Playing video with {media_player}: {final_video_file}")
    # Execute the detected media player
    subprocess.run(player_command, check=True)
    print(" Video playback finished.")
    print_ascii_line('=')
    
    # --- Save Video Logic ---
    print_ascii_header("SAVE VIDEO", '-')
    save_choice = input(f" Do you want to save this video (current name: '{suggested_filename_for_save if suggested_filename_for_save else final_video_file}')? (y/n): ").lower()
    
    if save_choice == 'y':
    # Ask for new filename, default to YouTube's title
    new_filename = input(f" Enter the desired filename (default: '{suggested_filename_for_save}'): ").strip()
    if not new_filename:
    new_filename = suggested_filename_for_save
    
    # Ensure the new filename has an extension
    if not os.path.splitext(new_filename)[1]:
    original_ext = os.path.splitext(final_video_file)[1]
    new_filename += original_ext
    
    # Ask for save folder
    target_folder = ""
    if last_save_folder and os.path.isdir(last_save_folder):
    use_previous_folder = input(f" Use previous save folder '{last_save_folder}'? (y/n): ").lower()
    if use_previous_folder == 'y':
    target_folder = last_save_folder
    else:
    target_folder = input(" Enter the new save folder path: ").strip()
    else:
    target_folder = input(" Enter the save folder path: ").strip()
    
    if not target_folder:
    print(" No save folder specified. Video will not be saved.")
    os.remove(final_video_file)
    print(f" Deleted unsaved video: {final_video_file}")
    return True
    
    os.makedirs(target_folder, exist_ok=True)
    
    destination_path = os.path.join(target_folder, new_filename)
    try:
    shutil.move(final_video_file, destination_path)
    print(f" Video saved successfully to: {destination_path}")
    # Update last save folder file
    with open(LAST_SAVE_FOLDER_FILE, 'w') as f:
    f.write(target_folder)
    # NOTE: last_save_folder needs to be updated in the main function's scope too, but
    # for now, the main loop re-reads it for the next iteration.
    print(f" Last save folder updated to: {target_folder}")
    except OSError as e:
    print(f" Error saving video: {e}")
    print(f" The video was not moved. It remains as '{final_video_file}' in the current directory.")
    print_ascii_line('=')
    else:
    print(" Video will not be saved.")
    # --- Delete Downloaded Video if not saved ---
    if final_video_file and os.path.exists(final_video_file):
    try:
    os.remove(final_video_file)
    except Exception as e:
    print(f" Warning: Could not delete {final_video_file}. Reason: {e}")
    print_ascii_line('=')
    
    return True # Indicate successful handling of the video flow
    
    except FileNotFoundError as e:
    print_ascii_header("RUNTIME ERROR", '#')
    print(f" Runtime Error: A required command was not found. Detail: {e}")
    print(" Please ensure all necessary applications are installed and in your system's PATH.")
    if final_video_file and os.path.exists(final_video_file):
    try: os.remove(final_video_file)
    except Exception: pass
    return False
    except subprocess.CalledProcessError as e:
    print_ascii_header("EXECUTION ERROR", '#')
    print(f" Command Execution Error (Exit code: {e.returncode}):")
    print(f" Command: {' '.join(e.cmd)}")
    if e.stdout: print(f" Output: {e.stdout.strip()}")
    print(" Please review the error messages above for details on what went wrong.")
    if final_video_file and os.path.exists(final_video_file):
    try: os.remove(final_video_file)
    except Exception: pass
    return False
    except Exception as e:
    print_ascii_header("UNEXPECTED ERROR", '#')
    print(f" An unexpected error occurred: {e}")
    if final_video_file and os.path.exists(final_video_file):
    try: os.remove(final_video_file)
    except Exception: pass
    return False
    finally:
    # Aggressive cleanup of any residual 'downloaded_video.*' files that might be left over
    print_ascii_header("FINAL CLEANUP OF TEMP FILES", '-')
    print(" Checking for residual temporary files...")
    for f in glob.glob(f"{OUTPUT_BASENAME}.*"):
    if os.path.exists(f):
    try:
    os.remove(f)
    print(f" Cleaned up residual temporary file: {f}")
    except Exception as e:
    print(f" Warning: Could not clean up residual temporary file {f}. Reason: {e}")
    if os.path.exists(TEST_SOUND_FILE):
    try:
    os.remove(TEST_SOUND_FILE)
    print(f" Cleaned up temporary sound file: {TEST_SOUND_FILE}")
    except Exception as e:
    print(f" Warning: Could not clean up temporary sound file {TEST_SOUND_FILE}. Reason: {e}")
    print_ascii_line('=')
    
    def process_new_video(media_player, last_save_folder):
    """
    Handles the flow for a user-provided new video link.
    """
    print_ascii_header("NEW VIDEO LINK", '-')
    youtube_link = input(" Please enter the YouTube video link: ")
    if not youtube_link:
    print(" No link entered. Returning to main menu.")
    return False
    
    # 1. Ask about saving to playlist BEFORE download
    playlist_choice = input(" Do you want to save this link to the playlist before playing? (y/n): ").lower()
    
    if playlist_choice == 'y':
    add_to_playlist(youtube_link)
    
    # 2. Ask about playing AFTER playlist decision
    play_choice = input(" Do you want to download and play the video now? (y/n): ").lower()
    
    if play_choice == 'y':
    # 3. Ask about Playback Options BEFORE download (if mpv)
    ontop_choice = 'n' # Default to no
    monitor_choice = None # Default to no monitor specified
    
    if media_player == "mpv":
    print_ascii_header("PLAYBACK OPTIONS (MPV)", '-')
    
    # --- Always On Top Option ---
    ontop_choice = input(
    " Do you want the player window to be 'Always On Top'?\n"
    " (This keeps the video visible above all other windows.) (y/n): "
    ).lower()
    
    # --- Monitor Selection Option ---
    monitor_input = input(
    " Enter the **monitor number** (e.g., 0, 1, 2, 56, 4532, 4000000) you want to play on, or press Enter to skip: " # <-- UPDATED EXAMPLE
    ).strip()
    
    if monitor_input.isdigit():
    monitor_choice = int(monitor_input)
    elif monitor_input:
    print(" Warning: Invalid monitor number entered. Skipping monitor selection.")
    
    print_ascii_line('=')
    
    return _process_link_workflow(youtube_link, media_player, last_save_folder, ontop_choice, monitor_choice)
    else:
    print(" Skipping video download and playback. Returning to main menu.")
    return True # Handled successfully, but no video processed.
    
    def process_playlist_video(media_player, last_save_folder):
    """
    Handles the flow for playing videos from the playlist, including recursion for 'play next'.
    Returns True if video processing was completed, False otherwise.
    """
    
    playlist_links = get_playlist_links()
    
    if not playlist_links:
    print_ascii_header("PLAYLIST EMPTY", '-')
    print(" The playlist is currently empty. Returning to main menu.")
    print_ascii_line('=')
    return False
    
    # Get the top link
    youtube_link = playlist_links[0]
    
    print_ascii_header(f"PLAYLIST - TOP VIDEO ({len(playlist_links)} links remaining)", '=')
    print(f" Video to play: {youtube_link}")
    
    # 1. Ask about Playback Options BEFORE download (if mpv)
    ontop_choice = 'n' # Default to no
    monitor_choice = None # Default to no monitor specified
    
    if media_player == "mpv":
    print_ascii_header("PLAYBACK OPTIONS (MPV)", '-')
    
    # --- Always On Top Option ---
    ontop_choice = input(
    " Do you want the player window to be 'Always On Top'?\n"
    " (This keeps the video visible above all other windows.) (y/n): "
    ).lower()
    
    # --- Monitor Selection Option ---
    monitor_input = input(
    " Enter the **monitor number** (e.g., 0, 1, 2, 56, 4532, 4000000) you want to play on, or press Enter to skip: " # <-- UPDATED EXAMPLE ).strip() if monitor_input.isdigit(): monitor_choice = int(monitor_input) elif monitor_input: print(" Warning: Invalid monitor number entered. Skipping monitor selection.") print_ascii_line('=') # Run the core download/play/save workflow video_processed = _process_link_workflow(youtube_link, media_player, last_save_folder, ontop_choice, monitor_choice) # After playback, handle the list cleanup if video_processed: # Ask to delete the link from the list delete_choice = input( f" Do you want to delete the played link from the playlist? (y/n): " ).lower() if delete_choice == 'y': # This call removes the link that was just played (the first one) removed = remove_first_from_playlist() if removed: playlist_links = get_playlist_links() # Re-read for the next prompt # Ask to play the next link if playlist_links: next_choice = input(f" There are {len(playlist_links)} links remaining. Do you want to play the next one? (y/n): ").lower() if next_choice == 'y': # Recursive call to play the next one (which is now at the top) return process_playlist_video(media_player, last_save_folder) else: print(" Playlist is now empty.") return video_processed def download_and_play_video(): """ The main control function that handles the menu loop. """ try: # 1. Perform pre-flight checks and get the necessary system info # This function handles the media player check and reads the last save folder media_player, last_save_folder = _get_player_and_folder() except SystemExit: # Catch SystemExit from checks if they fail return False # Main menu loop while True: print_ascii_header("MAIN MENU", '=') print(" 1. Download and play a NEW YouTube video link.") playlist_links = get_playlist_links() num_links = len(playlist_links) if num_links > 0:
    print(f" 2. Play the top video from the PLAYLIST ({num_links} links available).")
    else:
    print(" 2. Play from PLAYLIST (Playlist is empty).")
    
    print(" 3. Exit the program and clean up.")
    
    choice = input(" Enter your choice (1, 2, 3): ").strip()
    
    video_processed = False
    if choice == '1':
    video_processed = process_new_video(media_player, last_save_folder)
    elif choice == '2':
    # The playlist processing handles the inner loop (delete/play next)
    video_processed = process_playlist_video(media_player, last_save_folder)
    elif choice == '3':
    print(" Exiting. Goodbye!")
    sys.exit(UNINSTALL_AUDIO_TOOLS_EXIT_CODE)
    else:
    print(" Invalid choice. Please enter 1, 2, or 3.")
    
    # Update last_save_folder if a save operation was successful (for next loop iteration)
    if video_processed and os.path.exists(LAST_SAVE_FOLDER_FILE):
    try:
    with open(LAST_SAVE_FOLDER_FILE, 'r') as f:
    last_save_folder = f.read().strip()
    except Exception:
    pass # Ignore read errors
    
    print("\n" + "=" * 60) # Main menu separator
    # Loop continues to the start of the while True block for the next operation
    
    # Ensure the main function is called when the script is executed.
    if __name__ == "__main__":
    # Clean up residual files before the main process starts
    for f in glob.glob(f"{OUTPUT_BASENAME}.*"):
    if os.path.exists(f):
    try: os.remove(f)
    except Exception: pass
    if os.path.exists(TEST_SOUND_FILE):
    try: os.remove(TEST_SOUND_FILE)
    except Exception: pass
    
    download_and_play_video()
    EOF
    
    echo "" # Add a newline for better readability
    
    print_section_header "RUNNING PLAYER"
    # Step 4: Run the Python script
    echo " Executing Python script: $PYTHON_SCRIPT"
    # The Python script will now handle the loop and exit with a specific code when the user is done.
    python3 "$PYTHON_SCRIPT"
    PYTHON_EXIT_CODE=$? # Capture the exit code of the Python script
    
    echo "" # Add a newline for better readability
    
    print_section_header "FINAL CLEANUP"
    # Step 5: Clean up temporary files and potentially uninstall audio tools
    echo " Cleaning up shell script's temporary files..."
    
    # Remove the temporary Python script
    if [ -f "$PYTHON_SCRIPT" ]; then
    rm "$PYTHON_SCRIPT"
    echo " Removed temporary Python script: $PYTHON_SCRIPT"
    fi
    
    # Remove the yt-dlp binary as requested
    if [ -f "$YTDLP_BIN" ]; then
    rm "$YTDLP_BIN"
    echo " Removed yt-dlp binary: $YTDLP_BIN"
    fi
    
    # Condition for removing the last save folder file: only if Python script exited with 'no more videos' signal
    if [ $PYTHON_EXIT_CODE -eq 5 ] && [ -f "$LAST_SAVE_FOLDER_FILE" ]; then
    rm "$LAST_SAVE_FOLDER_FILE"
    echo " Removed last save folder file: $LAST_SAVE_FOLDER_FILE (as requested upon exit)"
    elif [ -f "$LAST_SAVE_FOLDER_FILE" ]; then
    echo " Keeping last save folder file: $LAST_SAVE_FOLDER_FILE (script did not exit via 'no more videos' option)"
    fi
    
    # Check if the Python script signaled for audio tool uninstallation
    # And if the flag file exists (meaning *this* script installed them)
    if [ $PYTHON_EXIT_CODE -eq 5 ]; then # Only consider uninstalling if Python script signaled end
    # --- NEW: Check and uninstall ffmpeg ---
    if [ -f "$FFMPEG_INSTALLED_FLAG" ]; then
    echo ""
    read -p " This script installed 'ffmpeg'. Do you want to uninstall it now? (y/n): " uninstall_ffmpeg_confirm
    if [[ "$uninstall_ffmpeg_confirm" =~ ^[Yy]$ ]]; then
    echo " Attempting to uninstall ffmpeg..."
    UNINSTALL_FFMPEG_CMD=""
    if command -v apt &> /dev/null; then
    UNINSTALL_FFMPEG_CMD="sudo apt remove -y ffmpeg"
    elif command -v dnf &> /dev/null; then
    UNINSTALL_FFMPEG_CMD="sudo dnf remove -y ffmpeg"
    elif command -v pacman &> /dev/null; then
    UNINSTALL_FFMPEG_CMD="sudo pacman -R --noconfirm ffmpeg"
    else
    echo " Error: No supported package manager found for uninstalling ffmpeg."
    echo " Please uninstall ffmpeg manually."
    fi
    
    if [ -n "$UNINSTALL_FFMPEG_CMD" ]; then
    if eval "$UNINSTALL_FFMPEG_CMD"; then
    echo " ffmpeg uninstalled successfully."
    rm "$FFMPEG_INSTALLED_FLAG" # Remove flag file
    else
    echo " Error: Failed to uninstall ffmpeg. Please check permissions."
    fi
    fi
    else
    echo " Skipping ffmpeg uninstallation."
    fi
    fi
    
    # --- Existing: Check and uninstall espeak-ng and alsa-utils ---
    if [ -f "$AUDIO_TOOLS_INSTALLED_FLAG" ]; then
    echo ""
    read -p " This script installed 'espeak-ng' and 'alsa-utils'. Do you want to uninstall them now? (y/n): " uninstall_audio_confirm
    if [[ "$uninstall_audio_confirm" =~ ^[Yy]$ ]]; then
    echo " Attempting to uninstall audio tools..."
    UNINSTALL_AUDIO_CMD=""
    if command -v apt &> /dev/null; then
    UNINSTALL_AUDIO_CMD="sudo apt remove -y espeak-ng alsa-utils"
    elif command -v dnf &> /dev/null; then
    UNINSTALL_AUDIO_CMD="sudo dnf remove -y espeak-ng alsa-utils"
    elif command -v pacman &> /dev/null; then
    # FIX: Corrected a typo in the previous version of the script which had /dev:wq/null
    UNINSTALL_AUDIO_CMD="sudo pacman -R --noconfirm espeak-ng alsa-utils"
    else
    echo " Error: No supported package manager found for uninstalling audio tools."
    echo " Please install espeak-ng and alsa-utils manually."
    fi
    
    if [ -n "$UNINSTALL_AUDIO_CMD" ]; then
    if eval "$UNINSTALL_AUDIO_CMD"; then
    echo " Audio tools uninstalled successfully."
    rm "$AUDIO_TOOLS_INSTALLED_FLAG" # Remove flag file
    else
    echo " Error: Failed to uninstall audio tools. Please check permissions."
    fi
    fi
    else
    echo " Skipping audio tools uninstallation."
    fi
    fi
    else # If Python script did not exit with code 5, do not offer uninstallation
    echo " Tool uninstallation not offered as script did not exit via 'no more videos' option or encountered an error."
    fi
    
    # Report final status based on the Python script's exit code
    if [ $PYTHON_EXIT_CODE -ne 0 ] && [ $PYTHON_EXIT_CODE -ne 5 ]; then
    echo " Script finished with errors (exit code: $PYTHON_EXIT_CODE)."
    exit $PYTHON_EXIT_CODE
    else
    echo " Script finished successfully."
    exit 0
    fi
    #8342
    thumbtak
    Moderator

    Bug fixes with line 813, line 29, line 28, and line 768.

    #!/bin/bash
    
    # ASCII Art Functions
    # Function to print the main ASCII art banner for the script.
    print_banner() {
    echo "+---------------------------------+"
    echo "|===========TAKS SHACK============|"
    echo "|======https://taksshack.com======|"
    echo "+---------------------------------+"
    
    }
    
    # Function to print a section header with ASCII art.
    # Takes the section title as an argument.
    print_section_header() {
    echo "---=[ $@ ]=---------------------------------------------------"
    echo ""
    }
    
    # --- Configuration ---
    # URL to download the latest yt-dlp binary (Linux/macOS)
    YTDLP_URL="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp"
    # Local path where yt-dlp will be saved and executed from
    YTDLP_BIN="./yt-dlp"
    # Name of the temporary Python script that will handle the download, play, and delete logic
    PYTHON_SCRIPT="yt_dlp_player.py"
    # Base name for the downloaded video file (e.g., "downloaded_video.mp4")
    # yt-dlp will append the correct extension.
    OUTPUT_BASENAME="downloaded_video"
    # File to store the last used save folder
    LAST_SAVE_FOLDER_FILE=".last_save_folder"
    # Flag file to indicate if audio tools were installed by this script
    AUDIO_TOOLS_INSTALLED_FLAG=".audio_tools_installed_by_script_flag"
    # Flag file to indicate if ffmpeg was installed by this script
    FFMPEG_INSTALLED_FLAG=".ffmpeg_installed_by_script_flag"
    # File for the playlist
    PLAYLIST_FILE="video_playlist.txt"
    
    # --- Main Script Execution ---
    
    print_banner
    print_section_header "SYSTEM SETUP"
    
    # Step 1: Download yt-dlp if it doesn't exist or isn't executable
    if [ ! -f "$YTDLP_BIN" ] || [ ! -x "$YTDLP_BIN" ]; then
    echo " yt-dlp binary not found or not executable. Attempting to download..."
    if command -v curl &> /dev/null; then
    echo " Using 'curl' to download yt-dlp..."
    curl -L "$YTDLP_URL" -o "$YTDLP_BIN"
    elif command -v wget &> /dev/null; then
    echo " Using 'wget' to download yt-dlp..."
    wget -O "$YTDLP_BIN" "$YTDLP_URL"
    else
    echo " Error: Neither 'curl' nor 'wget' found. Please install one of them to download yt-dlp."
    echo " Exiting script."
    exit 1
    fi
    
    if [ $? -eq 0 ]; then
    chmod +x "$YTDLP_BIN"
    echo " yt-dlp downloaded and made executable."
    else
    echo " Error: Failed to download yt-dlp. Please check your internet connection or the URL."
    echo " Exiting script."
    exit 1
    fi
    else
    echo " yt-dlp binary already exists and is executable. Skipping download."
    fi
    
    # Step 2: Check and install espeak-ng, aplay, and ffmpeg if not present
    ESPEAK_NG_INSTALLED=false
    APLAY_INSTALLED=false
    FFMPEG_INSTALLED=false
    
    if command -v espeak-ng &> /dev/null; then
    ESPEAK_NG_INSTALLED=true
    echo " espeak-ng is already installed."
    else
    echo " espeak-ng is NOT found."
    fi
    
    if command -v aplay &> /dev/null; then
    APLAY_INSTALLED=true
    echo " aplay is already installed."
    else
    echo " aplay is NOT found."
    fi
    
    if command -v ffmpeg &> /dev/null; then
    FFMPEG_INSTALLED=true
    echo " ffmpeg is already installed."
    else
    echo " ffmpeg is NOT found. It is required for merging video and audio."
    fi
    
    # If any critical tool is missing, offer to install
    if [ "$ESPEAK_NG_INSTALLED" = false ] || [ "$APLAY_INSTALLED" = false ] || [ "$FFMPEG_INSTALLED" = false ]; then
    read -p " Some required tools (espeak-ng, aplay, ffmpeg) are missing. Do you want to install them? (y/n): " install_tools_choice
    if [[ "$install_tools_choice" =~ ^[Yy]$ ]]; then
    echo " Attempting to install required tools..."
    INSTALL_CMD=""
    if command -v apt &> /dev/null; then
    INSTALL_CMD="sudo apt install -y espeak-ng alsa-utils ffmpeg"
    elif command -v dnf &> /dev/null; then
    INSTALL_CMD="sudo dnf install -y espeak-ng alsa-utils ffmpeg"
    elif command -v pacman &> /dev/null; then
    INSTALL_CMD="sudo pacman -S --noconfirm espeak-ng alsa-utils ffmpeg"
    else
    echo " Error: No supported package manager (apt, dnf, pacman) found for installing tools."
    echo " Please install espeak-ng, alsa-utils, and ffmpeg manually."
    fi
    
    if [ -n "$INSTALL_CMD" ]; then
    if eval "$INSTALL_CMD"; then
    echo " Required tools installed successfully."
    touch "$AUDIO_TOOLS_INSTALLED_FLAG"
    touch "$FFMPEG_INSTALLED_FLAG"
    else
    echo " Error: Failed to install required tools. Please check permissions or internet connection."
    fi
    fi
    else
    echo " Skipping installation of missing tools. Script functionality may be limited or fail."
    # If ffmpeg wasn't installed, exit because it's critical for merging.
    if [ "$FFMPEG_INSTALLED" = false ]; then
    echo " ffmpeg is critical for downloading videos with sound. Exiting."
    exit 1
    fi
    fi
    fi
    
    echo ""
    
    print_section_header "PYTHON SCRIPT CREATION"
    # Step 3: Create the Python script dynamically
    echo " Creating temporary Python script: $PYTHON_SCRIPT"
    cat <<'EOF' > "$PYTHON_SCRIPT"
    import subprocess
    import os
    import sys
    import glob
    import re
    import time
    import shutil
    
    # Path to the downloaded yt-dlp binary (relative to where the shell script runs)
    YTDLP_PATH = "./yt-dlp"
    # Base name for the downloaded video file
    OUTPUT_BASENAME = "downloaded_video"
    # File to store the last used save folder (Python will now read/write this directly)
    LAST_SAVE_FOLDER_FILE = ".last_save_folder"
    # File for the playlist
    PLAYLIST_FILE = "video_playlist.txt"
    # Temporary WAV file for the test sound
    TEST_SOUND_FILE = "taks_shack_test_sound.wav"
    
    # Exit code signaling to the bash script to uninstall audio tools and clean up last save folder
    UNINSTALL_AUDIO_TOOLS_EXIT_CODE = 5
    
    # Regex to find percentage in yt-dlp download lines
    PROGRESS_RE = re.compile(r'\[download\]\s+(\d+\.?\d*)%')
    
    # --- Utility Functions ---
    
    def print_ascii_line(char='-', length=60):
    """Prints a line of ASCII characters."""
    print(char * length)
    
    def print_ascii_header(text, char='='):
    """Prints a header with ASCII art."""
    print_ascii_line(char)
    print(f" {text}")
    print_ascii_line(char)
    print("") # Add a newline for spacing
    
    def draw_ascii_progress_bar(percentage, bar_length=40):
    """
    Draws an ASCII progress bar for the download.
    Updates the same line in the terminal using carriage return.
    """
    filled_len = int(bar_length * percentage // 100)
    bar = '#' * filled_len + '-' * (bar_length - filled_len)
    sys.stdout.write(f'\rDownloading: [ {bar} ] {percentage:6.2f}%') # Fixed width for percentage
    sys.stdout.flush()
    
    def play_test_sound():
    """
    Generates and plays a small test sound using espeak-ng and aplay.
    """
    print_ascii_header("AUDIO TEST", '-')
    test_text = "Initiating video playback. Stand by."
    
    # Check if espeak-ng and aplay are available before attempting to play
    if not (subprocess.run(["which", "espeak-ng"], capture_output=True).returncode == 0 and \
    subprocess.run(["which", "aplay"], capture_output=True).returncode == 0):
    print(" Skipping audio test: espeak-ng or aplay not found (or not in PATH).")
    print_ascii_line('=')
    return
    
    try:
    # Generate the WAV file
    print(f" Generating test sound: '{test_text}'...")
    subprocess.run(["espeak-ng", "-w", TEST_SOUND_FILE, test_text], check=True, capture_output=True)
    
    # Play the WAV file
    print(f" Playing test sound from {TEST_SOUND_FILE}...")
    subprocess.run(["aplay", TEST_SOUND_FILE], check=True, capture_output=True)
    print(" Test sound played successfully.")
    except FileNotFoundError as e:
    print(f" Warning: Audio test tools not found. {e.strerror}: '{e.filename}'.")
    print(" This should have been caught by the main bash script. Audio wake-up may be unavailable.")
    except subprocess.CalledProcessError as e:
    print(f" Warning: Failed to generate or play test sound. Error: {e.stderr.decode().strip()}")
    except Exception as e:
    print(f" An unexpected error occurred during audio test: {e}")
    finally:
    # Clean up the temporary sound file
    if os.path.exists(TEST_SOUND_FILE):
    os.remove(TEST_SOUND_FILE)
    print_ascii_line('=') # Separator line
    
    def get_playlist_links():
    """Reads the playlist file and returns a list of video links."""
    links = []
    if os.path.exists(PLAYLIST_FILE):
    try:
    with open(PLAYLIST_FILE, 'r') as f:
    # Use strip to clean up whitespace and ensure no empty lines are added
    links = [line.strip() for line in f if line.strip()]
    except Exception as e:
    print(f"Warning: Could not read playlist file '{PLAYLIST_FILE}': {e}")
    
    # Always return the list object, even if empty, preventing NoneType error.
    return links
    
    def update_playlist_file(links):
    """Writes the current list of links back to the playlist file."""
    try:
    # 'w' mode truncates the file and writes the new content
    with open(PLAYLIST_FILE, 'w') as f:
    f.write('\n'.join(links) + '\n')
    return True
    except Exception as e:
    print(f"Error: Could not rewrite playlist file '{PLAYLIST_FILE}': {e}")
    return False
    
    def add_to_playlist(youtube_link):
    """Checks for duplicates and appends a link to the playlist file if unique."""
    links = get_playlist_links()
    
    # Check for duplicates
    if youtube_link in links:
    print(f"\n Link already exists in the playlist. Skipping addition.")
    return False
    
    # Append the new link
    links.append(youtube_link)
    
    if update_playlist_file(links):
    print(f"\n Link successfully added to the end of the playlist: {PLAYLIST_FILE}")
    return True
    return False
    
    def remove_first_from_playlist():
    """Removes the first link from the playlist file and re-writes the file."""
    links = get_playlist_links()
    
    if not links:
    return None
    
    # Remove the first item
    removed_link = links.pop(0)
    
    if update_playlist_file(links):
    print(f"\n Link successfully removed from the top of the playlist.")
    return removed_link
    return None # Indicate failure
    
    def run_yt_dlp(youtube_link, cookie_option=None, is_browser_option=False):
    """
    Attempts to download a video using yt-dlp with optional cookies.
    Prints download progress to stdout using a custom ASCII bar.
    Returns (success_boolean, stderr_output, video_title_suggestion).
    """
    # Use --get-filename to preview the filename yt-dlp would use
    # Also use --get-title to get the actual title from YouTube
    info_command = [
    YTDLP_PATH,
    '--get-title',
    '--print', '%(title)s', # Get title
    '--print', '%(id)s.%(ext)s', # Get filename suggestion
    youtube_link
    ]
    if cookie_option:
    if is_browser_option:
    info_command.extend(['--cookies-from-browser', cookie_option])
    else:
    expanded_cookies_path = os.path.expanduser(cookie_option)
    info_command.extend(['--cookies', expanded_cookies_path])
    
    video_title = None
    suggested_filename = None
    try:
    info_process = subprocess.run(info_command, capture_output=True, text=True, check=True)
    # Assuming yt-dlp prints title on first line, filename on second
    info_lines = info_process.stdout.strip().split('\n')
    if len(info_lines) >= 2:
    video_title = info_lines[0].strip()
    # Sanitize the title for use as a filename
    suggested_filename = re.sub(r'[\\/:*?"<>|]', '_', video_title)
    # Remove leading/trailing spaces, and ensure it's not empty
    suggested_filename = suggested_filename.strip()
    if not suggested_filename:
    suggested_filename = "youtube_video" # Fallback if title is empty after sanitization
    
    # Append a generic extension for the prompt, actual extension will be handled by yt-dlp
    suggested_filename += ".mp4"
    else:
    print(f"Warning: Could not get full video info. Output: {info_process.stdout.strip()}")
    except subprocess.CalledProcessError as e:
    print(f"Error getting video info: {e.stderr}")
    except Exception as e:
    print(f"An unexpected error occurred while getting video info: {e}")
    
    download_command = [
    YTDLP_PATH,
    # Prioritize separate best video and audio, then merge. Fallback to best overall mp4, then just best.
    '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
    '--merge-output-format', 'mp4', # Merge audio and video into an MP4 container.
    '--output', f"{OUTPUT_BASENAME}.%(ext)s",
    '--sponsorblock-remove', 'sponsor',
    youtube_link
    ]
    
    # Add cookies option based on whether it's a browser or a file path
    if cookie_option:
    if is_browser_option:
    download_command.extend(['--cookies-from-browser', cookie_option])
    else:
    # Expand user's home directory (e.g., '~/.config/cookies.txt')
    expanded_cookies_path = os.path.expanduser(cookie_option)
    download_command.extend(['--cookies', expanded_cookies_path])
    
    stderr_output = ""
    try:
    # Use subprocess.Popen to stream output in real-time
    process = subprocess.Popen(
    download_command,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT, # Direct stderr to stdout for real-time reading
    text=True, # Decode stdout/stderr as text
    bufsize=1 # Line-buffered output for real-time printing
    )
    
    is_download_progress_active = False
    # Read stdout line by line
    for line in iter(process.stdout.readline, ''):
    # Check if the line is a progress update from yt-dlp
    match = PROGRESS_RE.search(line)
    if match:
    is_download_progress_active = True
    percentage = float(match.group(1))
    draw_ascii_progress_bar(percentage)
    else:
    if is_download_progress_active:
    sys.stdout.write('\n') # Move to next line after progress bar
    is_download_progress_active = False # Reset flag after progress bar is finalized by a non-progress line
    sys.stdout.write(line) # Print other yt-dlp output directly
    sys.stdout.flush()
    
    # After the loop, if a progress bar was the last thing printed, ensure a newline
    if is_download_progress_active:
    sys.stdout.write('\n')
    
    # Wait for the subprocess to complete and get its return code
    return_code = process.wait()
    return return_code == 0, stderr_output, suggested_filename
    except FileNotFoundError:
    return False, f"Error: yt-dlp binary not found at '{YTDLP_PATH}'. Ensure it's downloaded and executable.", suggested_filename
    except Exception as e:
    return False, f"An unexpected error occurred during yt-dlp execution: {e}", suggested_filename
    
    def find_package_manager_install_command(package_name):
    """
    Tries to find a supported package manager and returns the installation command list.
    """
    if subprocess.run(["which", "apt"], capture_output=True).returncode == 0:
    return ["sudo", "apt", "install", "-y", package_name]
    elif subprocess.run(["which", "dnf"], capture_output=True).returncode == 0:
    return ["sudo", "dnf", "install", "-y", package_name]
    elif subprocess.run(["which", "pacman"], capture_output=True).returncode == 0:
    return ["sudo", "pacman", "-S", "--noconfirm", package_name]
    return None
    
    def _get_player_and_folder():
    """Performs pre-checks and returns the determined media player and last save folder."""
    
    # 1. Read last_save_folder
    last_save_folder = ""
    if os.path.exists(LAST_SAVE_FOLDER_FILE):
    try:
    with open(LAST_SAVE_FOLDER_FILE, 'r') as f:
    last_save_folder = f.read().strip()
    except Exception as e:
    print(f"Warning: Could not read last save folder file '{LAST_SAVE_FOLDER_FILE}': {e}")
    last_save_folder = "" # Reset if read fails
    
    print_ascii_header("PYTHON DEPENDENCY CHECKS", '-')
    
    # 2. Check for a suitable media player
    media_player = None
    mpv_available = subprocess.run(["which", "mpv"], capture_output=True).returncode == 0
    vlc_available = subprocess.run(["which", "vlc"], capture_output=True).returncode == 0
    smplayer_available = subprocess.run(["which", "smplayer"], capture_output=True).returncode == 0
    
    sys.stdout.write(" Checking for media player (mpv, vlc, smplayer)...")
    sys.stdout.flush()
    
    if mpv_available:
    media_player = "mpv"
    print(f" [OK: Using {media_player}]")
    elif vlc_available or smplayer_available:
    print(" [FAILED to find mpv, alternatives found]")
    print_ascii_line('=')
    
    print_ascii_header("MPV MISSING - ACTION REQUIRED", '#')
    
    alt_options = []
    if vlc_available: alt_options.append("'v' for VLC")
    if smplayer_available: alt_options.append("'s' for SMPlayer")
    
    choice_prompt = (
    " The preferred player 'mpv' was not found.\n"
    " Do you want to try installing 'mpv' now (requires sudo) or use an alternative player?\n"
    f" (Type 'i' for install, {' or '.join(alt_options)}, 'e' to exit): "
    )
    
    install_choice = input(choice_prompt).lower().strip()
    
    if install_choice == 'i':
    install_cmd = find_package_manager_install_command("mpv")
    
    if install_cmd:
    try:
    print(f" Attempting to run: {' '.join(install_cmd)}")
    subprocess.run(install_cmd, check=True)
    print(" mpv installed successfully. Using mpv.")
    media_player = "mpv"
    except subprocess.CalledProcessError:
    print(" Failed to install mpv. Falling back to alternative player.")
    except FileNotFoundError:
    print(" Failed to run installation command (sudo not found or similar). Falling back.")
    else:
    print(" No supported package manager (apt, dnf, pacman) found for installation. Falling back to alternative player.")
    
    if not media_player:
    if install_choice == 'v' and vlc_available:
    media_player = "vlc"
    elif install_choice == 's' and smplayer_available:
    media_player = "smplayer"
    else:
    if vlc_available: media_player = "vlc"
    elif smplayer_available: media_player = "smplayer"
    
    if not media_player:
    print(" No valid player selected or available. Exiting.")
    sys.exit(1)
    
    print(f" Using player: {media_player}")
    print_ascii_line('=')
    
    else:
    # NO players are available (mpv, vlc, or smplayer)
    print(" [FAILED]")
    print(" Error: No compatible media player ('mpv', 'vlc', or 'smplayer') found in your PATH.")
    print(" Please install one of these players to proceed.")
    sys.exit(1) # Exit if no player is found
    
    # 3. Check for yt-dlp binary
    sys.stdout.write(f" Checking for yt-dlp at '{YTDLP_PATH}'...")
    sys.stdout.flush()
    if not os.path.exists(YTDLP_PATH) or not os.access(YTDLP_PATH, os.X_OK):
    print(" [FAILED]")
    print(f" Error: yt-dlp not found or not executable at '{YTDLP_PATH}'.")
    sys.exit(1)
    print(" [OK]")
    
    # 4. Check for ffmpeg.
    sys.stdout.write(" Checking for ffmpeg...")
    sys.stdout.flush()
    if subprocess.run(["which", "ffmpeg"], capture_output=True).returncode != 0:
    print(" [FAILED]")
    print(" Error: 'ffmpeg' is not found in your system's PATH.")
    sys.exit(1)
    print(" [OK]")
    
    print_ascii_line('=')
    return media_player, last_save_folder
    
    def _process_link_workflow(youtube_link, media_player, last_save_folder, ontop_choice='n', monitor_choice=None):
    """
    Handles the download, play, and optional save/delete for a single video link.
    Returns True if successfully downloaded/played/handled, False otherwise.
    """
    final_video_file = None
    suggested_filename_for_save = None
    
    try:
    print_ascii_header(f"PROCESSING: {youtube_link}", '=')
    
    download_attempt_successful = False
    
    # --- Video Download Attempt (with optional cookie retry) ---
    print("\n Attempting to download video with best video and separate audio streams...")
    success, _, suggested_filename_for_save = run_yt_dlp(youtube_link)
    
    if success:
    download_attempt_successful = True
    print("\n Video and audio downloaded and merged successfully.")
    else:
    print("\n Initial video download failed.")
    
    # --- Cookie Retry Logic ---
    retry_choice = input(
    "\n The download failed. This can happen if the video is private or age-restricted. "
    "Do you want to try again with browser cookies? (y/n): "
    ).lower()
    
    if retry_choice == 'y':
    cookie_method_choice = input(
    " How do you want to provide cookies?\n"
    " 1. From a browser (e.g., firefox, chrome)\n"
    " 2. From a cookies.txt file\n"
    " Enter 1 or 2: "
    ).strip()
    
    cookie_option_value = None
    is_browser = False
    
    if cookie_method_choice == '1':
    browser_options = {
    1: "firefox", 2: "chrome", 3: "chromium",
    4: "brave", 5: "edge", 6: "opera",
    7: "safari", 8: "vivaldi", 9: "librewolf"
    }
    print("\n Select a browser for cookies:")
    for num, browser in browser_options.items():
    print(f" {num}. {browser.capitalize()}")
    
    browser_selection_input = input(" Enter the number of your preferred browser: ").strip()
    try:
    browser_selection_num = int(browser_selection_input)
    if browser_selection_num in browser_options:
    browser_name = browser_options[browser_selection_num]
    cookie_option_value = browser_name
    is_browser = True
    print(f"\n Attempting to download video using cookies from {browser_name}...")
    else:
    print(" Invalid browser number. Falling back to cookies.txt file option.")
    cookie_method_choice = '2'
    except ValueError:
    print(" Invalid input. Please enter a number. Falling back to cookies.txt file option.")
    cookie_method_choice = '2'
    
    if cookie_method_choice == '2' or (cookie_method_choice == '1' and not is_browser):
    cookies_file_path = input(
    " Please enter the path to the cookies file "
    "(e.g., ~/.config/yt-dlp/cookies.txt or cookies.txt in current directory): "
    ).strip()
    if cookies_file_path:
    cookie_option_value = cookies_file_path
    is_browser = False
    print("\n Attempting to download video with cookies from file...")
    else:
    print(" No cookies file path provided. Cannot retry with cookies.")
    
    if cookie_option_value:
    success_retry, _, suggested_filename_for_save = run_yt_dlp(youtube_link, cookie_option_value, is_browser)
    
    if success_retry:
    download_attempt_successful = True
    print("\n Video and audio downloaded and merged successfully with cookies.")
    else:
    print(" Video download failed even with cookies.")
    else:
    print(" No valid cookie option provided. Cannot retry with cookies.")
    else:
    print(" Not retrying. Returning to main menu.")
    
    if not download_attempt_successful:
    print("\n Failed to download video. Returning to main menu.")
    return False
    
    # --- Find Downloaded File ---
    print_ascii_header("LOCATING VIDEO", '-')
    sys.stdout.write(" Searching for downloaded video file...")
    sys.stdout.flush()
    downloaded_files = glob.glob(f"{OUTPUT_BASENAME}.*")
    
    for f in downloaded_files:
    if f.startswith(OUTPUT_BASENAME) and (f.endswith(".mp4") or f.endswith(".webm") or f.endswith(".mkv")):
    final_video_file = f
    break
    
    if not final_video_file:
    print(" [NOT FOUND]")
    print(f" Error: Could not find a video file matching '{OUTPUT_BASENAME}.*' after download and merge.")
    return False
    
    print(" [FOUND]")
    print(f" Identified downloaded video file: {final_video_file}")
    print_ascii_line('=')
    
    # --- Play Test Sound before Video ---
    play_test_sound()
    
    # --- Play Video ---
    print_ascii_header("PLAYING VIDEO", '-')
    player_command = [media_player, final_video_file]
    
    if media_player == "mpv":
    # Base command for mpv: fullscreen and ignore aspect ratio
    player_command = [media_player, '--fs', '--no-keepaspect', final_video_file]
    
    if ontop_choice == 'y':
    # Insert --ontop flag after the player name
    player_command.insert(1, '--ontop')
    
    # NEW: Add monitor selection if provided
    if monitor_choice is not None:
    player_command.insert(1, f'--screen={monitor_choice}')
    print(f" MPV will attempt to play on monitor index: {monitor_choice}")
    
    elif media_player == "vlc":
    # VLC fullscreen/aspect ratio control is less common via CLI, but can be done.
    # Sticking to simple play command for cross-platform robustness.
    player_command = [media_player, '--fullscreen', final_video_file]
    if ontop_choice == 'y':
    # VLC has no direct --ontop flag from CLI, need to rely on settings or other methods.
    print(" Note: VLC does not support '--ontop' from command line; ignoring option.")
    if monitor_choice is not None:
    # VLC has no direct --screen flag from CLI, ignoring option.
    print(" Note: VLC does not support monitor selection from command line; ignoring option.")
    
    elif media_player == "smplayer":
    # SMPlayer handles its own settings for full screen/always on top
    player_command = [media_player, final_video_file]
    if ontop_choice == 'y':
    # SMPlayer also doesn't have a standardized CLI ontop flag, relying on its internal settings.
    print(" Note: SMPlayer does not support '--ontop' from command line; ignoring option.")
    if monitor_choice is not None:
    # SMPlayer also doesn't have a standardized CLI screen flag, ignoring option.
    print(" Note: SMPlayer does not support monitor selection from command line; ignoring option.")
    
    print(f" Playing video with {media_player}: {final_video_file}")
    # Execute the detected media player
    subprocess.run(player_command, check=True)
    print(" Video playback finished.")
    print_ascii_line('=')
    
    # --- Save Video Logic ---
    print_ascii_header("SAVE VIDEO", '-')
    save_choice = input(f" Do you want to save this video (current name: '{suggested_filename_for_save if suggested_filename_for_save else final_video_file}')? (y/n): ").lower()
    
    if save_choice == 'y':
    # Ask for new filename, default to YouTube's title
    new_filename = input(f" Enter the desired filename (default: '{suggested_filename_for_save}'): ").strip()
    if not new_filename:
    new_filename = suggested_filename_for_save
    
    # Ensure the new filename has an extension
    if not os.path.splitext(new_filename)[1]:
    original_ext = os.path.splitext(final_video_file)[1]
    new_filename += original_ext
    
    # Ask for save folder
    target_folder = ""
    if last_save_folder and os.path.isdir(last_save_folder):
    use_previous_folder = input(f" Use previous save folder '{last_save_folder}'? (y/n): ").lower()
    if use_previous_folder == 'y':
    target_folder = last_save_folder
    else:
    target_folder = input(" Enter the new save folder path: ").strip()
    else:
    target_folder = input(" Enter the save folder path: ").strip()
    
    if not target_folder:
    print(" No save folder specified. Video will not be saved.")
    os.remove(final_video_file)
    print(f" Deleted unsaved video: {final_video_file}")
    return True
    
    os.makedirs(target_folder, exist_ok=True)
    
    destination_path = os.path.join(target_folder, new_filename)
    try:
    shutil.move(final_video_file, destination_path)
    print(f" Video saved successfully to: {destination_path}")
    # Update last save folder file
    with open(LAST_SAVE_FOLDER_FILE, 'w') as f:
    f.write(target_folder)
    # NOTE: last_save_folder needs to be updated in the main function's scope too, but
    # for now, the main loop re-reads it for the next iteration.
    print(f" Last save folder updated to: {target_folder}")
    except OSError as e:
    print(f" Error saving video: {e}")
    print(f" The video was not moved. It remains as '{final_video_file}' in the current directory.")
    print_ascii_line('=')
    else:
    print(" Video will not be saved.")
    # --- Delete Downloaded Video if not saved ---
    if final_video_file and os.path.exists(final_video_file):
    try:
    os.remove(final_video_file)
    print(f" Deleted unsaved video: {final_video_file}")
    except Exception as e:
    print(f" Warning: Could not delete {final_video_file}. Reason: {e}")
    print_ascii_line('=')
    
    return True # Indicate successful handling of the video flow
    
    except FileNotFoundError as e:
    print_ascii_header("RUNTIME ERROR", '#')
    print(f" Runtime Error: A required command was not found. Detail: {e}")
    print(" Please ensure all necessary applications are installed and in your system's PATH.")
    if final_video_file and os.path.exists(final_video_file):
    try: os.remove(final_video_file)
    except Exception: pass
    return False
    except subprocess.CalledProcessError as e:
    print_ascii_header("EXECUTION ERROR", '#')
    print(f" Command Execution Error (Exit code: {e.returncode}):")
    print(f" Command: {' '.join(e.cmd)}")
    if e.stdout: print(f" Output: {e.stdout.strip()}")
    print(" Please review the error messages above for details on what went wrong.")
    if final_video_file and os.path.exists(final_video_file):
    try: os.remove(final_video_file)
    except Exception: pass
    return False
    except Exception as e:
    print_ascii_header("UNEXPECTED ERROR", '#')
    print(f" An unexpected error occurred: {e}")
    if final_video_file and os.path.exists(final_video_file):
    try: os.remove(final_video_file)
    except Exception: pass
    return False
    finally:
    # Aggressive cleanup of any residual 'downloaded_video.*' files that might be left over
    print_ascii_header("FINAL CLEANUP OF TEMP FILES", '-')
    print(" Checking for residual temporary files...")
    for f in glob.glob(f"{OUTPUT_BASENAME}.*"):
    if os.path.exists(f):
    try:
    os.remove(f)
    print(f" Cleaned up residual temporary file: {f}")
    except Exception as e:
    print(f" Warning: Could not clean up residual temporary file {f}. Reason: {e}")
    if os.path.exists(TEST_SOUND_FILE):
    try:
    os.remove(TEST_SOUND_FILE)
    print(f" Cleaned up temporary sound file: {TEST_SOUND_FILE}")
    except Exception as e:
    print(f" Warning: Could not clean up temporary sound file {TEST_SOUND_FILE}. Reason: {e}")
    print_ascii_line('=')
    
    def process_new_video(media_player, last_save_folder):
    """
    Handles the flow for a user-provided new video link.
    """
    print_ascii_header("NEW VIDEO LINK", '-')
    youtube_link = input(" Please enter the YouTube video link: ")
    if not youtube_link:
    print(" No link entered. Returning to main menu.")
    return False
    
    # 1. Ask about saving to playlist BEFORE download
    playlist_choice = input(" Do you want to save this link to the playlist before playing? (y/n): ").lower()
    
    if playlist_choice == 'y':
    add_to_playlist(youtube_link)
    
    # 2. Ask about playing AFTER playlist decision
    play_choice = input(" Do you want to download and play the video now? (y/n): ").lower()
    
    if play_choice == 'y':
    # 3. Ask about Playback Options BEFORE download (if mpv)
    ontop_choice = 'n' # Default to no
    monitor_choice = None # Default to no monitor specified
    
    if media_player == "mpv":
    print_ascii_header("PLAYBACK OPTIONS (MPV)", '-')
    
    # --- Always On Top Option ---
    ontop_choice = input(
    " Do you want the player window to be 'Always On Top'?\n"
    " (This keeps the video visible above all other windows.) (y/n): "
    ).lower()
    
    # --- Monitor Selection Option ---
    monitor_input = input(
    " Enter the **monitor number** (e.g., 0, 1) you want to play on, or press Enter to skip: "
    ).strip()
    
    if monitor_input.isdigit():
    monitor_choice = int(monitor_input)
    elif monitor_input:
    print(" Warning: Invalid monitor number entered. Skipping monitor selection.")
    
    print_ascii_line('=')
    
    return _process_link_workflow(youtube_link, media_player, last_save_folder, ontop_choice, monitor_choice)
    else:
    print(" Skipping video download and playback. Returning to main menu.")
    return True # Handled successfully, but no video processed.
    
    def process_playlist_video(media_player, last_save_folder):
    """
    Handles the flow for playing videos from the playlist, including recursion for 'play next'.
    Returns True if video processing was completed, False otherwise.
    """
    
    playlist_links = get_playlist_links()
    
    if not playlist_links:
    print_ascii_header("PLAYLIST EMPTY", '-')
    print(" The playlist is currently empty.")
    
    # --- START OF NEW LOGIC ---
    add_link_choice = input(
    " The playlist is empty. Do you want to add a link now and play it? (y/n): "
    ).lower()
    
    if add_link_choice == 'y':
    youtube_link = input(" Please enter the YouTube video link: ")
    if not youtube_link:
    print(" No link entered. Returning to main menu.")
    print_ascii_line('=')
    return False
    
    # Add the link to the playlist
    add_to_playlist(youtube_link)
    
    # Recursive call to process the newly added link (which is now the only one)
    # This ensures we proceed to playback options and the download flow
    print("\n Link added. Restarting playlist flow to process the video...")
    return process_playlist_video(media_player, last_save_folder)
    # --- END OF NEW LOGIC ---
    
    print_ascii_line('=')
    return False
    
    # Get the top link
    youtube_link = playlist_links[0]
    
    print_ascii_header(f"PLAYLIST - TOP VIDEO ({len(playlist_links)} links remaining)", '=')
    print(f" Video to play: {youtube_link}")
    
    # 1. Ask about Playback Options BEFORE download (if mpv)
    ontop_choice = 'n' # Default to no
    monitor_choice = None # Default to no monitor specified
    
    if media_player == "mpv":
    print_ascii_header("PLAYBACK OPTIONS (MPV)", '-')
    
    # --- Always On Top Option ---
    ontop_choice = input(
    " Do you want the player window to be 'Always On Top'?\n"
    " (This keeps the video visible above all other windows.) (y/n): "
    ).lower()
    
    # --- Monitor Selection Option ---
    monitor_input = input(
    " Enter the **monitor number** (e.g., 0, 1) you want to play on, or press Enter to skip: "
    ).strip()
    
    if monitor_input.isdigit():
    monitor_choice = int(monitor_input)
    elif monitor_input:
    print(" Warning: Invalid monitor number entered. Skipping monitor selection.")
    
    print_ascii_line('=')
    
    # Run the core download/play/save workflow
    video_processed = _process_link_workflow(youtube_link, media_player, last_save_folder, ontop_choice, monitor_choice)
    
    # After playback, handle the list cleanup
    if video_processed:
    # Ask to delete the link from the list
    delete_choice = input(
    f" Do you want to delete the played link from the playlist? (y/n): "
    ).lower()
    
    if delete_choice == 'y':
    # This call removes the link that was just played (the first one)
    removed = remove_first_from_playlist()
    
    if removed:
    playlist_links = get_playlist_links() # Re-read for the next prompt
    
    # Ask to play the next link
    if playlist_links:
    next_choice = input(f" There are {len(playlist_links)} links remaining. Do you want to play the next one? (y/n): ").lower()
    if next_choice == 'y':
    # Recursive call to play the next one (which is now at the top)
    return process_playlist_video(media_player, last_save_folder)
    else:
    print(" Playlist is now empty.")
    
    return video_processed
    
    def download_and_play_video():
    """
    The main control function that handles the menu loop.
    """
    try:
    # 1. Perform pre-flight checks and get the necessary system info
    # This function handles the media player check and reads the last save folder
    media_player, last_save_folder = _get_player_and_folder()
    except SystemExit:
    # Catch SystemExit from checks if they fail
    return False
    
    # Main menu loop
    while True:
    print_ascii_header("MAIN MENU", '=')
    print(" 1. Download and play a NEW YouTube video link.")
    
    playlist_links = get_playlist_links()
    num_links = len(playlist_links)
    
    if num_links > 0:
    print(f" 2. Play the top video from the PLAYLIST ({num_links} links available).")
    else:
    print(" 2. Play from PLAYLIST (Playlist is empty).")
    
    print(" 3. Exit the program and clean up.")
    
    choice = input(" Enter your choice (1, 2, 3): ").strip()
    
    video_processed = False
    if choice == '1':
    video_processed = process_new_video(media_player, last_save_folder)
    elif choice == '2':
    # The playlist processing handles the inner loop (delete/play next)
    video_processed = process_playlist_video(media_player, last_save_folder)
    elif choice == '3':
    print(" Exiting. Goodbye!")
    sys.exit(UNINSTALL_AUDIO_TOOLS_EXIT_CODE)
    else:
    print(" Invalid choice. Please enter 1, 2, or 3.")
    
    # Update last_save_folder if a save operation was successful (for next loop iteration)
    if video_processed and os.path.exists(LAST_SAVE_FOLDER_FILE):
    try:
    with open(LAST_SAVE_FOLDER_FILE, 'r') as f:
    last_save_folder = f.read().strip()
    except Exception:
    pass # Ignore read errors
    
    print("\n" + "=" * 60) # Main menu separator
    # Loop continues to the start of the while True block for the next operation
    
    # Ensure the main function is called when the script is executed.
    if __name__ == "__main__":
    # Clean up residual files before the main process starts
    for f in glob.glob(f"{OUTPUT_BASENAME}.*"):
    if os.path.exists(f):
    try: os.remove(f)
    except Exception: pass
    if os.path.exists(TEST_SOUND_FILE):
    try: os.remove(TEST_SOUND_FILE)
    except Exception: pass
    
    download_and_play_video()
    EOF
    
    echo "" # Add a newline for better readability
    
    print_section_header "RUNNING PLAYER"
    # Step 4: Run the Python script
    echo " Executing Python script: $PYTHON_SCRIPT"
    # The Python script will now handle the loop and exit with a specific code when the user is done.
    python3 "$PYTHON_SCRIPT"
    PYTHON_EXIT_CODE=$? # Capture the exit code of the Python script
    
    echo "" # Add a newline for better readability
    
    print_section_header "FINAL CLEANUP"
    # Step 5: Clean up temporary files and potentially uninstall audio tools
    echo " Cleaning up shell script's temporary files..."
    
    # Remove the temporary Python script
    if [ -f "$PYTHON_SCRIPT" ]; then
    rm "$PYTHON_SCRIPT"
    echo " Removed temporary Python script: $PYTHON_SCRIPT"
    fi
    
    # Remove the yt-dlp binary as requested
    if [ -f "$YTDLP_BIN" ]; then
    rm "$YTDLP_BIN"
    echo " Removed yt-dlp binary: $YTDLP_BIN"
    fi
    
    # Condition for removing the last save folder file: only if Python script exited with 'no more videos' signal
    if [ $PYTHON_EXIT_CODE -eq 5 ] && [ -f "$LAST_SAVE_FOLDER_FILE" ]; then
    rm "$LAST_SAVE_FOLDER_FILE"
    echo " Removed last save folder file: $LAST_SAVE_FOLDER_FILE (as requested upon exit)"
    elif [ -f "$LAST_SAVE_FOLDER_FILE" ]; then
    echo " Keeping last save folder file: $LAST_SAVE_FOLDER_FILE (script did not exit via 'no more videos' option)"
    fi
    
    # Check if the Python script signaled for audio tool uninstallation
    # And if the flag file exists (meaning *this* script installed them)
    if [ $PYTHON_EXIT_CODE -eq 5 ]; then # Only consider uninstalling if Python script signaled end
    # --- NEW: Check and uninstall ffmpeg ---
    if [ -f "$FFMPEG_INSTALLED_FLAG" ]; then
    echo ""
    read -p " This script installed 'ffmpeg'. Do you want to uninstall it now? (y/n): " uninstall_ffmpeg_confirm
    if [[ "$uninstall_ffmpeg_confirm" =~ ^[Yy]$ ]]; then
    echo " Attempting to uninstall ffmpeg..."
    UNINSTALL_FFMPEG_CMD=""
    if command -v apt &> /dev/null; then
    UNINSTALL_FFMPEG_CMD="sudo apt remove -y ffmpeg"
    elif command -v dnf &> /dev/null; then
    UNINSTALL_FFMPEG_CMD="sudo dnf remove -y ffmpeg"
    elif command -v pacman &> /dev/null; then
    UNINSTALL_FFMPEG_CMD="sudo pacman -R --noconfirm ffmpeg"
    else
    echo " Error: No supported package manager found for uninstalling ffmpeg."
    echo " Please uninstall ffmpeg manually."
    fi
    
    if [ -n "$UNINSTALL_FFMPEG_CMD" ]; then
    if eval "$UNINSTALL_FFMPEG_CMD"; then
    echo " ffmpeg uninstalled successfully."
    rm "$FFMPEG_INSTALLED_FLAG" # Remove flag file
    else
    echo " Error: Failed to uninstall ffmpeg. Please check permissions."
    fi
    fi
    else
    echo " Skipping ffmpeg uninstallation."
    fi
    fi
    
    # --- Existing: Check and uninstall espeak-ng and alsa-utils ---
    if [ -f "$AUDIO_TOOLS_INSTALLED_FLAG" ]; then
    echo ""
    read -p " This script installed 'espeak-ng' and 'alsa-utils'. Do you want to uninstall them now? (y/n): " uninstall_audio_confirm
    if [[ "$uninstall_audio_confirm" =~ ^[Yy]$ ]]; then
    echo " Attempting to uninstall audio tools..."
    UNINSTALL_AUDIO_CMD=""
    if command -v apt &> /dev/null; then
    UNINSTALL_AUDIO_CMD="sudo apt remove -y espeak-ng alsa-utils"
    elif command -v dnf &> /dev/null; then
    UNINSTALL_AUDIO_CMD="sudo dnf remove -y espeak-ng alsa-utils"
    elif command -v pacman &> /dev/null; then
    UNINSTALL_AUDIO_CMD="sudo pacman -R --noconfirm espeak-ng alsa-utils"
    else
    echo " Error: No supported package manager found for uninstalling audio tools."
    echo " Please install espeak-ng and alsa-utils manually."
    fi
    
    if [ -n "$UNINSTALL_AUDIO_CMD" ]; then
    if eval "$UNINSTALL_AUDIO_CMD"; then
    echo " Audio tools uninstalled successfully."
    rm "$AUDIO_TOOLS_INSTALLED_FLAG" # Remove flag file
    else
    echo " Error: Failed to uninstall audio tools. Please check permissions."
    fi
    fi
    else
    echo " Skipping audio tools uninstallation."
    fi
    fi
    else # If Python script did not exit with code 5, do not offer uninstallation
    echo " Tool uninstallation not offered as script did not exit via 'no more videos' option or encountered an error."
    fi
    
    # Report final status based on the Python script's exit code
    if [ $PYTHON_EXIT_CODE -ne 0 ] && [ $PYTHON_EXIT_CODE -ne 5 ]; then
    echo " Script finished with errors (exit code: $PYTHON_EXIT_CODE)."
    exit $PYTHON_EXIT_CODE
    else
    echo " Script finished successfully."
    exit 0
    fi

     

    #8347
    thumbtak
    Moderator

    Bug fixes and includes a installation of a supported player, if none of the players are found.

    #!/bin/bash
    
    # ASCII Art Functions
    # Function to print the main ASCII art banner for the script.
    print_banner() {
    echo "+---------------------------------+"
    echo "|===========TAKS SHACK============|"
    echo "|======https://taksshack.com======|"
    echo "+---------------------------------+"
    
    }
    
    # Function to print a section header with ASCII art.
    # Takes the section title as an argument.
    print_section_header() {
    echo "---=[ $@ ]=---------------------------------------------------"
    echo ""
    }
    
    # --- Configuration ---
    # URL to download the latest yt-dlp binary (Linux/macOS)
    YTDLP_URL="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp"
    # Local path where yt-dlp will be saved and executed from
    YTDLP_BIN="./yt-dlp"
    # Name of the temporary Python script that will handle the download, play, and delete logic
    PYTHON_SCRIPT="yt_dlp_player.py"
    # Base name for the downloaded video file (e.g., "downloaded_video.mp4")
    # yt-dlp will append the correct extension.
    OUTPUT_BASENAME="downloaded_video"
    # File to store the last used save folder
    LAST_SAVE_FOLDER_FILE=".last_save_folder"
    # Flag file to indicate if audio tools were installed by this script
    AUDIO_TOOLS_INSTALLED_FLAG=".audio_tools_installed_by_script_flag"
    # Flag file to indicate if ffmpeg was installed by this script
    FFMPEG_INSTALLED_FLAG=".ffmpeg_installed_by_script_flag"
    # Flag file to indicate if mpv was installed by this script
    MPV_INSTALLED_FLAG=".mpv_installed_by_script_flag" # <--- NEW FLAG
    # File for the playlist
    PLAYLIST_FILE="video_playlist.txt"
    
    # --- Main Script Execution ---
    
    print_banner
    print_section_header "SYSTEM SETUP"
    
    # Step 1: Download yt-dlp if it doesn't exist or isn't executable
    if [ ! -f "$YTDLP_BIN" ] || [ ! -x "$YTDLP_BIN" ]; then
    echo " yt-dlp binary not found or not executable. Attempting to download..."
    if command -v curl &> /dev/null; then
    echo " Using 'curl' to download yt-dlp..."
    curl -L "$YTDLP_URL" -o "$YTDLP_BIN"
    elif command -v wget &> /dev/null; then
    echo " Using 'wget' to download yt-dlp..."
    wget -O "$YTDLP_BIN" "$YTDLP_URL"
    else
    echo " Error: Neither 'curl' nor 'wget' found. Please install one of them to download yt-dlp."
    echo " Exiting script."
    exit 1
    fi
    
    if [ $? -eq 0 ]; then
    chmod +x "$YTDLP_BIN"
    echo " yt-dlp downloaded and made executable."
    else
    echo " Error: Failed to download yt-dlp. Please check your internet connection or the URL."
    echo " Exiting script."
    exit 1
    fi
    else
    echo " yt-dlp binary already exists and is executable. Skipping download."
    fi
    
    # Step 2: Check and install espeak-ng, aplay, and ffmpeg if not present
    ESPEAK_NG_INSTALLED=false
    APLAY_INSTALLED=false
    FFMPEG_INSTALLED=false
    
    if command -v espeak-ng &> /dev/null; then
    ESPEAK_NG_INSTALLED=true
    echo " espeak-ng is already installed."
    else
    echo " espeak-ng is NOT found."
    fi
    
    if command -v aplay &> /dev/null; then
    APLAY_INSTALLED=true
    echo " aplay is already installed."
    else
    echo " aplay is NOT found."
    fi
    
    if command -v ffmpeg &> /dev/null; then
    FFMPEG_INSTALLED=true
    echo " ffmpeg is already installed."
    else
    echo " ffmpeg is NOT found. It is required for merging video and audio."
    fi
    
    # If any critical tool is missing, offer to install
    if [ "$ESPEAK_NG_INSTALLED" = false ] || [ "$APLAY_INSTALLED" = false ] || [ "$FFMPEG_INSTALLED" = false ]; then
    read -p " Some required tools (espeak-ng, aplay, ffmpeg) are missing. Do you want to install them? (y/n): " install_tools_choice
    if [[ "$install_tools_choice" =~ ^[Yy]$ ]]; then
    echo " Attempting to install required tools..."
    INSTALL_CMD=""
    if command -v apt &> /dev/null; then
    INSTALL_CMD="sudo apt install -y espeak-ng alsa-utils ffmpeg"
    elif command -v dnf &> /dev/null; then
    INSTALL_CMD="sudo dnf install -y espeak-ng alsa-utils ffmpeg"
    elif command -v pacman &> /dev/null; then
    INSTALL_CMD="sudo pacman -S --noconfirm espeak-ng alsa-utils ffmpeg"
    else
    echo " Error: No supported package manager (apt, dnf, pacman) found for installing tools."
    echo " Please install espeak-ng, alsa-utils, and ffmpeg manually."
    fi
    
    if [ -n "$INSTALL_CMD" ]; then
    if eval "$INSTALL_CMD"; then
    echo " Required tools installed successfully."
    touch "$AUDIO_TOOLS_INSTALLED_FLAG"
    touch "$FFMPEG_INSTALLED_FLAG"
    else
    echo " Error: Failed to install required tools. Please check permissions or internet connection."
    fi
    fi
    else
    echo " Skipping installation of missing tools. Script functionality may be limited or fail."
    # If ffmpeg wasn't installed, exit because it's critical for merging.
    if [ "$FFMPEG_INSTALLED" = false ]; then
    echo " ffmpeg is critical for downloading videos with sound. Exiting."
    exit 1
    fi
    fi
    fi
    
    echo ""
    
    print_section_header "PYTHON SCRIPT CREATION"
    # Step 3: Create the Python script dynamically
    echo " Creating temporary Python script: $PYTHON_SCRIPT"
    cat <<'EOF' > "$PYTHON_SCRIPT"
    import subprocess
    import os
    import sys
    import glob
    import re
    import time
    import shutil
    
    # Path to the downloaded yt-dlp binary (relative to where the shell script runs)
    YTDLP_PATH = "./yt-dlp"
    # Base name for the downloaded video file
    OUTPUT_BASENAME = "downloaded_video"
    # File to store the last used save folder (Python will now read/write this directly)
    LAST_SAVE_FOLDER_FILE = ".last_save_folder"
    # File for the playlist
    PLAYLIST_FILE = "video_playlist.txt"
    # Temporary WAV file for the test sound
    TEST_SOUND_FILE = "taks_shack_test_sound.wav"
    # Flag file to indicate if mpv was installed by this script
    MPV_INSTALLED_FLAG = ".mpv_installed_by_script_flag" # <--- NEW CONSTANT
    
    # Exit code signaling to the bash script to uninstall audio tools and clean up last save folder
    UNINSTALL_AUDIO_TOOLS_EXIT_CODE = 5
    
    # Regex to find percentage in yt-dlp download lines
    PROGRESS_RE = re.compile(r'\[download\]\s+(\d+\.?\d*)%')
    
    # --- Utility Functions ---
    
    def print_ascii_line(char='-', length=60):
    """Prints a line of ASCII characters."""
    print(char * length)
    
    def print_ascii_header(text, char='='):
    """Prints a header with ASCII art."""
    print_ascii_line(char)
    print(f" {text}")
    print_ascii_line(char)
    print("") # Add a newline for spacing
    
    def draw_ascii_progress_bar(percentage, bar_length=40):
    """
    Draws an ASCII progress bar for the download.
    Updates the same line in the terminal using carriage return.
    """
    filled_len = int(bar_length * percentage // 100)
    bar = '#' * filled_len + '-' * (bar_length - filled_len)
    sys.stdout.write(f'\rDownloading: [ {bar} ] {percentage:6.2f}%') # Fixed width for percentage
    sys.stdout.flush()
    
    def play_test_sound():
    """
    Generates and plays a small test sound using espeak-ng and aplay.
    """
    print_ascii_header("AUDIO TEST", '-')
    test_text = "Initiating video playback. Stand by."
    
    # Check if espeak-ng and aplay are available before attempting to play
    if not (subprocess.run(["which", "espeak-ng"], capture_output=True).returncode == 0 and \
    subprocess.run(["which", "aplay"], capture_output=True).returncode == 0):
    print(" Skipping audio test: espeak-ng or aplay not found (or not in PATH).")
    print_ascii_line('=')
    return
    
    try:
    # Generate the WAV file
    print(f" Generating test sound: '{test_text}'...")
    subprocess.run(["espeak-ng", "-w", TEST_SOUND_FILE, test_text], check=True, capture_output=True)
    
    # Play the WAV file
    print(f" Playing test sound from {TEST_SOUND_FILE}...")
    subprocess.run(["aplay", TEST_SOUND_FILE], check=True, capture_output=True)
    print(" Test sound played successfully.")
    except FileNotFoundError as e:
    print(f" Warning: Audio test tools not found. {e.strerror}: '{e.filename}'.")
    print(" This should have been caught by the main bash script. Audio wake-up may be unavailable.")
    except subprocess.CalledProcessError as e:
    print(f" Warning: Failed to generate or play test sound. Error: {e.stderr.decode().strip()}")
    except Exception as e:
    print(f" An unexpected error occurred during audio test: {e}")
    finally:
    # Clean up the temporary sound file
    if os.path.exists(TEST_SOUND_FILE):
    os.remove(TEST_SOUND_FILE)
    print_ascii_line('=') # Separator line
    
    def get_playlist_links():
    """Reads the playlist file and returns a list of video links."""
    links = []
    if os.path.exists(PLAYLIST_FILE):
    try:
    with open(PLAYLIST_FILE, 'r') as f:
    # Use strip to clean up whitespace and ensure no empty lines are added
    links = [line.strip() for line in f if line.strip()]
    except Exception as e:
    print(f"Warning: Could not read playlist file '{PLAYLIST_FILE}': {e}")
    
    # Always return the list object, even if empty, preventing NoneType error.
    return links
    
    def update_playlist_file(links):
    """Writes the current list of links back to the playlist file."""
    try:
    # 'w' mode truncates the file and writes the new content
    with open(PLAYLIST_FILE, 'w') as f:
    f.write('\n'.join(links) + '\n')
    return True
    except Exception as e:
    print(f"Error: Could not rewrite playlist file '{PLAYLIST_FILE}': {e}")
    return False
    
    def add_to_playlist(youtube_link):
    """Checks for duplicates and appends a link to the playlist file if unique."""
    links = get_playlist_links()
    
    # Check for duplicates
    if youtube_link in links:
    print(f"\n Link already exists in the playlist. Skipping addition.")
    return False
    
    # Append the new link
    links.append(youtube_link)
    
    if update_playlist_file(links):
    print(f"\n Link successfully added to the end of the playlist: {PLAYLIST_FILE}")
    return True
    return False
    
    def remove_first_from_playlist():
    """Removes the first link from the playlist file and re-writes the file."""
    links = get_playlist_links()
    
    if not links:
    return None
    
    # Remove the first item
    removed_link = links.pop(0)
    
    if update_playlist_file(links):
    print(f"\n Link successfully removed from the top of the playlist.")
    return removed_link
    return None # Indicate failure
    
    def run_yt_dlp(youtube_link, cookie_option=None, is_browser_option=False):
    """
    Attempts to download a video using yt-dlp with optional cookies.
    Prints download progress to stdout using a custom ASCII bar.
    Returns (success_boolean, stderr_output, video_title_suggestion).
    """
    # Use --get-filename to preview the filename yt-dlp would use
    # Also use --get-title to get the actual title from YouTube
    info_command = [
    YTDLP_PATH,
    '--get-title',
    '--print', '%(title)s', # Get title
    '--print', '%(id)s.%(ext)s', # Get filename suggestion
    youtube_link
    ]
    if cookie_option:
    if is_browser_option:
    info_command.extend(['--cookies-from-browser', cookie_option])
    else:
    expanded_cookies_path = os.path.expanduser(cookie_option)
    info_command.extend(['--cookies', expanded_cookies_path])
    
    video_title = None
    suggested_filename = None
    try:
    info_process = subprocess.run(info_command, capture_output=True, text=True, check=True)
    # Assuming yt-dlp prints title on first line, filename on second
    info_lines = info_process.stdout.strip().split('\n')
    if len(info_lines) >= 2:
    video_title = info_lines[0].strip()
    # Sanitize the title for use as a filename
    suggested_filename = re.sub(r'[\\/:*?"<>|]', '_', video_title)
    # Remove leading/trailing spaces, and ensure it's not empty
    suggested_filename = suggested_filename.strip()
    if not suggested_filename:
    suggested_filename = "youtube_video" # Fallback if title is empty after sanitization
    
    # Append a generic extension for the prompt, actual extension will be handled by yt-dlp
    suggested_filename += ".mp4"
    else:
    print(f"Warning: Could not get full video info. Output: {info_process.stdout.strip()}")
    except subprocess.CalledProcessError as e:
    print(f"Error getting video info: {e.stderr}")
    except Exception as e:
    print(f"An unexpected error occurred while getting video info: {e}")
    
    download_command = [
    YTDLP_PATH,
    # Prioritize separate best video and audio, then merge. Fallback to best overall mp4, then just best.
    '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
    '--merge-output-format', 'mp4', # Merge audio and video into an MP4 container.
    '--output', f"{OUTPUT_BASENAME}.%(ext)s",
    '--sponsorblock-remove', 'sponsor',
    youtube_link
    ]
    
    # Add cookies option based on whether it's a browser or a file path
    if cookie_option:
    if is_browser_option:
    download_command.extend(['--cookies-from-browser', cookie_option])
    else:
    # Expand user's home directory (e.g., '~/.config/cookies.txt')
    expanded_cookies_path = os.path.expanduser(cookie_option)
    download_command.extend(['--cookies', expanded_cookies_path])
    
    stderr_output = ""
    try:
    # Use subprocess.Popen to stream output in real-time
    process = subprocess.Popen(
    download_command,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT, # Direct stderr to stdout for real-time reading
    text=True, # Decode stdout/stderr as text
    bufsize=1 # Line-buffered output for real-time printing
    )
    
    is_download_progress_active = False
    # Read stdout line by line
    for line in iter(process.stdout.readline, ''):
    # Check if the line is a progress update from yt-dlp
    match = PROGRESS_RE.search(line)
    if match:
    is_download_progress_active = True
    percentage = float(match.group(1))
    draw_ascii_progress_bar(percentage)
    else:
    if is_download_progress_active:
    sys.stdout.write('\n') # Move to next line after progress bar
    is_download_progress_active = False # Reset flag after progress bar is finalized by a non-progress line
    sys.stdout.write(line) # Print other yt-dlp output directly
    sys.stdout.flush()
    
    # After the loop, if a progress bar was the last thing printed, ensure a newline
    if is_download_progress_active:
    sys.stdout.write('\n')
    
    # Wait for the subprocess to complete and get its return code
    return_code = process.wait()
    return return_code == 0, stderr_output, suggested_filename
    except FileNotFoundError:
    return False, f"Error: yt-dlp binary not found at '{YTDLP_PATH}'. Ensure it's downloaded and executable.", suggested_filename
    except Exception as e:
    return False, f"An unexpected error occurred during yt-dlp execution: {e}", suggested_filename
    
    def find_package_manager_install_command(package_name):
    """
    Tries to find a supported package manager and returns the installation command list.
    """
    if subprocess.run(["which", "apt"], capture_output=True).returncode == 0:
    return ["sudo", "apt", "install", "-y", package_name]
    elif subprocess.run(["which", "dnf"], capture_output=True).returncode == 0:
    return ["sudo", "dnf", "install", "-y", package_name]
    elif subprocess.run(["which", "pacman"], capture_output=True).returncode == 0:
    return ["sudo", "pacman", "-S", "--noconfirm", package_name]
    return None
    
    def _get_player_and_folder():
    """Performs pre-checks and returns the determined media player and last save folder."""
    
    # 1. Read last_save_folder
    last_save_folder = ""
    if os.path.exists(LAST_SAVE_FOLDER_FILE):
    try:
    with open(LAST_SAVE_FOLDER_FILE, 'r') as f:
    last_save_folder = f.read().strip()
    except Exception as e:
    print(f"Warning: Could not read last save folder file '{LAST_SAVE_FOLDER_FILE}': {e}")
    last_save_folder = "" # Reset if read fails
    
    print_ascii_header("PYTHON DEPENDENCY CHECKS", '-')
    
    # 2. Check for a suitable media player
    media_player = None
    mpv_available = subprocess.run(["which", "mpv"], capture_output=True).returncode == 0
    vlc_available = subprocess.run(["which", "vlc"], capture_output=True).returncode == 0
    smplayer_available = subprocess.run(["which", "smplayer"], capture_output=True).returncode == 0
    
    sys.stdout.write(" Checking for media player (mpv, vlc, smplayer)...")
    sys.stdout.flush()
    
    if mpv_available:
    media_player = "mpv"
    print(f" [OK: Using {media_player}]")
    elif vlc_available or smplayer_available:
    print(" [FAILED to find mpv, alternatives found]")
    print_ascii_line('=')
    
    print_ascii_header("MPV MISSING - ACTION REQUIRED", '#')
    
    alt_options = []
    if vlc_available: alt_options.append("'v' for VLC")
    if smplayer_available: alt_options.append("'s' for SMPlayer")
    
    choice_prompt = (
    " The preferred player 'mpv' was not found.\n"
    " Do you want to try installing 'mpv' now (requires sudo) or use an alternative player?\n"
    f" (Type 'i' for install, {' or '.join(alt_options)}, 'e' to exit): "
    )
    
    install_choice = input(choice_prompt).lower().strip()
    
    if install_choice == 'i':
    install_cmd = find_package_manager_install_command("mpv")
    
    if install_cmd:
    try:
    print(f" Attempting to run: {' '.join(install_cmd)}")
    subprocess.run(install_cmd, check=True)
    print(" mpv installed successfully. Using mpv.")
    # Create the flag file to signal the bash script for uninstallation later
    with open(MPV_INSTALLED_FLAG, 'w') as f:
    f.write('installed')
    media_player = "mpv"
    except subprocess.CalledProcessError:
    print(" Failed to install mpv. Falling back to alternative player.")
    except FileNotFoundError:
    print(" Failed to run installation command (sudo not found or similar). Falling back.")
    else:
    print(" No supported package manager (apt, dnf, pacman) found for installation. Falling back to alternative player.")
    
    if not media_player:
    if install_choice == 'v' and vlc_available:
    media_player = "vlc"
    elif install_choice == 's' and smplayer_available:
    media_player = "smplayer"
    else:
    if vlc_available: media_player = "vlc"
    elif smplayer_available: media_player = "smplayer"
    
    if not media_player:
    print(" No valid player selected or available. Exiting.")
    sys.exit(1)
    
    print(f" Using player: {media_player}")
    print_ascii_line('=')
    
    else:
    # NO players are available (mpv, vlc, or smplayer) - THIS IS THE CRITICAL NEW LOGIC BLOCK
    print(" [FAILED]")
    print(" Error: No compatible media player ('mpv', 'vlc', or 'smplayer') found in your PATH.")
    
    # --- NEW INSTALLATION PROMPT ---
    install_choice = input(
    " The required player 'mpv' is missing.\n"
    " Do you want to attempt installing 'mpv' now (requires sudo)? (y/n): "
    ).lower().strip()
    
    if install_choice == 'y':
    install_cmd = find_package_manager_install_command("mpv")
    
    if install_cmd:
    try:
    print(f" Attempting to run: {' '.join(install_cmd)}")
    subprocess.run(install_cmd, check=True)
    print(" mpv installed successfully.")
    # Create the flag file to signal the bash script for uninstallation later
    with open(MPV_INSTALLED_FLAG, 'w') as f:
    f.write('installed')
    media_player = "mpv"
    except subprocess.CalledProcessError:
    print(" Failed to install mpv. Please check permissions and try again.")
    except FileNotFoundError:
    print(" Failed to run installation command (sudo not found or similar).")
    else:
    print(" No supported package manager (apt, dnf, pacman) found for installation.")
    
    if not media_player:
    print(" No valid player found or installed. Exiting.")
    sys.exit(1) # Exit if no player is found
    
    print_ascii_line('=')
    return media_player, last_save_folder
    
    def _process_link_workflow(youtube_link, media_player, last_save_folder, ontop_choice='n', monitor_choice=None):
    """
    Handles the download, play, and optional save/delete for a single video link.
    Returns True if successfully downloaded/played/handled, False otherwise.
    """
    final_video_file = None
    suggested_filename_for_save = None
    
    try:
    print_ascii_header(f"PROCESSING: {youtube_link}", '=')
    
    download_attempt_successful = False
    
    # --- Video Download Attempt (with optional cookie retry) ---
    print("\n Attempting to download video with best video and separate audio streams...")
    success, _, suggested_filename_for_save = run_yt_dlp(youtube_link)
    
    if success:
    download_attempt_successful = True
    print("\n Video and audio downloaded and merged successfully.")
    else:
    print("\n Initial video download failed.")
    
    # --- Cookie Retry Logic ---
    retry_choice = input(
    "\n The download failed. This can happen if the video is private or age-restricted. "
    "Do you want to try again with browser cookies? (y/n): "
    ).lower()
    
    if retry_choice == 'y':
    cookie_method_choice = input(
    " How do you want to provide cookies?\n"
    " 1. From a browser (e.g., firefox, chrome)\n"
    " 2. From a cookies.txt file\n"
    " Enter 1 or 2: "
    ).strip()
    
    cookie_option_value = None
    is_browser = False
    
    if cookie_method_choice == '1':
    browser_options = {
    1: "firefox", 2: "chrome", 3: "chromium",
    4: "brave", 5: "edge", 6: "opera",
    7: "safari", 8: "vivaldi", 9: "librewolf"
    }
    print("\n Select a browser for cookies:")
    for num, browser in browser_options.items():
    print(f" {num}. {browser.capitalize()}")
    
    browser_selection_input = input(" Enter the number of your preferred browser: ").strip()
    try:
    browser_selection_num = int(browser_selection_input)
    if browser_selection_num in browser_options:
    browser_name = browser_options[browser_selection_num]
    cookie_option_value = browser_name
    is_browser = True
    print(f"\n Attempting to download video using cookies from {browser_name}...")
    else:
    print(" Invalid browser number. Falling back to cookies.txt file option.")
    cookie_method_choice = '2'
    except ValueError:
    print(" Invalid input. Please enter a number. Falling back to cookies.txt file option.")
    cookie_method_choice = '2'
    
    if cookie_method_choice == '2' or (cookie_method_choice == '1' and not is_browser):
    cookies_file_path = input(
    " Please enter the path to the cookies file "
    "(e.g., ~/.config/yt-dlp/cookies.txt or cookies.txt in current directory): "
    ).strip()
    if cookies_file_path:
    cookie_option_value = cookies_file_path
    is_browser = False
    print("\n Attempting to download video with cookies from file...")
    else:
    print(" No cookies file path provided. Cannot retry with cookies.")
    
    if cookie_option_value:
    success_retry, _, suggested_filename_for_save = run_yt_dlp(youtube_link, cookie_option_value, is_browser)
    
    if success_retry:
    download_attempt_successful = True
    print("\n Video and audio downloaded and merged successfully with cookies.")
    else:
    print(" Video download failed even with cookies.")
    else:
    print(" Not retrying. Returning to main menu.")
    
    if not download_attempt_successful:
    print("\n Failed to download video. Returning to main menu.")
    return False
    
    # --- Find Downloaded File ---
    print_ascii_header("LOCATING VIDEO", '-')
    sys.stdout.write(" Searching for downloaded video file...")
    sys.stdout.flush()
    downloaded_files = glob.glob(f"{OUTPUT_BASENAME}.*")
    
    for f in downloaded_files:
    if f.startswith(OUTPUT_BASENAME) and (f.endswith(".mp4") or f.endswith(".webm") or f.endswith(".mkv")):
    final_video_file = f
    break
    
    if not final_video_file:
    print(" [NOT FOUND]")
    print(f" Error: Could not find a video file matching '{OUTPUT_BASENAME}.*' after download and merge.")
    return False
    
    print(" [FOUND]")
    print(f" Identified downloaded video file: {final_video_file}")
    print_ascii_line('=')
    
    # --- Play Test Sound before Video ---
    play_test_sound()
    
    # --- Play Video ---
    print_ascii_header("PLAYING VIDEO", '-')
    player_command = [media_player, final_video_file]
    
    if media_player == "mpv":
    # Base command for mpv: fullscreen and ignore aspect ratio
    player_command = [media_player, '--fs', '--no-keepaspect', final_video_file]
    
    if ontop_choice == 'y':
    # Insert --ontop flag after the player name
    player_command.insert(1, '--ontop')
    
    # NEW: Add monitor selection if provided
    if monitor_choice is not None:
    player_command.insert(1, f'--screen={monitor_choice}')
    print(f" MPV will attempt to play on monitor index: {monitor_choice}")
    
    elif media_player == "vlc":
    # VLC fullscreen/aspect ratio control is less common via CLI, but can be done.
    # Sticking to simple play command for cross-platform robustness.
    player_command = [media_player, '--fullscreen', final_video_file]
    if ontop_choice == 'y':
    # VLC has no direct --ontop flag from CLI, need to rely on settings or other methods.
    print(" Note: VLC does not support '--ontop' from command line; ignoring option.")
    if monitor_choice is not None:
    # VLC has no direct --screen flag from CLI, ignoring option.
    print(" Note: VLC does not support monitor selection from command line; ignoring option.")
    
    elif media_player == "smplayer":
    # SMPlayer handles its own settings for full screen/always on top
    player_command = [media_player, final_video_file]
    if ontop_choice == 'y':
    # SMPlayer also doesn't have a standardized CLI ontop flag, relying on its internal settings.
    print(" Note: SMPlayer does not support '--ontop' from command line; ignoring option.")
    if monitor_choice is not None:
    # SMPlayer also doesn't have a standardized CLI screen flag, ignoring option.
    print(" Note: SMPlayer does not support monitor selection from command line; ignoring option.")
    
    print(f" Playing video with {media_player}: {final_video_file}")
    # Execute the detected media player
    subprocess.run(player_command, check=True)
    print(" Video playback finished.")
    print_ascii_line('=')
    
    # --- Save Video Logic ---
    print_ascii_header("SAVE VIDEO", '-')
    save_choice = input(f" Do you want to save this video (current name: '{suggested_filename_for_save if suggested_filename_for_save else final_video_file}')? (y/n): ").lower()
    
    if save_choice == 'y':
    # Ask for new filename, default to YouTube's title
    new_filename = input(f" Enter the desired filename (default: '{suggested_filename_for_save}'): ").strip()
    if not new_filename:
    new_filename = suggested_filename_for_save
    
    # Ensure the new filename has an extension
    if not os.path.splitext(new_filename)[1]:
    original_ext = os.path.splitext(final_video_file)[1]
    new_filename += original_ext
    
    # Ask for save folder
    target_folder = ""
    if last_save_folder and os.path.isdir(last_save_folder):
    use_previous_folder = input(f" Use previous save folder '{last_save_folder}'? (y/n): ").lower()
    if use_previous_folder == 'y':
    target_folder = last_save_folder
    else:
    target_folder = input(" Enter the new save folder path: ").strip()
    else:
    target_folder = input(" Enter the save folder path: ").strip()
    
    if not target_folder:
    print(" No save folder specified. Video will not be saved.")
    os.remove(final_video_file)
    print(f" Deleted unsaved video: {final_video_file}")
    return True
    
    os.makedirs(target_folder, exist_ok=True)
    
    destination_path = os.path.join(target_folder, new_filename)
    try:
    shutil.move(final_video_file, destination_path)
    print(f" Video saved successfully to: {destination_path}")
    # Update last save folder file
    with open(LAST_SAVE_FOLDER_FILE, 'w') as f:
    f.write(target_folder)
    # NOTE: last_save_folder needs to be updated in the main function's scope too, but
    # for now, the main loop re-reads it for the next iteration.
    print(f" Last save folder updated to: {target_folder}")
    except OSError as e:
    print(f" Error saving video: {e}")
    print(f" The video was not moved. It remains as '{final_video_file}' in the current directory.")
    print_ascii_line('=')
    else:
    print(" Video will not be saved.")
    # --- Delete Downloaded Video if not saved ---
    if final_video_file and os.path.exists(final_video_file):
    try:
    os.remove(final_video_file)
    print(f" Deleted unsaved video: {final_video_file}")
    except Exception as e:
    print(f" Warning: Could not delete {final_video_file}. Reason: {e}")
    print_ascii_line('=')
    
    return True # Indicate successful handling of the video flow
    
    except FileNotFoundError as e:
    print_ascii_header("RUNTIME ERROR", '#')
    print(f" Runtime Error: A required command was not found. Detail: {e}")
    print(" Please ensure all necessary applications are installed and in your system's PATH.")
    if final_video_file and os.path.exists(final_video_file):
    try: os.remove(final_video_file)
    except Exception: pass
    return False
    except subprocess.CalledProcessError as e:
    print_ascii_header("EXECUTION ERROR", '#')
    print(f" Command Execution Error (Exit code: {e.returncode}):")
    print(f" Command: {' '.join(e.cmd)}")
    if e.stdout: print(f" Output: {e.stdout.strip()}")
    print(" Please review the error messages above for details on what went wrong.")
    if final_video_file and os.path.exists(final_video_file):
    try: os.remove(final_video_file)
    except Exception: pass
    return False
    except Exception as e:
    print_ascii_header("UNEXPECTED ERROR", '#')
    print(f" An unexpected error occurred: {e}")
    if final_video_file and os.path.exists(final_video_file):
    try: os.remove(final_video_file)
    except Exception: pass
    return False
    finally:
    # Aggressive cleanup of any residual 'downloaded_video.*' files that might be left over
    print_ascii_header("FINAL CLEANUP OF TEMP FILES", '-')
    print(" Checking for residual temporary files...")
    for f in glob.glob(f"{OUTPUT_BASENAME}.*"):
    if os.path.exists(f):
    try:
    os.remove(f)
    print(f" Cleaned up residual temporary file: {f}")
    except Exception as e:
    print(f" Warning: Could not clean up residual temporary file {f}. Reason: {e}")
    if os.path.exists(TEST_SOUND_FILE):
    try:
    os.remove(TEST_SOUND_FILE)
    print(f" Cleaned up temporary sound file: {TEST_SOUND_FILE}")
    except Exception as e:
    print(f" Warning: Could not clean up temporary sound file {TEST_SOUND_FILE}. Reason: {e}")
    print_ascii_line('=')
    
    def process_new_video(media_player, last_save_folder):
    """
    Handles the flow for a user-provided new video link.
    """
    print_ascii_header("NEW VIDEO LINK", '-')
    youtube_link = input(" Please enter the YouTube video link: ")
    if not youtube_link:
    print(" No link entered. Returning to main menu.")
    return False
    
    # 1. Ask about saving to playlist BEFORE download
    playlist_choice = input(" Do you want to save this link to the playlist before playing? (y/n): ").lower()
    
    if playlist_choice == 'y':
    add_to_playlist(youtube_link)
    
    # 2. Ask about playing AFTER playlist decision
    play_choice = input(" Do you want to download and play the video now? (y/n): ").lower()
    
    if play_choice == 'y':
    # 3. Ask about Playback Options BEFORE download (if mpv)
    ontop_choice = 'n' # Default to no
    monitor_choice = None # Default to no monitor specified
    
    if media_player == "mpv":
    print_ascii_header("PLAYBACK OPTIONS (MPV)", '-')
    
    # --- Always On Top Option ---
    ontop_choice = input(
    " Do you want the player window to be 'Always On Top'?\n"
    " (This keeps the video visible above all other windows.) (y/n): "
    ).lower()
    
    # --- Monitor Selection Option ---
    monitor_input = input(
    " Enter the **monitor number** (e.g., 0, 1) you want to play on, or press Enter to skip: "
    ).strip()
    
    if monitor_input.isdigit():
    monitor_choice = int(monitor_input)
    elif monitor_input:
    print(" Warning: Invalid monitor number entered. Skipping monitor selection.")
    
    print_ascii_line('=')
    
    return _process_link_workflow(youtube_link, media_player, last_save_folder, ontop_choice, monitor_choice)
    else:
    print(" Skipping video download and playback. Returning to main menu.")
    return True # Handled successfully, but no video processed.
    
    def process_playlist_video(media_player, last_save_folder):
    """
    Handles the flow for playing videos from the playlist, including recursion for 'play next'.
    Returns True if video processing was completed, False otherwise.
    """
    
    playlist_links = get_playlist_links()
    
    if not playlist_links:
    print_ascii_header("PLAYLIST EMPTY", '-')
    print(" The playlist is currently empty.")
    
    # --- START OF NEW LOGIC ---
    add_link_choice = input(
    " The playlist is empty. Do you want to add a link now and play it? (y/n): "
    ).lower()
    
    if add_link_choice == 'y':
    youtube_link = input(" Please enter the YouTube video link: ")
    if not youtube_link:
    print(" No link entered. Returning to main menu.")
    print_ascii_line('=')
    return False
    
    # Add the link to the playlist
    add_to_playlist(youtube_link)
    
    # Recursive call to process the newly added link (which is now the only one)
    # This ensures we proceed to playback options and the download flow
    print("\n Link added. Restarting playlist flow to process the video...")
    return process_playlist_video(media_player, last_save_folder)
    # --- END OF NEW LOGIC ---
    
    print_ascii_line('=')
    return False
    
    # Get the top link
    youtube_link = playlist_links[0]
    
    print_ascii_header(f"PLAYLIST - TOP VIDEO ({len(playlist_links)} links remaining)", '=')
    print(f" Video to play: {youtube_link}")
    
    # 1. Ask about Playback Options BEFORE download (if mpv)
    ontop_choice = 'n' # Default to no
    monitor_choice = None # Default to no monitor specified
    
    if media_player == "mpv":
    print_ascii_header("PLAYBACK OPTIONS (MPV)", '-')
    
    # --- Always On Top Option ---
    ontop_choice = input(
    " Do you want the player window to be 'Always On Top'?\n"
    " (This keeps the video visible above all other windows.) (y/n): "
    ).lower()
    
    # --- Monitor Selection Option ---
    monitor_input = input(
    " Enter the **monitor number** (e.g., 0, 1) you want to play on, or press Enter to skip: "
    ).strip()
    
    if monitor_input.isdigit():
    monitor_choice = int(monitor_input)
    elif monitor_input:
    print(" Warning: Invalid monitor number entered. Skipping monitor selection.")
    
    print_ascii_line('=')
    
    # Run the core download/play/save workflow
    video_processed = _process_link_workflow(youtube_link, media_player, last_save_folder, ontop_choice, monitor_choice)
    
    # After playback, handle the list cleanup
    if video_processed:
    # Ask to delete the link from the list
    delete_choice = input(
    f" Do you want to delete the played link from the playlist? (y/n): "
    ).lower()
    
    if delete_choice == 'y':
    # This call removes the link that was just played (the first one)
    removed = remove_first_from_playlist()
    
    if removed:
    playlist_links = get_playlist_links() # Re-read for the next prompt
    
    # Ask to play the next link
    if playlist_links:
    next_choice = input(f" There are {len(playlist_links)} links remaining. Do you want to play the next one? (y/n): ").lower()
    if next_choice == 'y':
    # Recursive call to play the next one (which is now at the top)
    return process_playlist_video(media_player, last_save_folder)
    elif delete_choice == 'y': # Only print if deletion made it empty
    print(" Playlist is now empty.")
    
    return video_processed
    
    def download_and_play_video():
    """
    The main control function that handles the menu loop.
    """
    try:
    # 1. Perform pre-flight checks and get the necessary system info
    # This function handles the media player check and reads the last save folder
    media_player, last_save_folder = _get_player_and_folder()
    except SystemExit:
    # Catch SystemExit from checks if they fail
    return False
    
    # Main menu loop
    while True:
    print_ascii_header("MAIN MENU", '=')
    print(" 1. Download and play a NEW YouTube video link.")
    
    playlist_links = get_playlist_links()
    num_links = len(playlist_links)
    
    if num_links > 0:
    print(f" 2. Play the top video from the PLAYLIST ({num_links} links available).")
    else:
    print(" 2. Play from PLAYLIST (Playlist is empty).")
    
    print(" 3. Exit the program and clean up.")
    
    choice = input(" Enter your choice (1, 2, 3): ").strip()
    
    video_processed = False
    if choice == '1':
    video_processed = process_new_video(media_player, last_save_folder)
    elif choice == '2':
    # The playlist processing handles the inner loop (delete/play next)
    video_processed = process_playlist_video(media_player, last_save_folder)
    elif choice == '3':
    print(" Exiting. Goodbye!")
    sys.exit(UNINSTALL_AUDIO_TOOLS_EXIT_CODE)
    else:
    print(" Invalid choice. Please enter 1, 2, or 3.")
    
    # Update last_save_folder if a save operation was successful (for next loop iteration)
    if video_processed and os.path.exists(LAST_SAVE_FOLDER_FILE):
    try:
    with open(LAST_SAVE_FOLDER_FILE, 'r') as f:
    last_save_folder = f.read().strip()
    except Exception:
    pass # Ignore read errors
    
    print("\n" + "=" * 60) # Main menu separator
    # Loop continues to the start of the while True block for the next operation
    
    # Ensure the main function is called when the script is executed.
    if __name__ == "__main__":
    # Clean up residual files before the main process starts
    for f in glob.glob(f"{OUTPUT_BASENAME}.*"):
    if os.path.exists(f):
    try: os.remove(f)
    except Exception: pass
    if os.path.exists(TEST_SOUND_FILE):
    try: os.remove(TEST_SOUND_FILE)
    except Exception: pass
    
    download_and_play_video()
    EOF
    
    echo "" # Add a newline for better readability
    
    print_section_header "RUNNING PLAYER"
    # Step 4: Run the Python script
    echo " Executing Python script: $PYTHON_SCRIPT"
    # The Python script will now handle the loop and exit with a specific code when the user is done.
    python3 "$PYTHON_SCRIPT"
    PYTHON_EXIT_CODE=$? # Capture the exit code of the Python script
    
    echo "" # Add a newline for better readability
    
    print_section_header "FINAL CLEANUP"
    # Step 5: Clean up temporary files and potentially uninstall audio tools
    echo " Cleaning up shell script's temporary files..."
    
    # Remove the temporary Python script
    if [ -f "$PYTHON_SCRIPT" ]; then
    rm "$PYTHON_SCRIPT"
    echo " Removed temporary Python script: $PYTHON_SCRIPT"
    fi
    
    # Remove the yt-dlp binary as requested
    if [ -f "$YTDLP_BIN" ]; then
    rm "$YTDLP_BIN"
    echo " Removed yt-dlp binary: $YTDLP_BIN"
    fi
    
    # Condition for removing the last save folder file: only if Python script exited with 'no more videos' signal
    if [ $PYTHON_EXIT_CODE -eq 5 ] && [ -f "$LAST_SAVE_FOLDER_FILE" ]; then
    rm "$LAST_SAVE_FOLDER_FILE"
    echo " Removed last save folder file: $LAST_SAVE_FOLDER_FILE (as requested upon exit)"
    elif [ -f "$LAST_SAVE_FOLDER_FILE" ]; then
    echo " Keeping last save folder file: $LAST_SAVE_FOLDER_FILE (script did not exit via 'no more videos' option)"
    fi
    
    # Check if the Python script signaled for tool uninstallation
    if [ $PYTHON_EXIT_CODE -eq 5 ]; then # Only consider uninstalling if Python script signaled end
    
    # --- NEW: Check and uninstall mpv ---
    if [ -f "$MPV_INSTALLED_FLAG" ]; then
    echo ""
    read -p " This script installed 'mpv'. Do you want to uninstall it now? (y/n): " uninstall_mpv_confirm
    if [[ "$uninstall_mpv_confirm" =~ ^[Yy]$ ]]; then
    echo " Attempting to uninstall mpv..."
    UNINSTALL_MPV_CMD=""
    if command -v apt &> /dev/null; then
    UNINSTALL_MPV_CMD="sudo apt remove -y mpv"
    elif command -v dnf &> /dev/null; then
    UNINSTALL_MPV_CMD="sudo dnf remove -y mpv"
    elif command -v pacman &> /dev/null; then
    UNINSTALL_MPV_CMD="sudo pacman -R --noconfirm mpv"
    else
    echo " Error: No supported package manager found for uninstalling mpv."
    echo " Please uninstall mpv manually."
    fi
    
    if [ -n "$UNINSTALL_MPV_CMD" ]; then
    if eval "$UNINSTALL_MPV_CMD"; then
    echo " mpv uninstalled successfully."
    rm "$MPV_INSTALLED_FLAG" # Remove flag file
    else
    echo " Error: Failed to uninstall mpv. Please check permissions."
    fi
    fi
    else
    echo " Skipping mpv uninstallation."
    fi
    fi
    
    # --- Existing: Check and uninstall ffmpeg ---
    if [ -f "$FFMPEG_INSTALLED_FLAG" ]; then
    echo ""
    read -p " This script installed 'ffmpeg'. Do you want to uninstall it now? (y/n): " uninstall_ffmpeg_confirm
    if [[ "$uninstall_ffmpeg_confirm" =~ ^[Yy]$ ]]; then
    echo " Attempting to uninstall ffmpeg..."
    UNINSTALL_FFMPEG_CMD=""
    if command -v apt &> /dev/null; then
    UNINSTALL_FFMPEG_CMD="sudo apt remove -y ffmpeg"
    elif command -v dnf &> /dev/null; then
    UNINSTALL_FFMPEG_CMD="sudo dnf remove -y ffmpeg"
    elif command -v pacman &> /dev/null; then
    UNINSTALL_FFMPEG_CMD="sudo pacman -R --noconfirm ffmpeg"
    else
    echo " Error: No supported package manager found for uninstalling ffmpeg."
    echo " Please uninstall ffmpeg manually."
    fi
    
    if [ -n "$UNINSTALL_FFMPEG_CMD" ]; then
    if eval "$UNINSTALL_FFMPEG_CMD"; then
    echo " ffmpeg uninstalled successfully."
    rm "$FFMPEG_INSTALLED_FLAG" # Remove flag file
    else
    echo " Error: Failed to uninstall ffmpeg. Please check permissions."
    fi
    fi
    else
    echo " Skipping ffmpeg uninstallation."
    fi
    fi
    
    # --- Existing: Check and uninstall espeak-ng and alsa-utils ---
    if [ -f "$AUDIO_TOOLS_INSTALLED_FLAG" ]; then
    echo ""
    read -p " This script installed 'espeak-ng' and 'alsa-utils'. Do you want to uninstall them now? (y/n): " uninstall_audio_confirm
    if [[ "$uninstall_audio_confirm" =~ ^[Yy]$ ]]; then
    echo " Attempting to uninstall audio tools..."
    UNINSTALL_AUDIO_CMD=""
    if command -v apt &> /dev/null; then
    UNINSTALL_AUDIO_CMD="sudo apt remove -y espeak-ng alsa-utils"
    elif command -v dnf &> /dev/null; then
    UNINSTALL_AUDIO_CMD="sudo dnf remove -y espeak-ng alsa-utils"
    elif command -v pacman &> /dev/null; then
    UNINSTALL_AUDIO_CMD="sudo pacman -R --noconfirm espeak-ng alsa-utils"
    else
    echo " Error: No supported package manager found for uninstalling audio tools."
    echo " Please install espeak-ng and alsa-utils manually."
    fi
    
    if [ -n "$UNINSTALL_AUDIO_CMD" ]; then
    if eval "$UNINSTALL_AUDIO_CMD"; then
    echo " Audio tools uninstalled successfully."
    rm "$AUDIO_TOOLS_INSTALLED_FLAG" # Remove flag file
    else
    echo " Error: Failed to uninstall audio tools. Please check permissions."
    fi
    fi
    else
    echo " Skipping audio tools uninstallation."
    fi
    fi
    else # If Python script did not exit with code 5, do not offer uninstallation
    echo " Tool uninstallation not offered as script did not exit via 'no more videos' option or encountered an error."
    fi
    
    # Report final status based on the Python script's exit code
    if [ $PYTHON_EXIT_CODE -ne 0 ] && [ $PYTHON_EXIT_CODE -ne 5 ]; then
    echo " Script finished with errors (exit code: $PYTHON_EXIT_CODE)."
    exit $PYTHON_EXIT_CODE
    else
    echo " Script finished successfully."
    exit 0
    fi
    #8351
    thumbtak
    Moderator

    The script was updated to resolve an issue where the aplay utility caused the audio alert to play at the system’s maximum hardware volume setting, which was excessively loud. The fix replaces aplay with the mpvmedia player and explicitly sets its volume to 25% (–volume=25`) for a controlled and non-disruptive sound level.

    #!/bin/bash
    
    # ASCII Art Functions
    # Function to print the main ASCII art banner for the script.
    print_banner() {
    echo "+---------------------------------+"
    echo "|===========TAKS SHACK============|"
    echo "|======https://taksshack.com======|"
    echo "+---------------------------------+"
    
    }
    
    # Function to print a section header with ASCII art.
    # Takes the section title as an argument.
    print_section_header() {
    echo "---=[ $@ ]=---------------------------------------------------"
    echo ""
    }
    
    # --- Configuration ---
    # URL to download the latest yt-dlp binary (Linux/macOS)
    YTDLP_URL="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp"
    # Local path where yt-dlp will be saved and executed from
    YTDLP_BIN="./yt-dlp"
    # Name of the temporary Python script that will handle the download, play, and delete logic
    PYTHON_SCRIPT="yt_dlp_player.py"
    # Base name for the downloaded video file (e.g., "downloaded_video.mp4")
    # yt-dlp will append the correct extension.
    OUTPUT_BASENAME="downloaded_video"
    # File to store the last used save folder
    LAST_SAVE_FOLDER_FILE=".last_save_folder"
    # Flag file to indicate if audio tools were installed by this script
    AUDIO_TOOLS_INSTALLED_FLAG=".audio_tools_installed_by_script_flag"
    # Flag file to indicate if ffmpeg was installed by this script
    FFMPEG_INSTALLED_FLAG=".ffmpeg_installed_by_script_flag"
    # Flag file to indicate if mpv was installed by this script
    MPV_INSTALLED_FLAG=".mpv_installed_by_script_flag" # <--- NEW FLAG
    # File for the playlist
    PLAYLIST_FILE="video_playlist.txt"
    
    # --- Main Script Execution ---
    
    print_banner
    print_section_header "SYSTEM SETUP"
    
    # Step 1: Download yt-dlp if it doesn't exist or isn't executable
    if [ ! -f "$YTDLP_BIN" ] || [ ! -x "$YTDLP_BIN" ]; then
    echo " yt-dlp binary not found or not executable. Attempting to download..."
    if command -v curl &> /dev/null; then
    echo " Using 'curl' to download yt-dlp..."
    curl -L "$YTDLP_URL" -o "$YTDLP_BIN"
    elif command -v wget &> /dev/null; then
    echo " Using 'wget' to download yt-dlp..."
    wget -O "$YTDLP_BIN" "$YTDLP_URL"
    else
    echo " Error: Neither 'curl' nor 'wget' found. Please install one of them to download yt-dlp."
    echo " Exiting script."
    exit 1
    fi
    
    if [ $? -eq 0 ]; then
    chmod +x "$YTDLP_BIN"
    echo " yt-dlp downloaded and made executable."
    else
    echo " Error: Failed to download yt-dlp. Please check your internet connection or the URL."
    echo " Exiting script."
    exit 1
    fi
    else
    echo " yt-dlp binary already exists and is executable. Skipping download."
    fi
    
    # Step 2: Check and install espeak-ng, aplay, and ffmpeg if not present
    ESPEAK_NG_INSTALLED=false
    APLAY_INSTALLED=false
    FFMPEG_INSTALLED=false
    
    if command -v espeak-ng &> /dev/null; then
    ESPEAK_NG_INSTALLED=true
    echo " espeak-ng is already installed."
    else
    echo " espeak-ng is NOT found."
    fi
    
    if command -v aplay &> /dev/null; then
    APLAY_INSTALLED=true
    echo " aplay is already installed."
    else
    echo " aplay is NOT found."
    fi
    
    if command -v ffmpeg &> /dev/null; then
    FFMPEG_INSTALLED=true
    echo " ffmpeg is already installed."
    else
    echo " ffmpeg is NOT found. It is required for merging video and audio."
    fi
    
    # If any critical tool is missing, offer to install
    if [ "$ESPEAK_NG_INSTALLED" = false ] || [ "$APLAY_INSTALLED" = false ] || [ "$FFMPEG_INSTALLED" = false ]; then
    read -p " Some required tools (espeak-ng, aplay, ffmpeg) are missing. Do you want to install them? (y/n): " install_tools_choice
    if [[ "$install_tools_choice" =~ ^[Yy]$ ]]; then
    echo " Attempting to install required tools..."
    INSTALL_CMD=""
    if command -v apt &> /dev/null; then
    INSTALL_CMD="sudo apt install -y espeak-ng alsa-utils ffmpeg"
    elif command -v dnf &> /dev/null; then
    INSTALL_CMD="sudo dnf install -y espeak-ng alsa-utils ffmpeg"
    elif command -v pacman &> /dev/null; then
    INSTALL_CMD="sudo pacman -S --noconfirm espeak-ng alsa-utils ffmpeg"
    else
    echo " Error: No supported package manager (apt, dnf, pacman) found for installing tools."
    echo " Please install espeak-ng, alsa-utils, and ffmpeg manually."
    fi
    
    if [ -n "$INSTALL_CMD" ]; then
    if eval "$INSTALL_CMD"; then
    echo " Required tools installed successfully."
    touch "$AUDIO_TOOLS_INSTALLED_FLAG"
    touch "$FFMPEG_INSTALLED_FLAG"
    else
    echo " Error: Failed to install required tools. Please check permissions or internet connection."
    fi
    fi
    else
    echo " Skipping installation of missing tools. Script functionality may be limited or fail."
    # If ffmpeg wasn't installed, exit because it's critical for merging.
    if [ "$FFMPEG_INSTALLED" = false ]; then
    echo " ffmpeg is critical for downloading videos with sound. Exiting."
    exit 1
    fi
    fi
    fi
    
    echo ""
    
    print_section_header "PYTHON SCRIPT CREATION"
    # Step 3: Create the Python script dynamically
    echo " Creating temporary Python script: $PYTHON_SCRIPT"
    cat <<'EOF' > "$PYTHON_SCRIPT"
    import subprocess
    import os
    import sys
    import glob
    import re
    import time
    import shutil
    
    # Path to the downloaded yt-dlp binary (relative to where the shell script runs)
    YTDLP_PATH = "./yt-dlp"
    # Base name for the downloaded video file
    OUTPUT_BASENAME = "downloaded_video"
    # File to store the last used save folder
    LAST_SAVE_FOLDER_FILE = ".last_save_folder"
    # File for the playlist
    PLAYLIST_FILE = "video_playlist.txt"
    # Temporary WAV file for the test sound
    TEST_SOUND_FILE = "taks_shack_test_sound.wav"
    # Flag file to indicate if mpv was installed by this script
    MPV_INSTALLED_FLAG = ".mpv_installed_by_script_flag" # <--- NEW CONSTANT
    
    # Exit code signaling to the bash script to uninstall audio tools and clean up last save folder
    UNINSTALL_AUDIO_TOOLS_EXIT_CODE = 5
    
    # Regex to find percentage in yt-dlp download lines
    PROGRESS_RE = re.compile(r'\[download\]\s+(\d+\.?\d*)%')
    
    # --- Utility Functions ---
    
    def print_ascii_line(char='-', length=60):
    """Prints a line of ASCII characters."""
    print(char * length)
    
    def print_ascii_header(text, char='='):
    """Prints a header with ASCII art."""
    print_ascii_line(char)
    print(f" {text}")
    print_ascii_line(char)
    print("") # Add a newline for spacing
    
    def draw_ascii_progress_bar(percentage, bar_length=40):
    """
    Draws an ASCII progress bar for the download.
    Updates the same line in the terminal using carriage return.
    """
    filled_len = int(bar_length * percentage // 100)
    bar = '#' * filled_len + '-' * (bar_length - filled_len)
    sys.stdout.write(f'\rDownloading: [ {bar} ] {percentage:6.2f}%') # Fixed width for percentage
    sys.stdout.flush()
    
    def play_test_sound():
    """
    Generates and plays a small test sound using espeak-ng and a compatible player.
    Prioritizes mpv for volume control, falls back to aplay with a warning.
    """
    print_ascii_header("AUDIO TEST", '-')
    test_text = "Initiating video playback. Stand by."
    
    espeak_available = subprocess.run(["which", "espeak-ng"], capture_output=True).returncode == 0
    mpv_available = subprocess.run(["which", "mpv"], capture_output=True).returncode == 0
    aplay_available = subprocess.run(["which", "aplay"], capture_output=True).returncode == 0
    
    if not espeak_available:
    print(" Skipping audio test: espeak-ng not found (or not in PATH).")
    print_ascii_line('=')
    return
    
    if not mpv_available and not aplay_available:
    print(" Skipping audio test: No compatible audio player (mpv or aplay) found.")
    print_ascii_line('=')
    return
    
    try:
    # Generate the WAV file
    print(f" Generating test sound: '{test_text}'...")
    # Capture output to suppress espeak-ng stdout messages
    subprocess.run(["espeak-ng", "-w", TEST_SOUND_FILE, test_text], check=True, capture_output=True)
    
    # Play the WAV file using MPV for volume control
    if mpv_available:
    # Set volume to 25% for a low but hearable alert
    volume_level = 25
    print(f" Playing test sound from {TEST_SOUND_FILE} using mpv at {volume_level}% volume...")
    # Capture output to suppress mpv stdout messages
    subprocess.run(["mpv", f"--volume={volume_level}", TEST_SOUND_FILE], check=True, capture_output=True)
    # Fallback to aplay if mpv is not installed
    elif aplay_available:
    print(f" Playing test sound from {TEST_SOUND_FILE} using aplay...")
    print(" WARNING: Using aplay, volume control is not possible and sound may be LOUD.")
    # Capture output to suppress aplay stdout messages
    subprocess.run(["aplay", TEST_SOUND_FILE], check=True, capture_output=True)
    
    print(" Test sound played successfully.")
    except FileNotFoundError as e:
    print(f" Warning: Audio test tools not found. {e.strerror}: '{e.filename}'.")
    print(" This should have been caught by the main bash script. Audio wake-up may be unavailable.")
    except subprocess.CalledProcessError as e:
    # Only print the player error if it's not mpv, since the bash script installs aplay/ffmpeg
    if mpv_available: # If mpv was attempted, the error might be an actual mpv issue
    print(f" Warning: Failed to generate or play test sound. Error: {e.stderr.decode().strip()}")
    else:
    print(f" Warning: Failed to generate or play test sound using aplay. Error: {e.stderr.decode().strip()}")
    except Exception as e:
    print(f" An unexpected error occurred during audio test: {e}")
    finally:
    # Clean up the temporary sound file
    if os.path.exists(TEST_SOUND_FILE):
    os.remove(TEST_SOUND_FILE)
    print_ascii_line('=') # Separator line
    
    def get_playlist_links():
    """Reads the playlist file and returns a list of video links."""
    links = []
    if os.path.exists(PLAYLIST_FILE):
    try:
    with open(PLAYLIST_FILE, 'r') as f:
    # Use strip to clean up whitespace and ensure no empty lines are added
    links = [line.strip() for line in f if line.strip()]
    except Exception as e:
    print(f"Warning: Could not read playlist file '{PLAYLIST_FILE}': {e}")
    
    # Always return the list object, even if empty, preventing NoneType error.
    return links
    
    def update_playlist_file(links):
    """Writes the current list of links back to the playlist file."""
    try:
    # 'w' mode truncates the file and writes the new content
    with open(PLAYLIST_FILE, 'w') as f:
    f.write('\n'.join(links) + '\n')
    return True
    except Exception as e:
    print(f"Error: Could not rewrite playlist file '{PLAYLIST_FILE}': {e}")
    return False
    
    def add_to_playlist(youtube_link):
    """Checks for duplicates and appends a link to the playlist file if unique."""
    links = get_playlist_links()
    
    # Check for duplicates
    if youtube_link in links:
    print(f"\n Link already exists in the playlist. Skipping addition.")
    return False
    
    # Append the new link
    links.append(youtube_link)
    
    if update_playlist_file(links):
    print(f"\n Link successfully added to the end of the playlist: {PLAYLIST_FILE}")
    return True
    return False
    
    def remove_first_from_playlist():
    """Removes the first link from the playlist file and re-writes the file."""
    links = get_playlist_links()
    
    if not links:
    return None
    
    # Remove the first item
    removed_link = links.pop(0)
    
    if update_playlist_file(links):
    print(f"\n Link successfully removed from the top of the playlist.")
    return removed_link
    return None # Indicate failure
    
    def run_yt_dlp(youtube_link, cookie_option=None, is_browser_option=False):
    """
    Attempts to download a video using yt-dlp with optional cookies.
    Prints download progress to stdout using a custom ASCII bar.
    Returns (success_boolean, stderr_output, video_title_suggestion).
    """
    # Use --get-filename to preview the filename yt-dlp would use
    # Also use --get-title to get the actual title from YouTube
    info_command = [
    YTDLP_PATH,
    '--get-title',
    '--print', '%(title)s', # Get title
    '--print', '%(id)s.%(ext)s', # Get filename suggestion
    youtube_link
    ]
    if cookie_option:
    if is_browser_option:
    info_command.extend(['--cookies-from-browser', cookie_option])
    else:
    expanded_cookies_path = os.path.expanduser(cookie_option)
    info_command.extend(['--cookies', expanded_cookies_path])
    
    video_title = None
    suggested_filename = None
    try:
    info_process = subprocess.run(info_command, capture_output=True, text=True, check=True)
    # Assuming yt-dlp prints title on first line, filename on second
    info_lines = info_process.stdout.strip().split('\n')
    if len(info_lines) >= 2:
    video_title = info_lines[0].strip()
    # Sanitize the title for use as a filename
    suggested_filename = re.sub(r'[\\/:*?"<>|]', '_', video_title)
    # Remove leading/trailing spaces, and ensure it's not empty
    suggested_filename = suggested_filename.strip()
    if not suggested_filename:
    suggested_filename = "youtube_video" # Fallback if title is empty after sanitization
    
    # Append a generic extension for the prompt, actual extension will be handled by yt-dlp
    suggested_filename += ".mp4"
    else:
    print(f"Warning: Could not get full video info. Output: {info_process.stdout.strip()}")
    except subprocess.CalledProcessError as e:
    print(f"Error getting video info: {e.stderr}")
    except Exception as e:
    print(f"An unexpected error occurred while getting video info: {e}")
    
    download_command = [
    YTDLP_PATH,
    # Prioritize separate best video and audio, then merge. Fallback to best overall mp4, then just best.
    '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
    '--merge-output-format', 'mp4', # Merge audio and video into an MP4 container.
    '--output', f"{OUTPUT_BASENAME}.%(ext)s",
    '--sponsorblock-remove', 'sponsor',
    youtube_link
    ]
    
    # Add cookies option based on whether it's a browser or a file path
    if cookie_option:
    if is_browser_option:
    download_command.extend(['--cookies-from-browser', cookie_option])
    else:
    # Expand user's home directory (e.g., '~/.config/cookies.txt')
    expanded_cookies_path = os.path.expanduser(cookie_option)
    download_command.extend(['--cookies', expanded_cookies_path])
    
    stderr_output = ""
    try:
    # Use subprocess.Popen to stream output in real-time
    process = subprocess.Popen(
    download_command,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT, # Direct stderr to stdout for real-time reading
    text=True, # Decode stdout/stderr as text
    bufsize=1 # Line-buffered output for real-time printing
    )
    
    is_download_progress_active = False
    # Read stdout line by line
    for line in iter(process.stdout.readline, ''):
    # Check if the line is a progress update from yt-dlp
    match = PROGRESS_RE.search(line)
    if match:
    is_download_progress_active = True
    percentage = float(match.group(1))
    draw_ascii_progress_bar(percentage)
    else:
    if is_download_progress_active:
    sys.stdout.write('\n') # Move to next line after progress bar
    is_download_progress_active = False # Reset flag after progress bar is finalized by a non-progress line
    sys.stdout.write(line) # Print other yt-dlp output directly
    sys.stdout.flush()
    
    # After the loop, if a progress bar was the last thing printed, ensure a newline
    if is_download_progress_active:
    sys.stdout.write('\n')
    
    # Wait for the subprocess to complete and get its return code
    return_code = process.wait()
    return return_code == 0, stderr_output, suggested_filename
    except FileNotFoundError:
    return False, f"Error: yt-dlp binary not found at '{YTDLP_PATH}'. Ensure it's downloaded and executable.", suggested_filename
    except Exception as e:
    return False, f"An unexpected error occurred during yt-dlp execution: {e}", suggested_filename
    
    def find_package_manager_install_command(package_name):
    """
    Tries to find a supported package manager and returns the installation command list.
    """
    if subprocess.run(["which", "apt"], capture_output=True).returncode == 0:
    return ["sudo", "apt", "install", "-y", package_name]
    elif subprocess.run(["which", "dnf"], capture_output=True).returncode == 0:
    return ["sudo", "dnf", "install", "-y", package_name]
    elif subprocess.run(["which", "pacman"], capture_output=True).returncode == 0:
    return ["sudo", "pacman", "-S", "--noconfirm", package_name]
    return None
    
    def _get_player_and_folder():
    """Performs pre-checks and returns the determined media player and last save folder."""
    
    # 1. Read last_save_folder
    last_save_folder = ""
    if os.path.exists(LAST_SAVE_FOLDER_FILE):
    try:
    with open(LAST_SAVE_FOLDER_FILE, 'r') as f:
    last_save_folder = f.read().strip()
    except Exception as e:
    print(f"Warning: Could not read last save folder file '{LAST_SAVE_FOLDER_FILE}': {e}")
    last_save_folder = "" # Reset if read fails
    
    print_ascii_header("PYTHON DEPENDENCY CHECKS", '-')
    
    # 2. Check for a suitable media player
    media_player = None
    mpv_available = subprocess.run(["which", "mpv"], capture_output=True).returncode == 0
    vlc_available = subprocess.run(["which", "vlc"], capture_output=True).returncode == 0
    smplayer_available = subprocess.run(["which", "smplayer"], capture_output=True).returncode == 0
    
    sys.stdout.write(" Checking for media player (mpv, vlc, smplayer)...")
    sys.stdout.flush()
    
    if mpv_available:
    media_player = "mpv"
    print(f" [OK: Using {media_player}]")
    elif vlc_available or smplayer_available:
    print(" [FAILED to find mpv, alternatives found]")
    print_ascii_line('=')
    
    print_ascii_header("MPV MISSING - ACTION REQUIRED", '#')
    
    alt_options = []
    if vlc_available: alt_options.append("'v' for VLC")
    if smplayer_available: alt_options.append("'s' for SMPlayer")
    
    choice_prompt = (
    " The preferred player 'mpv' was not found.\n"
    " Do you want to try installing 'mpv' now (requires sudo) or use an alternative player?\n"
    f" (Type 'i' for install, {' or '.join(alt_options)}, 'e' to exit): "
    )
    
    install_choice = input(choice_prompt).lower().strip()
    
    if install_choice == 'i':
    install_cmd = find_package_manager_install_command("mpv")
    
    if install_cmd:
    try:
    print(f" Attempting to run: {' '.join(install_cmd)}")
    subprocess.run(install_cmd, check=True)
    print(" mpv installed successfully. Using mpv.")
    # Create the flag file to signal the bash script for uninstallation later
    with open(MPV_INSTALLED_FLAG, 'w') as f:
    f.write('installed')
    media_player = "mpv"
    except subprocess.CalledProcessError:
    print(" Failed to install mpv. Falling back to alternative player.")
    except FileNotFoundError:
    print(" Failed to run installation command (sudo not found or similar). Falling back.")
    else:
    print(" No supported package manager (apt, dnf, pacman) found for installation. Falling back to alternative player.")
    
    if not media_player:
    if install_choice == 'v' and vlc_available:
    media_player = "vlc"
    elif install_choice == 's' and smplayer_available:
    media_player = "smplayer"
    else:
    if vlc_available: media_player = "vlc"
    elif smplayer_available: media_player = "smplayer"
    
    if not media_player:
    print(" No valid player selected or available. Exiting.")
    sys.exit(1)
    
    print(f" Using player: {media_player}")
    print_ascii_line('=')
    
    else:
    # NO players are available (mpv, vlc, or smplayer) - THIS IS THE CRITICAL NEW LOGIC BLOCK
    print(" [FAILED]")
    print(" Error: No compatible media player ('mpv', 'vlc', or 'smplayer') found in your PATH.")
    
    # --- NEW INSTALLATION PROMPT ---
    install_choice = input(
    " The required player 'mpv' is missing.\n"
    " Do you want to attempt installing 'mpv' now (requires sudo)? (y/n): "
    ).lower().strip()
    
    if install_choice == 'y':
    install_cmd = find_package_manager_install_command("mpv")
    
    if install_cmd:
    try:
    print(f" Attempting to run: {' '.join(install_cmd)}")
    subprocess.run(install_cmd, check=True)
    print(" mpv installed successfully.")
    # Create the flag file to signal the bash script for uninstallation later
    with open(MPV_INSTALLED_FLAG, 'w') as f:
    f.write('installed')
    media_player = "mpv"
    except subprocess.CalledProcessError:
    print(" Failed to install mpv. Please check permissions and try again.")
    except FileNotFoundError:
    print(" Failed to run installation command (sudo not found or similar).")
    else:
    print(" No supported package manager (apt, dnf, pacman) found for installation.")
    
    if not media_player:
    print(" No valid player found or installed. Exiting.")
    sys.exit(1) # Exit if no player is found
    
    print_ascii_line('=')
    return media_player, last_save_folder
    
    def _process_link_workflow(youtube_link, media_player, last_save_folder, flow_options):
    """
    Handles the download, play, and optional save/delete for a single video link.
    Takes the dictionary of options for playback and saving. Updates and returns the dictionary.
    Returns (success_bool, options_dict).
    """
    final_video_file = None
    suggested_filename_for_save = None
    
    # Get playback options from the dictionary (set in process_new_video)
    ontop_choice = flow_options.get('ontop_choice', 'n')
    monitor_choice = flow_options.get('monitor_choice')
    
    try:
    print_ascii_header(f"PROCESSING: {youtube_link}", '=')
    
    download_attempt_successful = False
    
    # --- Video Download Attempt (with optional cookie retry) ---
    print("\n Attempting to download video with best video and separate audio streams...")
    success, _, suggested_filename_for_save = run_yt_dlp(youtube_link)
    
    if success:
    download_attempt_successful = True
    print("\n Video and audio downloaded and merged successfully.")
    else:
    print("\n Initial video download failed.")
    
    # --- Cookie Retry Logic ---
    retry_choice = 'n'
    if 'cookie_retry_choice' in flow_options:
    retry_choice = flow_options['cookie_retry_choice']
    print(f" Cookie retry option repeated: '{'Yes' if retry_choice == 'y' else 'No'}'.")
    else:
    retry_choice = input(
    "\n The download failed. This can happen if the video is private or age-restricted. "
    "Do you want to try again with browser cookies? (y/n): "
    ).lower()
    flow_options['cookie_retry_choice'] = retry_choice
    
    if retry_choice == 'y':
    
    cookie_option_value = flow_options.get('cookie_option_value')
    is_browser = flow_options.get('is_browser', False)
    
    if cookie_option_value is None: # First run of cookie retry flow
    cookie_method_choice = input(
    " How do you want to provide cookies?\n"
    " 1. From a browser (e.g., firefox, chrome)\n"
    " 2. From a cookies.txt file\n"
    " Enter 1 or 2: "
    ).strip()
    
    if cookie_method_choice == '1':
    browser_options = {
    1: "firefox", 2: "chrome", 3: "chromium",
    4: "brave", 5: "edge", 6: "opera",
    7: "safari", 8: "vivaldi", 9: "librewolf"
    }
    print("\n Select a browser for cookies:")
    for num, browser in browser_options.items():
    print(f" {num}. {browser.capitalize()}")
    
    browser_selection_input = input(" Enter the number of your preferred browser: ").strip()
    try:
    browser_selection_num = int(browser_selection_input)
    if browser_selection_num in browser_options:
    browser_name = browser_options[browser_selection_num]
    cookie_option_value = browser_name
    is_browser = True
    print(f"\n Attempting to download video using cookies from {browser_name}...")
    else:
    print(" Invalid browser number. Falling back to cookies.txt file option.")
    cookie_method_choice = '2'
    except ValueError:
    print(" Invalid input. Please enter a number. Falling back to cookies.txt file option.")
    cookie_method_choice = '2'
    
    if cookie_method_choice == '2' or (cookie_method_choice == '1' and not is_browser):
    cookies_file_path = input(
    " Please enter the path to the cookies file "
    "(e.g., ~/.config/yt-dlp/cookies.txt or cookies.txt in current directory): "
    ).strip()
    if cookies_file_path:
    cookie_option_value = cookies_file_path
    is_browser = False
    print("\n Attempting to download video with cookies from file...")
    else:
    print(" No cookies file path provided. Cannot retry with cookies.")
    cookie_option_value = None # Ensure it's None if empty path given
    
    # Save the cookie options for repeat
    flow_options['cookie_option_value'] = cookie_option_value
    flow_options['is_browser'] = is_browser
    else:
    # Repeat run: use saved options
    if is_browser:
    print(f" Repeating cookie download using browser: {cookie_option_value}...")
    else:
    print(f" Repeating cookie download using file: {cookie_option_value}...")
    
    if cookie_option_value:
    success_retry, _, suggested_filename_for_save = run_yt_dlp(youtube_link, cookie_option_value, is_browser)
    
    if success_retry:
    download_attempt_successful = True
    print("\n Video and audio downloaded and merged successfully with cookies.")
    else:
    print(" Video download failed even with cookies.")
    else:
    print(" No cookie option was established. Not retrying.")
    
    if not download_attempt_successful:
    print("\n Failed to download video. Returning to main menu.")
    return False, flow_options
    
    # --- Find Downloaded File ---
    print_ascii_header("LOCATING VIDEO", '-')
    sys.stdout.write(" Searching for downloaded video file...")
    sys.stdout.flush()
    downloaded_files = glob.glob(f"{OUTPUT_BASENAME}.*")
    
    for f in downloaded_files:
    if f.startswith(OUTPUT_BASENAME) and (f.endswith(".mp4") or f.endswith(".webm") or f.endswith(".mkv")):
    final_video_file = f
    break
    
    if not final_video_file:
    print(" [NOT FOUND]")
    print(f" Error: Could not find a video file matching '{OUTPUT_BASENAME}.*' after download and merge.")
    return False, flow_options
    
    print(" [FOUND]")
    print(f" Identified downloaded video file: {final_video_file}")
    print_ascii_line('=')
    
    # --- Play Test Sound before Video ---
    # FIX: The play_test_sound function has been modified to use mpv with low volume (25%)
    play_test_sound()
    
    # --- Play Video ---
    print_ascii_header("PLAYING VIDEO", '-')
    player_command = [media_player, final_video_file]
    
    if media_player == "mpv":
    # Base command for mpv: fullscreen and ignore aspect ratio
    player_command = [media_player, '--fs', '--no-keepaspect', final_video_file]
    
    if ontop_choice == 'y':
    # Insert --ontop flag after the player name
    player_command.insert(1, '--ontop')
    
    # Add monitor selection if provided
    if monitor_choice is not None:
    player_command.insert(1, f'--screen={monitor_choice}')
    print(f" MPV will attempt to play on monitor index: {monitor_choice}")
    
    elif media_player == "vlc":
    player_command = [media_player, '--fullscreen', final_video_file]
    if ontop_choice == 'y':
    print(" Note: VLC does not support '--ontop' from command line; ignoring option.")
    if monitor_choice is not None:
    print(" Note: VLC does not support monitor selection from command line; ignoring option.")
    
    elif media_player == "smplayer":
    player_command = [media_player, final_video_file]
    if ontop_choice == 'y':
    print(" Note: SMPlayer does not support '--ontop' from command line; ignoring option.")
    if monitor_choice is not None:
    print(" Note: SMPlayer does not support monitor selection from command line; ignoring option.")
    
    print(f" Playing video with {media_player}: {final_video_file}")
    # Execute the detected media player
    subprocess.run(player_command, check=True)
    print(" Video playback finished.")
    print_ascii_line('=')
    
    # --- Save Video Logic ---
    print_ascii_header("SAVE VIDEO", '-')
    
    # 1. Get save_choice
    save_choice = 'n'
    if 'save_choice' in flow_options:
    save_choice = flow_options['save_choice']
    print(f" Save option repeated: '{'Save' if save_choice == 'y' else 'Do not save'}'.")
    else:
    save_choice = input(f" Do you want to save this video (current name: '{suggested_filename_for_save if suggested_filename_for_save else final_video_file}')? (y/n): ").lower()
    flow_options['save_choice'] = save_choice # Save the choice for repeat
    
    if save_choice == 'y':
    # 2. Get new filename (The name itself changes, but the *action* of using a custom name can be repeated)
    new_filename = suggested_filename_for_save
    custom_filename_format = flow_options.get('custom_filename_format')
    
    if 'custom_filename_entered' in flow_options and flow_options['custom_filename_entered'] == True:
    # This is a repeat run, and the user chose a custom name last time.
    print(f" Filename option repeated: Asking for new custom filename.")
    new_filename_input = input(f" Enter the desired filename (default: '{suggested_filename_for_save}'): ").strip()
    if new_filename_input:
    new_filename = new_filename_input
    else:
    # First run or repeat run where they used the default filename.
    new_filename_input = input(f" Enter the desired filename (default: '{suggested_filename_for_save}'): ").strip()
    
    # For the first run, track if they entered a custom name
    if 'custom_filename_entered' not in flow_options:
    flow_options['custom_filename_entered'] = (new_filename_input != "")
    
    if new_filename_input:
    new_filename = new_filename_input
    
    # Ensure the new filename has an extension
    if not os.path.splitext(new_filename)[1]:
    original_ext = os.path.splitext(final_video_file)[1]
    new_filename += original_ext
    
    # 3. Ask for save folder
    target_folder = ""
    use_previous_folder = 'n' # Default
    
    if last_save_folder and os.path.isdir(last_save_folder):
    
    if 'use_previous_folder_choice' in flow_options:
    # Repeat run: use the saved choice
    use_previous_folder = flow_options['use_previous_folder_choice']
    print(f" Folder option repeated: '{'Use previous folder' if use_previous_folder == 'y' else 'Ask for new folder'}'.")
    else:
    # First run: ask the user
    use_previous_folder = input(f" Use previous save folder '{last_save_folder}'? (y/n): ").lower()
    flow_options['use_previous_folder_choice'] = use_previous_folder
    
    if use_previous_folder == 'y':
    target_folder = last_save_folder
    else:
    target_folder = input(" Enter the new save folder path: ").strip()
    else:
    # last_save_folder is not valid, always ask
    target_folder = input(" Enter the save folder path: ").strip()
    if 'use_previous_folder_choice' not in flow_options:
    flow_options['use_previous_folder_choice'] = 'n' # Force 'n' if no last folder exists
    
    if not target_folder:
    print(" No save folder specified. Video will not be saved.")
    os.remove(final_video_file)
    print(f" Deleted unsaved video: {final_video_file}")
    return True, flow_options # Successful flow, but no save
    
    os.makedirs(target_folder, exist_ok=True)
    
    destination_path = os.path.join(target_folder, new_filename)
    try:
    shutil.move(final_video_file, destination_path)
    print(f" Video saved successfully to: {destination_path}")
    # Update last save folder file
    with open(LAST_SAVE_FOLDER_FILE, 'w') as f:
    f.write(target_folder)
    print(f" Last save folder updated to: {target_folder}")
    except OSError as e:
    print(f" Error saving video: {e}")
    print(f" The video was not moved. It remains as '{final_video_file}' in the current directory.")
    print_ascii_line('=')
    else:
    print(" Video will not be saved.")
    # --- Delete Downloaded Video if not saved ---
    if final_video_file and os.path.exists(final_video_file):
    try:
    os.remove(final_video_file)
    print(f" Deleted unsaved video: {final_video_file}")
    except Exception as e:
    print(f" Warning: Could not delete {final_video_file}. Reason: {e}")
    print_ascii_line('=')
    
    return True, flow_options # Indicate successful handling of the video flow
    
    except FileNotFoundError as e:
    print_ascii_header("RUNTIME ERROR", '#')
    print(f" Runtime Error: A required command was not found. Detail: {e}")
    print(" Please ensure all necessary applications are installed and in your system's PATH.")
    if final_video_file and os.path.exists(final_video_file):
    try: os.remove(final_video_file)
    except Exception: pass
    return False, flow_options
    except subprocess.CalledProcessError as e:
    print_ascii_header("EXECUTION ERROR", '#')
    print(f" Command Execution Error (Exit code: {e.returncode}):")
    print(f" Command: {' '.join(e.cmd)}")
    if e.stdout: print(f" Output: {e.stdout.strip()}")
    print(" Please review the error messages above for details on what went wrong.")
    if final_video_file and os.path.exists(final_video_file):
    try: os.remove(final_video_file)
    except Exception: pass
    return False, flow_options
    except Exception as e:
    print_ascii_header("UNEXPECTED ERROR", '#')
    print(f" An unexpected error occurred: {e}")
    if final_video_file and os.path.exists(final_video_file):
    try: os.remove(final_video_file)
    except Exception: pass
    return False, flow_options
    finally:
    # Aggressive cleanup of any residual 'downloaded_video.*' files that might be left over
    print_ascii_header("FINAL CLEANUP OF TEMP FILES", '-')
    print(" Checking for residual temporary files...")
    for f in glob.glob(f"{OUTPUT_BASENAME}.*"):
    if os.path.exists(f):
    try:
    os.remove(f)
    print(f" Cleaned up residual temporary file: {f}")
    except Exception as e:
    print(f" Warning: Could not clean up residual temporary file {f}. Reason: {e}")
    if os.path.exists(TEST_SOUND_FILE):
    try:
    os.remove(TEST_SOUND_FILE)
    print(f" Cleaned up temporary sound file: {TEST_SOUND_FILE}")
    except Exception as e:
    print(f" Warning: Could not clean up temporary sound file {TEST_SOUND_FILE}. Reason: {e}")
    print_ascii_line('=')
    
    def process_new_video(media_player, last_save_folder, last_options=None):
    """
    Handles the flow for a user-provided new video link.
    Returns (video_processed_bool, options_dict).
    """
    # Initialize current options from last_options or as a new dictionary
    current_options = last_options.copy() if last_options is not None else {}
    youtube_link = ""
    is_repeat_run = last_options is not None
    
    # --- 1. Get new link (ALWAYS ASK) ---
    print_ascii_header("NEW VIDEO LINK", '-')
    youtube_link = input(" Please enter the YouTube video link: ")
    if not youtube_link:
    print(" No link entered. Returning to main menu.")
    return False, current_options
    
    # --- 2. Ask about saving to playlist BEFORE download ---
    playlist_choice = 'n'
    if is_repeat_run and 'playlist_choice' in last_options:
    playlist_choice = last_options['playlist_choice']
    print(f" Playlist option repeated: '{'Add to playlist' if playlist_choice == 'y' else 'Do not add'}'.")
    else:
    playlist_choice = input(" Do you want to save this link to the playlist before playing? (y/n): ").lower()
    current_options['playlist_choice'] = playlist_choice
    
    if playlist_choice == 'y':
    add_to_playlist(youtube_link)
    
    # --- 3. Ask about playing AFTER playlist decision (ALWAYS YES IF REPEATING) ---
    play_choice = 'y'
    if not is_repeat_run:
    play_choice = input(" Do you want to download and play the video now? (y/n): ").lower()
    
    if play_choice == 'y':
    # 4. Ask about Playback Options BEFORE download (if mpv)
    ontop_choice = 'n'
    monitor_choice = None
    
    if media_player == "mpv":
    print_ascii_header("PLAYBACK OPTIONS (MPV)", '-')
    
    # --- Always On Top Option ---
    if is_repeat_run and 'ontop_choice' in last_options:
    ontop_choice = last_options['ontop_choice']
    print(f" On-Top option repeated: '{'Yes' if ontop_choice == 'y' else 'No'}'.")
    else:
    ontop_choice = input(
    " Do you want the player window to be 'Always On Top'?\n"
    " (This keeps the video visible above all other windows.) (y/n): "
    ).lower()
    current_options['ontop_choice'] = ontop_choice
    
    # --- Monitor Selection Option ---
    if is_repeat_run and 'monitor_choice' in last_options:
    monitor_choice = last_options['monitor_choice']
    print(f" Monitor option repeated: '{monitor_choice if monitor_choice is not None else 'Default monitor'}'.")
    else:
    monitor_input = input(
    " Enter the **monitor number** (e.g., 0, 1) you want to play on, or press Enter to skip: "
    ).strip()
    
    if monitor_input.isdigit():
    monitor_choice = int(monitor_input)
    elif monitor_input:
    print(" Warning: Invalid monitor number entered. Skipping monitor selection.")
    
    # Convert None to a storable value if user skipped, though it's saved as None if no input.
    current_options['monitor_choice'] = monitor_choice
    
    print_ascii_line('=')
    
    # Add the playback choices to options for _process_link_workflow to use in the player command
    current_options['ontop_choice'] = ontop_choice
    current_options['monitor_choice'] = monitor_choice
    
    # 5. Run the core flow and get the updated options from the save block
    video_processed, updated_options = _process_link_workflow(youtube_link, media_player, last_save_folder, current_options)
    
    return video_processed, updated_options
    else:
    print(" Skipping video download and playback. Returning to main menu.")
    return True, current_options # Handled successfully, but no video processed.
    
    def process_playlist_video(media_player, last_save_folder):
    """
    Handles the flow for playing videos from the playlist, including recursion for 'play next'.
    Returns True if video processing was completed, False otherwise.
    """
    
    playlist_links = get_playlist_links()
    
    if not playlist_links:
    print_ascii_header("PLAYLIST EMPTY", '-')
    print(" The playlist is currently empty.")
    
    # --- START OF NEW LOGIC ---
    add_link_choice = input(
    " The playlist is empty. Do you want to add a link now and play it? (y/n): "
    ).lower()
    
    if add_link_choice == 'y':
    youtube_link = input(" Please enter the YouTube video link: ")
    if not youtube_link:
    print(" No link entered. Returning to main menu.")
    print_ascii_line('=')
    return False
    
    # Add the link to the playlist
    add_to_playlist(youtube_link)
    
    # Recursive call to process the newly added link (which is now the only one)
    # This ensures we proceed to playback options and the download flow
    print("\n Link added. Restarting playlist flow to process the video...")
    return process_playlist_video(media_player, last_save_folder)
    # --- END OF NEW LOGIC ---
    
    print_ascii_line('=')
    return False
    
    # Get the top link
    youtube_link = playlist_links[0]
    
    print_ascii_header(f"PLAYLIST - TOP VIDEO ({len(playlist_links)} links remaining)", '=')
    print(f" Video to play: {youtube_link}")
    
    # 1. Ask about Playback Options BEFORE download (if mpv)
    ontop_choice = 'n' # Default to no
    monitor_choice = None # Default to no monitor specified
    
    # Options dictionary for the core workflow. This is not persistent.
    temp_options = {}
    
    if media_player == "mpv":
    print_ascii_header("PLAYBACK OPTIONS (MPV)", '-')
    
    # --- Always On Top Option ---
    ontop_choice = input(
    " Do you want the player window to be 'Always On Top'?\n"
    " (This keeps the video visible above all other windows.) (y/n): "
    ).lower()
    temp_options['ontop_choice'] = ontop_choice
    
    # --- Monitor Selection Option ---
    monitor_input = input(
    " Enter the **monitor number** (e.g., 0, 1) you want to play on, or press Enter to skip: "
    ).strip()
    
    if monitor_input.isdigit():
    monitor_choice = int(monitor_input)
    elif monitor_input:
    print(" Warning: Invalid monitor number entered. Skipping monitor selection.")
    
    temp_options['monitor_choice'] = monitor_choice
    
    print_ascii_line('=')
    
    # Add the playback choices to options for _process_link_workflow
    temp_options['ontop_choice'] = ontop_choice
    temp_options['monitor_choice'] = monitor_choice
    
    # Run the core download/play/save workflow
    # Note: the save block's option updates are not persisted for the playlist flow
    video_processed, _ = _process_link_workflow(youtube_link, media_player, last_save_folder, temp_options)
    
    # After playback, handle the list cleanup
    if video_processed:
    # Ask to delete the link from the list
    delete_choice = input(
    f" Do you want to delete the played link from the playlist? (y/n): "
    ).lower()
    removed = False
    
    if delete_choice == 'y':
    # This call removes the link that was just played (the first one)
    removed = remove_first_from_playlist()
    
    if removed:
    playlist_links = get_playlist_links() # Re-read for the next prompt
    
    # Ask to play the next link
    if playlist_links:
    next_choice = input(f" There are {len(playlist_links)} links remaining. Do you want to play the next one? (y/n): ").lower()
    if next_choice == 'y':
    # Recursive call to play the next one (which is now at the top)
    return process_playlist_video(media_player, last_save_folder)
    elif delete_choice == 'y': # Only print if deletion made it empty
    print(" Playlist is now empty.")
    
    return video_processed
    
    def download_and_play_video():
    """
    The main control function that handles the menu loop.
    """
    try:
    # 1. Perform pre-flight checks and get the necessary system info
    media_player, last_save_folder = _get_player_and_folder()
    except SystemExit:
    return False
    
    # State variable to hold the last set of choices for choice '1' (New Video)
    last_new_video_options = {}
    
    # Main menu loop
    while True:
    print_ascii_header("MAIN MENU", '=')
    print(" 1. Download and play a NEW YouTube video link.")
    
    playlist_links = get_playlist_links()
    num_links = len(playlist_links)
    
    if num_links > 0:
    print(f" 2. Play the top video from the PLAYLIST ({num_links} links available).")
    else:
    print(" 2. Play from PLAYLIST (Playlist is empty).")
    
    print(" 3. Exit the program and clean up.")
    
    choice = input(" Enter your choice (1, 2, 3): ").strip()
    
    video_processed = False
    
    if choice == '1':
    # Pass the stored options on the first call. The function updates and returns the new set.
    video_processed, last_new_video_options = process_new_video(media_player, last_save_folder, last_new_video_options)
    elif choice == '2':
    # The playlist processing handles the inner loop (delete/play next)
    video_processed = process_playlist_video(media_player, last_save_folder)
    elif choice == '3':
    print(" Exiting. Goodbye!")
    sys.exit(UNINSTALL_AUDIO_TOOLS_EXIT_CODE)
    else:
    print(" Invalid choice. Please enter 1, 2, or 3.")
    
    # Update last_save_folder if a save operation was successful (for next loop iteration)
    if video_processed and os.path.exists(LAST_SAVE_FOLDER_FILE):
    try:
    with open(LAST_SAVE_FOLDER_FILE, 'r') as f:
    last_save_folder = f.read().strip()
    except Exception:
    pass # Ignore read errors
    
    # --- START OF REPEAT QUESTION LOGIC (Only for NEW VIDEO flow) ---
    if video_processed and choice == '1':
    print_ascii_header("NEXT ACTION", '-')
    repeat_choice = input(
    " Would you like to repeat the same options (Download and play a **New** YouTube link)? (y/n): "
    ).lower().strip()
    
    if repeat_choice == 'y':
    print("\n" + "=" * 60) # Separator before new workflow
    
    # Inner loop for continuous repeats using the last_new_video_options
    while True:
    # Pass the stored options to process_new_video
    # This tells process_new_video to use the saved choices
    video_processed, last_new_video_options = process_new_video(media_player, last_save_folder, last_new_video_options)
    
    if not video_processed:
    break # Exit inner loop if process_new_video failed or user skipped play
    
    print_ascii_header("NEXT ACTION", '-')
    repeat_choice = input(
    " Would you like to repeat the same options (Download and play a **New** YouTube link)? (y/n): "
    ).lower().strip()
    
    if repeat_choice != 'y':
    break # Exit inner loop
    
    print("\n" + "=" * 60) # Separator before new workflow
    
    print("\n" + "=" * 60) # Main menu separator after inner loop / on 'n' choice
    
    else:
    print("\n" + "=" * 60) # Main menu separator
    # --- END OF REPEAT QUESTION LOGIC ---
    
    # Ensure the main function is called when the script is executed.
    if __name__ == "__main__":
    # Clean up residual files before the main process starts
    for f in glob.glob(f"{OUTPUT_BASENAME}.*"):
    if os.path.exists(f):
    try:
    os.remove(f)
    except Exception:
    pass
    if os.path.exists(TEST_SOUND_FILE):
    try:
    os.remove(TEST_SOUND_FILE)
    except Exception:
    pass
    
    download_and_play_video()
    EOF
    
    echo "" # Add a newline for better readability
    
    print_section_header "RUNNING PLAYER"
    # Step 4: Run the Python script
    echo " Executing Python script: $PYTHON_SCRIPT"
    # The Python script will now handle the loop and exit with a specific code when the user is done.
    python3 "$PYTHON_SCRIPT"
    PYTHON_EXIT_CODE=$? # Capture the exit code of the Python script
    
    echo "" # Add a newline for better readability
    
    print_section_header "FINAL CLEANUP"
    # Step 5: Clean up temporary files and potentially uninstall audio tools
    echo " Cleaning up shell script's temporary files..."
    
    # Remove the temporary Python script
    if [ -f "$PYTHON_SCRIPT" ]; then
    rm "$PYTHON_SCRIPT"
    echo " Removed temporary Python script: $PYTHON_SCRIPT"
    fi
    
    # Remove the yt-dlp binary as requested
    if [ -f "$YTDLP_BIN" ]; then
    rm "$YTDLP_BIN"
    echo " Removed yt-dlp binary: $YTDLP_BIN"
    fi
    
    # Condition for removing the last save folder file: only if Python script exited with 'no more videos' signal
    if [ $PYTHON_EXIT_CODE -eq 5 ] && [ -f "$LAST_SAVE_FOLDER_FILE" ]; then
    rm "$LAST_SAVE_FOLDER_FILE"
    echo " Removed last save folder file: $LAST_SAVE_FOLDER_FILE (as requested upon exit)"
    elif [ -f "$LAST_SAVE_FOLDER_FILE" ]; then
    echo " Keeping last save folder file: $LAST_SAVE_FOLDER_FILE (script did not exit via 'no more videos' option)"
    fi
    
    # Check if the Python script signaled for tool uninstallation
    if [ $PYTHON_EXIT_CODE -eq 5 ]; then # Only consider uninstalling if Python script signaled end
    
    # --- NEW: Check and uninstall mpv ---
    if [ -f "$MPV_INSTALLED_FLAG" ]; then
    echo ""
    read -p " This script installed 'mpv'. Do you want to uninstall it now? (y/n): " uninstall_mpv_confirm
    if [[ "$uninstall_mpv_confirm" =~ ^[Yy]$ ]]; then
    echo " Attempting to uninstall mpv..."
    UNINSTALL_MPV_CMD=""
    if command -v apt &> /dev/null; then
    UNINSTALL_MPV_CMD="sudo apt remove -y mpv"
    elif command -v dnf &> /dev/null; then
    UNINSTALL_MPV_CMD="sudo dnf remove -y mpv"
    elif command -v pacman &> /dev/null; then
    UNINSTALL_MPV_CMD="sudo pacman -R --noconfirm mpv"
    else
    echo " Error: No supported package manager found for uninstalling mpv."
    echo " Please uninstall mpv manually."
    fi
    
    if [ -n "$UNINSTALL_MPV_CMD" ]; then
    if eval "$UNINSTALL_MPV_CMD"; then
    echo " mpv uninstalled successfully."
    rm "$MPV_INSTALLED_FLAG" # Remove flag file
    else
    echo " Error: Failed to uninstall mpv. Please check permissions."
    fi
    fi
    else
    echo " Skipping mpv uninstallation."
    fi
    fi
    
    # --- Existing: Check and uninstall ffmpeg ---
    if [ -f "$FFMPEG_INSTALLED_FLAG" ]; then
    echo ""
    read -p " This script installed 'ffmpeg'. Do you want to uninstall it now? (y/n): " uninstall_ffmpeg_confirm
    if [[ "$uninstall_ffmpeg_confirm" =~ ^[Yy]$ ]]; then
    echo " Attempting to uninstall ffmpeg..."
    UNINSTALL_FFMPEG_CMD=""
    if command -v apt &> /dev/null; then
    UNINSTALL_FFMPEG_CMD="sudo apt remove -y ffmpeg"
    elif command -v dnf &> /dev/null; then
    UNINSTALL_FFMPEG_CMD="sudo dnf remove -y ffmpeg"
    elif command -v pacman &> /dev/null; then
    UNINSTALL_FFMPEG_CMD="sudo pacman -R --noconfirm ffmpeg"
    else
    echo " Error: No supported package manager found for uninstalling ffmpeg."
    echo " Please uninstall ffmpeg manually."
    fi
    
    if [ -n "$UNINSTALL_FFMPEG_CMD" ]; then
    if eval "$UNINSTALL_FFMPEG_CMD"; then
    echo " ffmpeg uninstalled successfully."
    rm "$FFMPEG_INSTALLED_FLAG" # Remove flag file
    else
    echo " Error: Failed to uninstall ffmpeg. Please check permissions."
    fi
    fi
    else
    echo " Skipping ffmpeg uninstallation."
    fi
    fi
    
    # --- Existing: Check and uninstall espeak-ng and alsa-utils ---
    if [ -f "$AUDIO_TOOLS_INSTALLED_FLAG" ]; then
    echo ""
    read -p " This script installed 'espeak-ng' and 'alsa-utils'. Do you want to uninstall them now? (y/n): " uninstall_audio_confirm
    if [[ "$uninstall_audio_confirm" =~ ^[Yy]$ ]]; then
    echo " Attempting to uninstall audio tools..."
    UNINSTALL_AUDIO_CMD=""
    if command -v apt &> /dev/null; then
    UNINSTALL_AUDIO_CMD="sudo apt remove -y espeak-ng alsa-utils"
    elif command -v dnf &> /dev/null; then
    UNINSTALL_AUDIO_CMD="sudo dnf remove -y espeak-ng alsa-utils"
    elif command -v pacman &> /dev/null; then
    UNINSTALL_AUDIO_CMD="sudo pacman -R --noconfirm espeak-ng alsa-utils"
    else
    echo " Error: No supported package manager found for uninstalling audio tools."
    echo " Please install espeak-ng and alsa-utils manually."
    fi
    
    if [ -n "$UNINSTALL_AUDIO_CMD" ]; then
    if eval "$UNINSTALL_AUDIO_CMD"; then
    echo " Audio tools uninstalled successfully."
    rm "$AUDIO_TOOLS_INSTALLED_FLAG" # Remove flag file
    else
    echo " Error: Failed to uninstall audio tools. Please check permissions."
    fi
    fi
    else
    echo " Skipping audio tools uninstallation."
    fi
    fi
    else # If Python script did not exit with code 5, do not offer uninstallation
    echo " Tool uninstallation not offered as script did not exit via 'no more videos' option or encountered an error."
    fi
    
    # Report final status based on the Python script's exit code
    if [ $PYTHON_EXIT_CODE -ne 0 ] && [ $PYTHON_EXIT_CODE -ne 5 ]; then
    echo " Script finished with errors (exit code: $PYTHON_EXIT_CODE)."
    exit $PYTHON_EXIT_CODE
    else
    echo " Script finished successfully."
    exit 0
    fi
    #8401
    thumbtak
    Moderator

    Updated Script

    #!/bin/bash
    
    # ASCII Art Functions
    print_banner() {
    echo "+---------------------------------+"
    echo "|===========TAKS SHACK============|"
    echo "|======https://taksshack.com======|"
    echo "+---------------------------------+"
    }
    
    print_section_header() {
    echo "---=[ $@ ]=---------------------------------------------------"
    echo ""
    }
    
    # --- Configuration ---
    YTDLP_URL="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp"
    YTDLP_BIN="./yt-dlp"
    PYTHON_SCRIPT="yt_dlp_player.py"
    OUTPUT_BASENAME="downloaded_video"
    LAST_SAVE_FOLDER_FILE=".last_save_folder"
    AUDIO_TOOLS_INSTALLED_FLAG=".audio_tools_installed_by_script_flag"
    FFMPEG_INSTALLED_FLAG=".ffmpeg_installed_by_script_flag"
    PLAYLIST_FILE="video_playlist.txt"
    
    # --- Main Script Execution ---
    print_banner
    print_section_header "SYSTEM SETUP"
    
    if [ ! -f "$YTDLP_BIN" ] || [ ! -x "$YTDLP_BIN" ]; then
    echo " yt-dlp binary not found. Downloading..."
    if command -v curl &> /dev/null; then
    curl -L "$YTDLP_URL" -o "$YTDLP_BIN"
    elif command -v wget &> /dev/null; then
    wget -O "$YTDLP_BIN" "$YTDLP_URL"
    fi
    chmod +x "$YTDLP_BIN"
    fi
    
    # Tool Check
    command -v ffmpeg &> /dev/null || (sudo apt update && sudo apt install -y ffmpeg espeak-ng alsa-utils && touch "$FFMPEG_INSTALLED_FLAG")
    
    print_section_header "PYTHON SCRIPT CREATION"
    cat <<'EOF' > "$PYTHON_SCRIPT"
    import subprocess
    import os
    import sys
    import glob
    import re
    import shutil
    
    YTDLP_PATH = "./yt-dlp"
    OUTPUT_BASENAME = "downloaded_video"
    LAST_SAVE_FOLDER_FILE = ".last_save_folder"
    PLAYLIST_FILE = "video_playlist.txt"
    PROGRESS_RE = re.compile(r'\[download\]\s+(\d+\.?\d*)%')
    
    # Global session cookie path
    session_cookie_path = None
    
    def print_ascii_header(text, char='='):
    print("-" * 60)
    print(f" {text}")
    print("-" * 60)
    
    def draw_ascii_progress_bar(percentage, bar_length=40):
    filled_len = int(bar_length * percentage // 100)
    bar = '#' * filled_len + '-' * (bar_length - filled_len)
    sys.stdout.write(f'\rDownloading: [ {bar} ] {percentage:6.2f}%')
    sys.stdout.flush()
    
    def get_monitor_choice():
    print("\n Select monitor for playback (0-9).")
    choice = input(" Monitor index [default 0]: ").strip()
    return choice if choice else "0"
    
    def show_cookie_help():
    print("\n--- COOKIE ASSISTANCE ---")
    print("1. Chrome / Brave / Opera")
    print("2. Firefox")
    print("3. Microsoft Edge")
    print("4. Safari")
    print("5. Skip instructions and enter path")
    
    b_choice = input("\nSelect your browser for instructions: ").strip()
    
    if b_choice == '1':
    print("\n[ Chrome / Chromium ]")
    print("1. Install: https://chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc")
    print("2. Open YouTube, click the extension icon, and click 'Export'.")
    print("3. Save the file and provide the path here.")
    elif b_choice == '2':
    print("\n[ Firefox ]")
    print("1. Search Firefox Add-ons for 'Export Cookies.txt'.")
    print("2. Export while on the YouTube tab.")
    print("3. Provide the saved file path here.")
    elif b_choice == '3':
    print("\n[ Microsoft Edge ]")
    print("1. Use the Chrome Web Store link (Edge supports Chrome extensions):")
    print(" https://chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc")
    print("2. Export and provide the path here.")
    elif b_choice == '4':
    print("\n[ Safari ]")
    print("1. It is recommended to use Chrome or Firefox for exporting cookies.txt.")
    print("2. Otherwise, use a 'cookies.txt' extension from the Mac App Store.")
    
    def run_yt_dlp(youtube_link, cookie_option=None):
    info_cmd = [YTDLP_PATH, '--get-title', '--print', '%(title)s', '--print', '%(id)s.%(ext)s', youtube_link]
    if cookie_option:
    info_cmd.extend(['--cookies', os.path.expanduser(cookie_option)])
    
    suggested_filename = None
    try:
    res = subprocess.run(info_cmd, capture_output=True, text=True, check=True)
    lines = res.stdout.strip().split('\n')
    if len(lines) >= 2:
    suggested_filename = re.sub(r'[\\/:*?"<>|]', '_', lines[0].strip()) + ".mp4"
    except: pass
    
    dl_cmd = [YTDLP_PATH, '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
    '--merge-output-format', 'mp4', '--output', f"{OUTPUT_BASENAME}.%(ext)s", youtube_link]
    if cookie_option:
    dl_cmd.extend(['--cookies', os.path.expanduser(cookie_option)])
    
    try:
    p = subprocess.Popen(dl_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    for line in iter(p.stdout.readline, ''):
    m = PROGRESS_RE.search(line)
    if m: draw_ascii_progress_bar(float(m.group(1)))
    return p.wait() == 0, suggested_filename
    except: return False, suggested_filename
    
    def attempt_download_with_retry(link):
    global session_cookie_path
    success, name = run_yt_dlp(link, cookie_option=session_cookie_path)
    
    if not success and not session_cookie_path:
    print("\n[!] Download failed. Link may be restricted (Age-gate/Login).")
    show_cookie_help()
    path = input("\n Please enter path to cookies.txt (or ENTER to skip): ").strip()
    if path and os.path.exists(os.path.expanduser(path)):
    session_cookie_path = path
    print(f" Retrying with cookies: {session_cookie_path}...")
    success, name = run_yt_dlp(link, cookie_option=session_cookie_path)
    elif not success and session_cookie_path:
    success, name = run_yt_dlp(link, cookie_option=session_cookie_path)
    
    return success, name
    
    def _process_link_workflow(youtube_link, last_folder, monitor_id=None, mode='stream'):
    print_ascii_header(f"PROCESSING: {youtube_link}")
    
    if monitor_id is None:
    monitor_id = get_monitor_choice()
    
    success, suggested = attempt_download_with_retry(youtube_link)
    
    if not success:
    print("\n[!] Could not process link.")
    return False, last_folder
    
    files = glob.glob(f"{OUTPUT_BASENAME}.*")
    if not files:
    print("\n[!] Video file not found after download.")
    return False, last_folder
    
    final_file = files[0]
    
    # Run playback
    subprocess.run(["mpv", "--fs", f"--screen={monitor_id}", final_file])
    
    if mode == 'download':
    save_choice = input("Save video? (y/n): ").lower()
    if save_choice == 'y':
    folder_prompt = f"Use {last_folder}? (y/n): " if last_folder else "Enter Path: "
    use_last = input(folder_prompt).lower() if last_folder else 'n'
    
    target_folder = last_folder if use_last == 'y' else input("Path: ").strip()
    
    if target_folder:
    os.makedirs(target_folder, exist_ok=True)
    dest_path = os.path.join(target_folder, suggested or final_file)
    try:
    shutil.move(final_file, dest_path)
    print(f" Saved to: {dest_path}")
    with open(LAST_SAVE_FOLDER_FILE, 'w') as f: f.write(target_folder)
    return True, target_folder
    except Exception as e:
    print(f" Error saving file: {e}")
    
    # Clean up if not saved or if in stream mode
    if os.path.exists(final_file):
    os.remove(final_file)
    return True, last_folder
    
    def silent_batch_download(last_save_folder):
    print_ascii_header("SILENT BATCH DOWNLOAD", '-')
    print(" Enter links (one per line). Press ENTER twice to begin.")
    links = []
    while True:
    l = sys.stdin.readline().strip()
    if not l: break
    links.append(l)
    if not links: return last_save_folder
    
    if last_save_folder and os.path.isdir(last_save_folder):
    target_folder = last_save_folder if input(f" Use folder '{last_save_folder}'? (y/n): ").lower() == 'y' else input(" Path: ").strip()
    else: target_folder = input(" Path: ").strip()
    
    if not target_folder: return last_save_folder
    os.makedirs(target_folder, exist_ok=True)
    
    dupe_action = None
    for i, link in enumerate(links):
    print(f"\n[{i+1}/{len(links)}] {link}")
    success, name = attempt_download_with_retry(link)
    
    if success:
    if name:
    dest = os.path.join(target_folder, name)
    if os.path.exists(dest):
    if not dupe_action:
    choice = input(f" '{name}' exists. (s)kip or (o)verwrite all? ").lower()
    dupe_action = 'overwrite' if choice == 'o' else 'skip'
    if dupe_action == 'skip': continue
    else: os.remove(dest)
    
    files = glob.glob(f"{OUTPUT_BASENAME}.*")
    if files: shutil.move(files[0], os.path.join(target_folder, name or files[0]))
    else:
    print(f" Skipping {link} due to failure.")
    
    with open(LAST_SAVE_FOLDER_FILE, 'w') as f: f.write(target_folder)
    return target_folder
    
    def main():
    last_folder = ""
    if os.path.exists(LAST_SAVE_FOLDER_FILE):
    with open(LAST_SAVE_FOLDER_FILE, 'r') as f: last_folder = f.read().strip()
    
    while True:
    print_ascii_header("MAIN MENU", '=')
    if session_cookie_path: print(f" SESSION COOKIES: {session_cookie_path}")
    print(" 1. Stream a SINGLE YouTube video link")
    print(" 2. Download and play a SINGLE YouTube video link")
    print(" 3. Download and play MULTIPLE YouTube video links")
    
    p_links = []
    if os.path.exists(PLAYLIST_FILE):
    with open(PLAYLIST_FILE, 'r') as f: p_links = [l for l in f if l.strip()]
    
    print(f" 4. Play from PLAYLIST ({len(p_links)} links)")
    print(" 5. Silent Batch DOWNLOAD (No playback)")
    print(" 6. Exit and clean up")
    
    choice = input(" Choice: ").strip()
    if choice == '1':
    link = input("Link: ")
    _process_link_workflow(link, last_folder, mode='stream')
    elif choice == '2':
    link = input("Link: ")
    _, last_folder = _process_link_workflow(link, last_folder, mode='download')
    elif choice == '3':
    print("Enter links (double enter to start):")
    links = []
    while True:
    l = sys.stdin.readline().strip()
    if not l: break
    links.append(l)
    if links:
    m_id = get_monitor_choice()
    for l in links: _, last_folder = _process_link_workflow(l, last_folder, monitor_id=m_id, mode='download')
    elif choice == '4':
    if p_links:
    m_id = get_monitor_choice()
    for l in p_links: _process_link_workflow(l.strip(), last_folder, monitor_id=m_id, mode='stream')
    elif choice == '5':
    last_folder = silent_batch_download(last_folder)
    elif choice == '6':
    sys.exit(5)
    
    if __name__ == "__main__":
    main()
    EOF
    
    # --- FINAL CLEANUP BASH LOGIC ---
    print_section_header "RUNNING PLAYER"
    python3 "$PYTHON_SCRIPT"
    PYTHON_EXIT_CODE=$?
    
    [ -f "$PYTHON_SCRIPT" ] && rm "$PYTHON_SCRIPT"
    [ -f "$YTDLP_BIN" ] && rm "$YTDLP_BIN"
    
    if [ $PYTHON_EXIT_CODE -eq 5 ]; then
    if [ -f "$AUDIO_TOOLS_INSTALLED_FLAG" ]; then
    echo ""
    read -p " This script installed audio tools. Uninstall them now? (y/n): " uninstall_confirm
    if [[ "$uninstall_confirm" =~ ^[Yy]$ ]]; then
    sudo apt remove -y espeak-ng alsa-utils && rm "$AUDIO_TOOLS_INSTALLED_FLAG"
    echo " Tools uninstalled."
    fi
    fi
    fi
    
    echo " Done."
    exit 0
Viewing 12 posts - 16 through 27 (of 27 total)
  • You must be logged in to reply to this topic.
TAKs Shack