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