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