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 14 replies, 1 voice, and was last updated 4 days, 15 hours ago by
thumbtak.
-
AuthorPosts
-
October 6, 2023 at 8:44 am #7026
thumbtak
ModeratorInstall Tampermonkey [Chrome] [Opera] [Firefox]Click uBlock => Gears Icon => Filter lists => CustomAdd the following URL:Click [Apply changes]Browser Settings => Privacy and security => Cookies and Other site data => Toggle on, Send a “Do Not Track” request with your browser trafficYou are now done. Enjoy!For more info, visit the following link:
October 9, 2023 at 1:13 pm #7029thumbtak
ModeratorI have been working hard to try and combat this annoying issue. I do not want to allow ads as they can be a security risk, well the ones on YouTube as they have tracking. Anyway, I found another way around it, that seems to be working for now, they keep making code changes. The way I found only works in Linux and requires you to install an application, or two.Open TerminalRun the following command in terminal:
$ sudo apt install snapd
Step number two installs an application store that is commonly used on Linux.Run the following command in terminal:
$ sudo snap install red-app
Step number three installs an application called “Red – Youtube Client for Linux”.Open the app called “Red” or “red-app”.Make sure you check for updates by occasionally running the command,
$ sudo snap refresh
You are now finished. Hopefully this works.Note: If anyone is having issues, let us know, and I am sure one of use will have something that can add to help.
October 9, 2023 at 4:22 pm #7033thumbtak
ModeratorI found another Linux solution that might work for people.
- Install the following applications in terminal:
$ sudo apt install mpv
$ sudo apt install snapd
$ sudo snap install youtube-dl-pro
- Open DL-YOUTUBE
- Copy and paste the YouTube link in the app.
- Click Download or Play
- Enjoy!
-
This reply was modified 1 year, 8 months ago by
thumbtak. Reason: Info redacted. Look below
October 20, 2023 at 5:10 pm #7040thumbtak
ModeratorI finally found a workaround. It takes a little configuring, but here is how you do it.
- Go to: https://support.google.com/accounts/answer/3024190?hl=en
- Click: Download your data
- Click: Deselect all
- Check: YouTube and YouTube Music
- Click: All YouTube dad including
- Click: Deselect all
- Check: subscriptions
- Click: OK
- Click: Next step
- Change: 2GB => 50 GB
- Click: Create export
- Wait for e-mail
- Open e-mail
- Click: Download your file
- Go to download location
- Unzip or uncompress file
- Go to https://piped.drgns.space/
- Click: Register
- Make an account
- Click: Login
- Fill in account info
- Click: Feed
- Click: Subscriptions
- Click: Import from JSON/CSV
- Click: Choose File
- Open CSV file from the file you downloaded
- Click: Import
- Enjoy ad free and no sponsors
October 24, 2023 at 9:55 am #7046thumbtak
ModeratorUpdate
The piped sites were giving me issues today and yesterday. You can view the website and copy over your playlist, but for now the videos are not working.Workaround
You can use Invidious (https://yt.oelrichsgarcia.de/) to make YouTube video work again. It also allows subscriptions importing.Positives
Ads skipped.Negatives
Sponsors not skipped.November 16, 2023 at 10:19 am #7050thumbtak
ModeratorDecember 2, 2023 at 8:23 pm #7055thumbtak
Moderatorpiped.dragon.space is still not working, so if you need an alternative one, where your login should still work, and all your saved data, you can use the link below.
January 18, 2024 at 10:51 pm #7093thumbtak
ModeratorAn update to this post on how you can get YouTube to work constantly.
- Install Piped Watch for your browser [Chrome] [Firefox] [Brave]
- Check Piped sites to see which ones work.
- Open YouTube.
- Right-Click the video you want to watch and open in a new tab.
- The video will open, and you should be able to watch the video.
Note: You might need to check the quality as when set to auto, it might use a very low quality.
Note [2]: You can open the video by clicking the video like normal, and press F5 on the new page to open it in a Piped site.
Note [3]: Repeat step 2 if the Piped site is not working or the video is not loading in a Piped site.March 4, 2025 at 12:56 pm #7992thumbtak
ModeratorThere are two ways to avoid as on YouTube. Option one has a GUI, but option 2 removes the ads from the videos and works better.
Option 1
$ sudo apt install snapd $ sudo snap install ytdownloader
Option 2
$ sudo apt install python3-pip $ mkdir ~/Videos/Youtube/ $ cd ~/Videos/Youtube/ $ python3 -m venv yt-dlp-venv $ source yt-dlp-venv/bin/activate $ pip install --upgrade yt-dlp $ yt-dlp your_youtube_video_url
Option 2 [Script] Save as yt-dlp.sh
#!/bin/bash # Install dependencies (if not already installed) if ! command -v python3 &> /dev/null; then sudo apt install python3 -y fi if ! command -v python3-pip &> /dev/null; then sudo apt install python3-pip -y fi # Create or enter the download directory DOWNLOAD_DIR="$HOME/Videos/Youtube" mkdir -p "$DOWNLOAD_DIR" cd "$DOWNLOAD_DIR" # Create and activate a virtual environment VENV_DIR="$DOWNLOAD_DIR/yt-dlp-venv" if [ ! -d "$VENV_DIR" ]; then python3 -m venv "$VENV_DIR" fi source "$VENV_DIR/bin/activate" # Use the virtual environment's python executable "$VENV_DIR/bin/python3" -m pip install --upgrade yt-dlp # Prompt the user for the YouTube link read -p "Enter the YouTube video URL: " YOUTUBE_URL # Download the video if [[ -n "$YOUTUBE_URL" ]]; then yt-dlp "$YOUTUBE_URL" else echo "No URL provided." fi # Deactivate the virtual environment deactivate
Option 2 [Run Script]
$ bash yt-dlp.sh
June 10, 2025 at 7:28 pm #8068thumbtak
ModeratorHere is a script that downloads videos from YouTube, plays them in smplayer, and deletes the video after it is done. This will also remove sponsor ads and other ads.
1.
$ sudo apt install smplayer
2. Check “Options => Preferences => Video => Start video in fullscreen”
3. Check “Options => Preferences => General => Close when finished playback”
4.$ mkdir ~/Documents/Scripts
5.$ cd ~/Documents/Scripts
6.$ mousepad YouTube-Ad-Free.sh
7. Use the script below:Updated script below
8.
$ bash YouTube-Ad-Free.sh
-
This reply was modified 1 month ago by
thumbtak.
-
This reply was modified 1 month ago by
thumbtak. Reason: Updated script to include, cookie grabbing, if needed, with percent for download, and changed it to delete yt-dlp file left behind in the old script
-
This reply was modified 4 weeks, 1 day ago by
thumbtak. Reason: Beautification
-
This reply was modified 3 weeks, 2 days ago by
thumbtak. Reason: TTS added to wake up some speakers, Watch another video question, save video to location question
-
This reply was modified 3 weeks, 2 days ago by
thumbtak. Reason: Bugfix
-
This reply was modified 3 weeks, 2 days ago by
thumbtak. Reason: Saves location where you last saved the file. When the you saw no, to another, it will remove the saved location
-
This reply was modified 3 weeks, 2 days ago by
thumbtak.
June 14, 2025 at 4:32 pm #8073thumbtak
ModeratorI updated the script to make it look more beautiful, and less tossed together.
June 20, 2025 at 2:15 pm #8083thumbtak
ModeratorAnother update:
- TTS added to wake up some speakers
- Watch another video question
- Save video to location question
June 29, 2025 at 4:52 pm #8096thumbtak
ModeratorUpdated script from post #8068
Update: The script now downloads video, seperatly downloads audio, combines them before playing with ffmpeg. This is to make sure you get the best video quality and audio quality.
#!/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" # This file will now be conditionally deleted in cleanup # 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" # <--- NEW: Flag for ffmpeg installation # --- 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 # <--- NEW: FFMPEG check variable 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 # <--- NEW: FFMPEG check 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 # <--- MODIFIED: Include ffmpeg in check read -p " Some required tools (espeak-ng, aplay, ffmpeg) are missing. Do you want to install them? (y/n): " install_tools_choice # <--- MODIFIED: Updated prompt 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" # <--- MODIFIED: Add ffmpeg elif command -v dnf &> /dev/null; then INSTALL_CMD="sudo dnf install -y espeak-ng alsa-utils ffmpeg" # <--- MODIFIED: Add ffmpeg elif command -v pacman &> /dev/null; then INSTALL_CMD="sudo pacman -S --noconfirm espeak-ng alsa-utils ffmpeg" # <--- MODIFIED: Add 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." # <--- MODIFIED: Updated message fi if [ -n "$INSTALL_CMD" ]; then if eval "$INSTALL_CMD"; then echo " Required tools installed successfully." touch "$AUDIO_TOOLS_INSTALLED_FLAG" # Create flag file for audio tools touch "$FFMPEG_INSTALLED_FLAG" # <--- NEW: Create flag file for ffmpeg else echo " Error: Failed to install required tools. Please check permissions or internet connection." # <--- MODIFIED: Updated message fi fi else echo " Skipping installation of missing tools. Script functionality may be limited or fail." # <--- MODIFIED: Updated message # 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 "" # Add a newline for better readability # Removed the direct read of LAST_SAVE_FOLDER into a bash variable here, # as the Python script will now handle reading it dynamically from the file. # The environment variable is no longer strictly necessary for this purpose, # but keeping it doesn't harm. 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" # 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*)%') 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 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}") # Continue without title/filename if info gathering fails except Exception as e: print(f"An unexpected error occurred while getting video info: {e}") download_command = [ YTDLP_PATH, # --- START MODIFICATION FOR AUDIO --- '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', # Prioritize separate best video and audio, then merge. Fallback to best overall mp4, then just best. '--merge-output-format', 'mp4', # This tells yt-dlp to merge the audio and video if downloaded separately, into an MP4 container. # --- END MODIFICATION FOR AUDIO --- '--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.PIPE, 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 a progress bar was active, print a newline before other output # to prevent other messages from overwriting the bar. if is_download_progress_active: sys.stdout.write('\n') # Move to next line after progress bar is_download_progress_active = False 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 # so the next output starts on a fresh line. if is_download_progress_active: sys.stdout.write('\n') # The flag <code>is_download_progress_active</code> is reset by the shell script's cleanup # Read any remaining content from stderr (usually error messages) stderr_output = process.stderr.read() # 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 download_and_play_video(): """ Prompts for a YouTube link, attempts to download the video (with cookie retry logic), plays it, and then handles saving/deleting based on user choice. Returns True if video processing was completed (saved or deleted), False otherwise. """ final_video_file = None # Initialize to None to ensure it's always defined for cleanup # --- NEW: Read last_save_folder directly from the file in Python every time --- 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 try: # --- Pre-flight Checks (for Python specific dependencies) --- print_ascii_header("PYTHON DEPENDENCY CHECKS", '-') # 1. Check if smplayer is installed and accessible in the system's PATH. sys.stdout.write(" Checking for smplayer...") sys.stdout.flush() # Ensure message is displayed immediately if subprocess.run(["which", "smplayer"], capture_output=True).returncode != 0: print(" [FAILED]") print(" Error: 'smplayer' is not found in your system's PATH.") print(" Please ensure smplayer is installed and its executable is in your PATH.") sys.exit(1) # Exit if smplayer is not found print(" [OK]") # 2. Check if the yt-dlp binary exists and is executable. sys.stdout.write(f" Checking for yt-dlp at '{YTDLP_PATH}'...") sys.stdout.flush() # Ensure message is displayed immediately 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}'.") print(" Please ensure the shell script has successfully downloaded and set permissions for yt-dlp.") sys.exit(1) # Exit if yt-dlp is not ready print(" [OK]") # 3. Check for ffmpeg. This is crucial for merging audio and video. 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.") print(" ffmpeg is required by yt-dlp to merge video and audio streams.") print(" Please ensure ffmpeg is installed (e.g., 'sudo apt install ffmpeg' on Debian/Ubuntu).") sys.exit(1) # Exit if ffmpeg is not found print(" [OK]") print_ascii_line('=') # Separator line # --- User Input --- print_ascii_header("VIDEO LINK", '-') youtube_link = input(" Please enter the YouTube video link: ") if not youtube_link: print(" No link entered. Exiting video processing.") return False # Indicate that no video was processed print_ascii_header(f"PROCESSING: {youtube_link}", '=') download_attempt_successful = False suggested_filename_for_save = None # --- First Video Download Attempt (without cookies) --- print("\n Attempting to download video with best video and separate audio streams...") success, stderr, 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.") print(" yt-dlp output (stderr):\n" + stderr) # Show error for debugging # Check if error output suggests cookies might be needed for authentication needs_cookie_keywords = [ "private video", "age restricted", "login required", "sign in", "authentication", "cookies", "access denied" ] should_suggest_cookies = any(keyword in stderr.lower() for keyword in needs_cookie_keywords) if should_suggest_cookies: retry_choice = input( "\n The download failed, possibly due to requiring authentication or cookies. " "Do you want to try again (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' # Fallback to file option except ValueError: print(" Invalid input. Please enter a number. Falling back to cookies.txt file option.") cookie_method_choice = '2' # Fallback to file option if cookie_method_choice == '2' or (cookie_method_choice == '1' and not is_browser): # Fallback or direct choice for file 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 # Explicitly set to false for file path 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, stderr_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.") print(" yt-dlp output (stderr with cookies):\n" + stderr_retry) else: print(" No valid cookie option provided. Cannot retry with cookies.") else: print(" Not retrying. Exiting video processing.") else: print(" Download failed for reasons not immediately indicating a cookie requirement.") if not download_attempt_successful: print("\n Failed to download video. Exiting script.") return False # Indicate that no video was processed successfully # --- Find Downloaded File --- print_ascii_header("LOCATING VIDEO", '-') sys.stdout.write(" Searching for downloaded video file...") sys.stdout.flush() # yt-dlp with --merge-output-format will create a single file, typically .mp4 downloaded_files = glob.glob(f"{OUTPUT_BASENAME}.*") # Filter for the most likely playable video file. Prioritize common video extensions. 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 # Found a suitable file, exit loop if not final_video_file: print(" [NOT FOUND]") print(f" Error: Could not find a video file matching '{OUTPUT_BASENAME}.*' after download and merge.") print(f" Please check the directory for downloaded files. Found: {downloaded_files}") return False # Indicate failure print(" [FOUND]") print(f" Identified downloaded video file: {final_video_file}") print_ascii_line('=') # Separator line # --- Play Test Sound before Video --- # The bash script ensures espeak-ng/aplay are installed if user opted for it play_test_sound() # --- Play Video --- print_ascii_header("PLAYING VIDEO", '-') print(f" Playing video with smplayer: {final_video_file}") subprocess.run(["smplayer", final_video_file], check=True) # Execute smplayer print(" Video playback finished.") print_ascii_line('=') # Separator 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]: # Get the extension from the downloaded file 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) # Delete if user decides not to save after all print(f" Deleted unsaved video: {final_video_file}") return True # Indicate that the process was handled, but video not saved # Ensure target folder exists os.makedirs(target_folder, exist_ok=True) destination_path = os.path.join(target_folder, new_filename) try: # Using shutil.move for cross-device compatibility shutil.move(final_video_file, destination_path) print(f" Video saved successfully to: {destination_path}") # Update last save folder file (now this update will be effective for next loop iteration) 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.") # Don't delete, let the user manually handle if move failed print_ascii_line('=') # Separator 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(" It might be in use or permissions are incorrect.") print_ascii_line('=') # Separator line return True # Indicate that the video processing was completed (either saved or deleted) 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 (smplayer, curl/wget, python3) are installed and in your system's PATH.") return False # Indicate failure 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" Stdout: {e.stdout.decode('utf-8')}") if e.stderr: print(f" Stderr: {e.stderr.decode('utf-8')}") print(" Please review the error messages above for details on what went wrong.") return False # Indicate failure except Exception as e: print_ascii_header("UNEXPECTED ERROR", '#') print(f" An unexpected error occurred: {e}") return False # Indicate failure finally: # Aggressive cleanup of any residual 'downloaded_video.*' files that might be left over # This acts as a fallback if explicit deletion failed or was skipped 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): # Ensure file still exists before trying to remove 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}") # Also clean up the test sound file if for some reason it wasn't already removed 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('=') # Separator line # Ensure the main function is called when the script is executed. if __name__ == "__main__": while True: video_processed_successfully = download_and_play_video() watch_another = input("\n Would you like to watch another YouTube video? (y/n): ").lower() if watch_another != 'y': print(" Exiting. Goodbye!") sys.exit(UNINSTALL_AUDIO_TOOLS_EXIT_CODE) # Signal bash script to uninstall # If 'y', loop continues, nothing special to exit with. 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:wq/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
July 4, 2025 at 2:53 pm #8103thumbtak
ModeratorWe have updated the program to run in python.
The screenshot, above, depends on your system theme. It could look different, but the general layout is the same.
You will have to do the following if you want to run the new program.
Commands:
import tkinter as tk from tkinter import filedialog, messagebox 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). OUTPUT_BASENAME = "downloaded_video" # 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 = 20 # Increased rows to make squares smaller _GRID_COLS = 80 # Increased columns to make squares smaller _GRID_LINE_THICKNESS = 2 # Thickness of the grid lines (Increased for more prominent lines) # _BLOCK_SIZE, _GRID_CANVAS_WIDTH, _GRID_CANVAS_HEIGHT will be calculated dynamically 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 # Updated application title to "Defrag YouTube Video" self.root.title("Defrag YouTube Video") # Set a default window size, allowing it to be resized by the user. self.root.geometry("1200x900") # Increased initial window size self.root.resizable(True, True) self.root.configure(bg=_WINDOW_BG) # Set root window background # Variables to store the path of the currently downloaded video # and its suggested title from YouTube. self.downloaded_video_path = None self.suggested_video_title = None self.smplayer_process = None # To store the subprocess object for SMPlayer # Store references to the grid blocks for dynamic coloring self.grid_rects = [] # Changed to store Canvas rectangle IDs # Load the last saved folder path for saving videos. self.last_save_folder = self._load_last_save_folder() # Initialize Tkinter BooleanVars for checkboxes self.play_after_download_var = tk.BooleanVar(value=True) # Default to True (checked) # Removed self.auto_delete_after_play_var as per user request # Initialize the GUI widgets. self._create_widgets() # Perform initial checks for external system dependencies. 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: # Log a warning if the file cannot be read or parsed. 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 = [] # --- Critical Dependencies --- # smplayer is used for playing the downloaded video. if subprocess.run(["which", "smplayer"], capture_output=True).returncode != 0: missing_critical.append("smplayer") else: self._log("smplayer: [OK]", "green") # ffmpeg is required by yt-dlp to merge video and audio streams. if subprocess.run(["which", "ffmpeg"], capture_output=True).returncode != 0: missing_critical.append("ffmpeg") else: self._log("ffmpeg: [OK]", "green") # --- Optional Dependencies (for audio test) --- # espeak-ng and aplay are used to play a short "wake-up" sound. 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") # Handle critical missing dependencies. 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() # Close the application if critical dependencies are missing. else: self._log("All critical dependencies are installed.", "green") self._log("Dependency check complete.", "blue") # --- yt-dlp Update Check --- self._log("Checking for yt-dlp updates...", "blue") try: installed_version = yt_dlp.version.__version__ self._log(f"Installed yt-dlp version: {installed_version}") # Run pip list --outdated and capture output pip_outdated_process = subprocess.run( [sys.executable, "-m", "pip", "list", "--outdated"], # Use sys.executable for robust pip call 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") # Ask user if they want to update 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 update."): 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") self._log(update_process.stdout, "green") messagebox.showinfo("Update Complete", "yt-dlp has been updated. Please restart the application to use the new version.") self.root.destroy() # Close the current application instance # Restart the application 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") # Check for the "externally-managed-environment" error 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") self._log(force_update_process.stdout, "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: # This specific error means yt_dlp.version.__version__ was not found. # This can happen with older yt-dlp installations or if it's not installed correctly. 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 for padding and consistent layout. main_frame = tk.Frame(self.root, bg=_WINDOW_BG) main_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=15) # Header Label for the application title. 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) # Add the clickable URL below the title self.url_label = tk.Label(main_frame, text="https://taksshack.com", foreground=_TEXT_FG, # Changed to black 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 Input Section --- 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) # Entry widget for the user to paste the YouTube video link. 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) # Bind the Enter key to trigger the download. # Changed to call start_process() self.url_entry.bind("<Return>", lambda event: self.start_process()) # Button to initiate the download. Renamed to "START" self.start_button = tk.Button(url_frame, text="START", command=self.start_process, width=10, font=_GENERAL_FONT, bg=_BUTTON_FACE, fg=_TEXT_FG, relief=tk.RAISED, borderwidth=2) self.start_button.pack(side=tk.RIGHT, padx=(0, 5), pady=5) # --- Checkboxes Frame (for Play After Download) --- checkbox_frame = tk.Frame(main_frame, bg=_WINDOW_BG) checkbox_frame.pack(pady=(5, 10), anchor=tk.W, padx=5) # Play After Download Checkbox 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)) # --- Cookie Options Section --- 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) # Radio buttons to select the cookie method. self.cookie_method = tk.StringVar(value="none") # Default: No cookies 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) # Option menu for selecting a browser if "From Browser" is chosen. self.browser_var = tk.StringVar(value="firefox") # Default browser 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) # Entry widget for the cookie file path if "From File" is chosen. 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) # Button to browse for a cookie file. 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) # --- Download Progress Section (Defragmenter-style Grid) --- progress_section_frame = tk.Frame(main_frame, bg=_WINDOW_BG) progress_section_frame.pack(pady=10, padx=5, fill=tk.X) # Grid container frame with sunken relief and calculated size 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) # Fill both X and Y, expand # Bind the <Configure> event to redraw the grid when the canvas size changes self.grid_canvas.bind("<Configure>", self._on_canvas_resize) # Initial drawing of the grid (will be updated on resize) self.grid_rects = [] # Ensure it's empty before first draw # Progress percentage label below the grid 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)) # Progress bar below the percentage (for fine-grained progress) 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 for the progress colors legend_frame = tk.Frame(progress_section_frame, bg=_WINDOW_BG) legend_frame.pack(pady=(5, 0), fill=tk.X) # Helper function for legend items 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 Buttons Section (Play, Save, Delete) --- 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) # --- Activity Log Section --- 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) # Text widget to display logs and messages to the user. 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) # Scrollbar for the log text area. 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) # Configure tags for different log message colors. 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") # Initial call to set the correct cookie input states 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() # After redrawing, ensure the progress is updated to reflect the current download state # (if a download is in progress) # This is a simplified approach; a more robust solution might store the last known percentage # and apply it here. For now, it will just reset to 0% visually if no download is active. 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") # Clear all existing items on the canvas self.grid_rects = [] # Reset the list of rectangle IDs 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: # Fallback for initial calls before widgets are fully rendered # or if the window is minimized. self.root.update_idletasks() # Force update to get actual dimensions 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 # Still zero, exit # Calculate block dimensions based on filling the actual canvas size. # These will be float values. block_width_float = canvas_width_actual / _GRID_COLS block_height_float = canvas_height_actual / _GRID_ROWS # Draw rectangles for r in range(_GRID_ROWS): for c in range(_GRID_COLS): x1 = int(c * block_width_float) y1 = int(r * block_height_float) # For the last column, ensure x2 reaches the absolute right edge of the canvas. # For the last row, ensure y2 reaches the absolute bottom edge of the canvas. 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) # Draw horizontal grid lines for r in range(_GRID_ROWS + 1): y = int(r * block_height_float) if r == _GRID_ROWS: # Ensure the very last line is at the bottom edge y = canvas_height_actual self.grid_canvas.create_line(0, y, canvas_width_actual, y, fill=_DARK_GRAY, width=_GRID_LINE_THICKNESS) # Draw vertical grid lines for c in range(_GRID_COLS + 1): x = int(c * block_width_float) if c == _GRID_COLS: # Ensure the very last line is at the right edge 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") # messagebox.showwarning("Paste Error", "Clipboard is empty or inaccessible.") # Suppress pop-up for smoother start except Exception as e: self._log(f"An error occurred while pasting: {e}", "red") # messagebox.showerror("Paste Error", f"An error occurred while pasting: {e}") # Suppress pop-up def start_process(self): """ Combines pasting the URL and starting the download process. This is the new command for the "START" button. """ self._paste_url() # First, try to paste from clipboard self.start_download() # Then, start the download with the content in the entry 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": # Enable the main OptionMenu widget self.browser_menu.config(state=tk.NORMAL) # Enable its internal menu (the dropdown options) 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) # Explicitly disable the internal menu options when not in use 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) # Explicitly disable the internal menu options when not in use 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) # Enable editing self.log_text.insert(tk.END, message + "\n", color) # Insert message self.log_text.see(tk.END) # Scroll to the end self.log_text.config(state=tk.DISABLED) # Disable editing def _update_progress(self, d): """ Callback function for yt-dlp to update the custom progress bar and label. This function is called frequently by yt-dlp during the download process. """ total_blocks = _GRID_ROWS * _GRID_COLS if d['status'] == 'downloading': total_bytes = d.get('total_bytes') or d.get('total_bytes_estimate') downloaded_bytes = d.get('downloaded_bytes') if total_bytes and downloaded_bytes: percent = (downloaded_bytes / total_bytes) * 100 # Update progress label self.progress_label.config(text=f"{percent:.0f}% completed") # Update small progress bar canvas_width = self.small_progress_bar.winfo_width() if canvas_width == 1: # Initial state before widget is fully rendered self.root.update_idletasks() canvas_width = self.small_progress_bar.winfo_width() if canvas_width == 1: # Still not rendered, skip update for now return fill_width = (percent / 100) * canvas_width self.small_progress_bar.coords(self.small_progress_fill, 0, 0, fill_width, 10) # Update grid blocks completed_blocks = int((percent / 100) * total_blocks) current_block_index = completed_blocks # The block that is currently "in progress" # Ensure grid_rects is not empty before attempting to update if not self.grid_rects: self._log("Warning: grid_rects is empty, cannot update progress visually.", "orange") return for i in range(total_blocks): # Use itemconfig to change fill color of existing rectangle if i < completed_blocks: self.grid_canvas.itemconfig(self.grid_rects[i], fill=_DEFRAGMENTED_COLOR) # Downloaded elif i == current_block_index: self.grid_canvas.itemconfig(self.grid_rects[i], fill=_IN_PROGRESS_COLOR) # In progress else: self.grid_canvas.itemconfig(self.grid_rects[i], fill=_NOT_DEFRAGMENTED_COLOR) # Not downloaded self.root.update_idletasks() # Force GUI update immediately elif d['status'] == 'finished': # Ensure all blocks are "downloaded" and progress is 100% if self.grid_rects: # Check if grid_rects is populated for rect_id in self.grid_rects: # Iterate through rectangle IDs self.grid_canvas.itemconfig(rect_id, fill=_DEFRAGMENTED_COLOR) canvas_width = self.small_progress_bar.winfo_width() self.small_progress_bar.coords(self.small_progress_fill, 0, 0, canvas_width, 10) self.progress_label.config(text="100% completed - Finished") self._log("Download finished successfully.", "green") self.root.update_idletasks() def start_download(self): """ Initiates the video download process. It validates the URL and starts the download in a separate thread to keep the GUI responsive. """ youtube_link = self.url_entry.get().strip() # --- URL Validation --- # Regex to validate YouTube video URLs. # This pattern covers common YouTube video and short URLs. youtube_regex = ( r'(https?://)?(www\.)?' '(youtube|youtu|youtube-nocookie)\.(com|be)/' '(watch\?v=|embed/|v/|.+\?v=|)' '([a-zA-Z0-9_-]{11})' ) if not re.match(youtube_regex, youtube_link): messagebox.showwarning("Input Error", "Please enter a valid YouTube video link.") self._log(f"Invalid URL entered: {youtube_link}", "red") return # Disable action buttons during the download process. self.start_button.config(state=tk.DISABLED) # Changed from download_button self.play_button.config(state=tk.DISABLED) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) # Reset progress bar and grid for a new download. # Call _draw_grid to reset the visual state of the grid self._draw_grid() self.small_progress_bar.coords(self.small_progress_fill, 0, 0, 0, 10) self.progress_label.config(text="0% completed") self._log("\n--- Starting New Download ---", "blue") self._log(f"Downloading: {youtube_link}") # Run the download operation in a separate thread. # This prevents the GUI from freezing while yt-dlp is working. download_thread = threading.Thread(target=self._download_video_thread, args=(youtube_link,)) download_thread.start() def _download_video_thread(self, youtube_link): """ The actual video download logic, executed in a separate thread. Uses the yt-dlp Python library. """ self.downloaded_video_path = None self.suggested_video_title = None # yt-dlp options configuration. ydl_opts = { # Prioritize best quality video (mp4) and audio (m4a), then merge them. # Fallback to best overall mp4, then just best. 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', 'merge_output_format': 'mp4', # Ensure merged output is MP4. # Output template for the downloaded file. # yt-dlp will automatically append the correct extension. 'outtmpl': f'{OUTPUT_BASENAME}.%(ext)s', 'progress_hooks': [self._update_progress], # Callback for progress updates. 'postprocessors': [{ 'key': 'SponsorBlock', 'categories': ['sponsor'] # Remove sponsor segments. }], 'quiet': True, # Suppress most console output from yt-dlp itself. 'no_warnings': True, # Suppress warnings from yt-dlp. } # Apply cookie options based on user selection. cookie_method = self.cookie_method.get() if cookie_method == "browser": browser_name = self.browser_var.get() if browser_name: # Ensure a browser is selected # Pass the browser name as a list, as yt-dlp expects a sequence for cookiesfrombrowser ydl_opts['cookiesfrombrowser'] = [browser_name] self._log(f"Using cookies from browser: {browser_name}", "blue") else: self._log("Warning: No browser selected for cookies. Proceeding without browser cookies.", "orange") # No messagebox here, just log, as it's optional. elif cookie_method == "file": cookie_file = self.cookie_file_entry.get().strip() # Expand user's home directory (e.g., ~/.config/cookies.txt) expanded_cookie_file = os.path.expanduser(cookie_file) if cookie_file and os.path.exists(expanded_cookie_file): ydl_opts['cookiefile'] = 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.")) # Reset cookie options in GUI if file is invalid. self.root.after(0, lambda: self.cookie_method.set("none")) self.root.after(0, self._toggle_cookie_input) # Update GUI on main thread try: with yt_dlp.YoutubeDL(ydl_opts) as ydl: # Extract information and download the video. info_dict = ydl.extract_info(youtube_link, download=True) # Get the video title and sanitize it for use as a filename. self.suggested_video_title = info_dict.get('title', 'downloaded_video') self.suggested_video_title = re.sub(r'[\\/:*?"<>|]', '_', self.suggested_video_title) self.suggested_video_title = self.suggested_video_title.strip() if not self.suggested_video_title: self.suggested_video_title = "youtube_video" # Fallback title # Find the actual downloaded file. # yt-dlp creates a file starting with OUTPUT_BASENAME and an extension. downloaded_files = glob.glob(f"{OUTPUT_BASENAME}.*") for f in downloaded_files: # Prioritize common video extensions. if f.startswith(OUTPUT_BASENAME) and (f.endswith(".mp4") or f.endswith(".webm") or f.endswith(".mkv")): self.downloaded_video_path = f break if not self.downloaded_video_path: raise Exception("Downloaded video file not found after yt-dlp completion.") self._log(f"Video '{self.suggested_video_title}' downloaded to: {self.downloaded_video_path}", "green") self._play_test_sound() # Play the test sound after successful download. # Check if the "Play video after download" checkbox is checked if self.play_after_download_var.get(): self.root.after(0, self.play_video) # Call play_video on the main thread # Enable action buttons on the main thread after download is complete. 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)) except yt_dlp.utils.DownloadError as e: self._log(f"Download Error: {e}", "red") self.root.after(0, lambda: messagebox.showerror("Download Error", f"Failed to download video: {e}")) except Exception as e: self._log(f"An unexpected error occurred: {e}", "red") self.root.after(0, lambda: messagebox.showerror("Error", f"An unexpected error occurred during download: {e}")) finally: # Re-enable the start button on the main thread. self.root.after(0, lambda: self.start_button.config(state=tk.NORMAL)) # Changed from download_button 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: # Generate WAV file from text. subprocess.run(["espeak-ng", "-w", TEST_SOUND_FILE, test_text], check=True, capture_output=True) # Play the generated WAV file. 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: # Clean up the temporary sound file. 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. Auto-delete functionality has been removed; videos are not deleted after playing. """ 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.") return self._log(f"Playing video: {self.downloaded_video_path}", "blue") try: # Launch smplayer in a non-blocking way (Popen) so the GUI remains responsive. self.smplayer_process = subprocess.Popen(["smplayer", self.downloaded_video_path]) self._log("smplayer launched. Check your desktop for the video player window.", "green") # Start a thread to monitor SMPlayer (only for cleanup, not auto-delete) monitor_thread = threading.Thread(target=self._monitor_playback_and_cleanup) monitor_thread.daemon = True # Allow the main program to exit even if this thread is running 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") except Exception as e: messagebox.showerror("Playback Error", f"An error occurred during playback: {e}") self._log(f"Error during playback: {e}", "red") def _monitor_playback_and_cleanup(self): """ Monitors the SMPlayer process. This is now primarily for knowing when playback is finished, not for auto-deletion. This runs in a separate thread. """ if self.smplayer_process: self._log("Monitoring SMPlayer process for closure...", "blue") self.smplayer_process.wait() # Wait until SMPlayer process terminates self.root.after(0, lambda: self._log("SMPlayer process closed.", "blue")) self.smplayer_process = None # Clear the process reference def save_video(self): """ Allows the user to save the downloaded video to a chosen location. Uses a file dialog for path selection and moves the file. """ if not self.downloaded_video_path or not os.path.exists(self.downloaded_video_path): messagebox.showwarning("Save Error", "No video downloaded or file not found to save.") return self._log("Initiating video save process...", "blue") # Log for saving in progress # Determine the original file extension. original_ext = os.path.splitext(self.downloaded_video_path)[1] # Suggest a filename based on the video title and original extension. initial_filename = f"{self.suggested_video_title}{original_ext}" if self.suggested_video_title else os.path.basename(self.downloaded_video_path) # Set the initial directory for the file dialog to the last saved folder, # or the user's home directory if no last folder is set or it's invalid. initial_dir = self.last_save_folder if os.path.isdir(self.last_save_folder) else os.path.expanduser("~") # Open a "Save As" file 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: # Move the downloaded file to the new location. shutil.move(self.downloaded_video_path, save_path) self._log(f"Video saved successfully to: {save_path}", "green") self.downloaded_video_path = None # Mark as moved, no longer managed by app. self.suggested_video_title = None # Reset title. # Disable action buttons as the video has been saved/moved. self.play_button.config(state=tk.DISABLED) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) # Update the last save folder for future use. 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: # Only log if it's an auto-delete attempt on a non-existent file 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: try: os.remove(self.downloaded_video_path) self._log(f"Deleted downloaded video: {self.downloaded_video_path}", "green") self.downloaded_video_path = None # Mark as deleted. self.suggested_video_title = None # Reset title. # Disable action buttons as the video has been deleted. self.play_button.config(state=tk.DISABLED) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) except Exception as e: messagebox.showerror("Delete Error", f"Failed to delete video: {e}") self._log(f"Error deleting video: {e}", "red") elif confirm: # Only log cancellation if it was a user-initiated delete self._log("Video deletion cancelled by user.", "blue") def cleanup_on_exit(self): """ Performs final cleanup of any leftover temporary files (downloaded video, test sound file) when the application window is closed. """ self._log("Performing final cleanup...", "blue") # Clean up any residual 'downloaded_video.*' files that might be left over. for f in glob.glob(f"{OUTPUT_BASENAME}.*"): if os.path.exists(f): # Ensure file still exists before trying to remove. try: os.remove(f) self._log(f"Cleaned up residual temporary file: {f}", "blue") except Exception as e: self._log(f"Warning: Could not clean up temporary file {f}. Reason: {e}", "orange") # Clean up the temporary test 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") self._log("Cleanup complete. Exiting application.", "blue") self.root.destroy() # Destroy the Tkinter root window to close the application. if __name__ == "__main__": # Create the main Tkinter window. root = tk.Tk() # Create an instance of the application. app = YouTubeDownloaderApp(root) # Bind the cleanup function to the window close event. root.protocol("WM_DELETE_WINDOW", app.cleanup_on_exit) # Start the Tkinter event loop. root.mainloop()
Save the code and run the command with:
$ python3 youtube-watcher.py
Note:
This can take a moment to open, and don’t close the terminal window you used to open the app. If you do, this will close the app. The best to do is minimize it. If you want to add a menu item, you can also use the command to add the app to your applications menu, or panel, for a shortcut.-
This reply was modified 1 week, 2 days ago by
thumbtak. Reason: Added checkbox called "Auto delete video after playing"
-
This reply was modified 1 week, 2 days ago by
thumbtak. Reason: Removed popup "asking if user wants to delete the video" after the box is checked for auto delete
-
This reply was modified 1 week, 2 days ago by
thumbtak. Reason: Paste button added, and grab button changed to download icon
-
This reply was modified 1 week, 2 days ago by
thumbtak. Reason: Theme and layout changed
-
This reply was modified 1 week, 2 days ago by
thumbtak. Reason: Script overhaul to fix bug
-
This reply was modified 1 week, 2 days ago by
thumbtak. Reason: Cookies weren't working do to an issue where each letter was being sent from the browser, not as a single word. This was fixed
July 9, 2025 at 4:09 pm #8135thumbtak
ModeratorUpdated:
import tkinter as tk from tkinter import filedialog, messagebox 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). OUTPUT_BASENAME = "downloaded_video" # 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 = 20 # Increased rows to make squares smaller _GRID_COLS = 80 # Increased columns to make squares smaller _GRID_LINE_THICKNESS = 2 # Thickness of the grid lines (Increased for more prominent lines) # _BLOCK_SIZE, _GRID_CANVAS_WIDTH, _GRID_CANVAS_HEIGHT will be calculated dynamically 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 # Updated application title to "Defrag YouTube Video" self.root.title("Defrag YouTube Video") # Set a default window size, allowing it to be resized by the user. self.root.geometry("1200x900") # Increased initial window size self.root.resizable(True, True) self.root.configure(bg=_WINDOW_BG) # Set root window background # Variables to store the path of the currently downloaded video # and its suggested title from YouTube. self.downloaded_video_path = None self.suggested_video_title = None self.smplayer_process = None # To store the subprocess object for SMPlayer # Store references to the grid blocks for dynamic coloring self.grid_rects = [] # Changed to store Canvas rectangle IDs # Load the last saved folder path for saving videos. self.last_save_folder = self._load_last_save_folder() # Initialize Tkinter BooleanVars for checkboxes self.play_after_download_var = tk.BooleanVar(value=True) # Default to True (checked) # New: Auto-save and Auto-delete options self.auto_save_after_download_var = tk.BooleanVar(value=False) # Default to False self.auto_delete_after_play_var = tk.BooleanVar(value=False) # Default to False # Initialize the GUI widgets. self._create_widgets() # Perform initial checks for external system dependencies. 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: # Log a warning if the file cannot be read or parsed. 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 = [] # --- Critical Dependencies --- # smplayer is used for playing the downloaded video. if subprocess.run(["which", "smplayer"], capture_output=True).returncode != 0: missing_critical.append("smplayer") else: self._log("smplayer: [OK]", "green") # ffmpeg is required by yt-dlp to merge video and audio streams. if subprocess.run(["which", "ffmpeg"], capture_output=True).returncode != 0: missing_critical.append("ffmpeg") else: self._log("ffmpeg: [OK]", "green") # --- Optional Dependencies (for audio test) --- # espeak-ng and aplay are used to play a short "wake-up" sound. 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") # Handle critical missing dependencies. 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() # Close the application if critical dependencies are missing. else: self._log("All critical dependencies are installed.", "green") self._log("Dependency check complete.", "blue") # --- yt-dlp Update Check --- self._log("Checking for yt-dlp updates...", "blue") try: installed_version = yt_dlp.version.__version__ self._log(f"Installed yt-dlp version: {installed_version}") # Run pip list --outdated and capture output pip_outdated_process = subprocess.run( [sys.executable, "-m", "pip", "list", "--outdated"], # Use sys.executable for robust pip call 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") # Ask user if they want to update 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") self._log(update_process.stdout, "green") messagebox.showinfo("Update Complete", "yt-dlp has been updated. Please restart the application to use the new version.") self.root.destroy() # Close the current application instance # Restart the application 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") # Check for the "externally-managed-environment" error 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") self._log(force_update_process.stdout, "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: # This specific error means yt_dlp.version.__version__ was not found. # This can happen with older yt-dlp installations or if it's not installed correctly. 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 for padding and consistent layout. main_frame = tk.Frame(self.root, bg=_WINDOW_BG) main_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=15) # Header Label for the application title. 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) # Add the clickable URL below the title self.url_label = tk.Label(main_frame, text="https://taksshack.com", foreground=_TEXT_FG, # Changed to black background=_WINDOW_BG, font=_MONOSPACE_FONT, cursor="") self.url_label.pack(pady=(0, 10)) self.url_label.bind("", lambda e: self._open_url("https://taksshack.com")) # --- URL Input Section --- 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) # Entry widget for the user to paste the YouTube video link. 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) # Bind the Enter key to trigger the download. # Changed to call start_process() self.url_entry.bind("", lambda event: self.start_process()) # Button to initiate the download. Renamed to "START" self.start_button = tk.Button(url_frame, text="START", command=self.start_process, width=10, font=_GENERAL_FONT, bg=_BUTTON_FACE, fg=_TEXT_FG, relief=tk.RAISED, borderwidth=2) self.start_button.pack(side=tk.RIGHT, padx=(0, 5), pady=5) # --- Checkboxes Frame (for Play After Download) --- checkbox_frame = tk.Frame(main_frame, bg=_WINDOW_BG) checkbox_frame.pack(pady=(5, 10), anchor=tk.W, padx=5) # Play After Download Checkbox 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)) # New: Auto-save checkbox 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)) # New: Auto-delete checkbox 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 Options Section --- 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) # Radio buttons to select the cookie method. self.cookie_method = tk.StringVar(value="none") # Default: No cookies 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) # Option menu for selecting a browser if "From Browser" is chosen. self.browser_var = tk.StringVar(value="firefox") # Default browser 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) # Entry widget for the cookie file path if "From File" is chosen. 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) # Button to browse for a cookie file. 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) # --- Download Progress Section (Defragmenter-style Grid) --- progress_section_frame = tk.Frame(main_frame, bg=_WINDOW_BG) progress_section_frame.pack(pady=10, padx=5, fill=tk.X) # Grid container frame with sunken relief and calculated size 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) # Bind the event to redraw the grid when the canvas size changes self.grid_canvas.bind("", self._on_canvas_resize) # Initial drawing of the grid (will be updated on resize) self.grid_rects = [] # Ensure it's empty before first draw # Progress percentage label below the grid 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)) # Progress bar below the percentage (for fine-grained progress) 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 for the progress colors legend_frame = tk.Frame(progress_section_frame, bg=_WINDOW_BG) legend_frame.pack(pady=(5, 0), fill=tk.X) # Helper function for legend items 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 Buttons Section (Play, Save, Delete) --- 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) # --- Activity Log Section --- 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) # Text widget to display logs and messages to the user. 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) # Scrollbar for the log text area. 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) # Configure tags for different log message colors. 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") # Initial call to set the correct cookie input states self._toggle_cookie_input() def _on_canvas_resize(self, event): """ Callback function for the event of the grid_canvas. Redraws the grid whenever the canvas is resized. """ self._draw_grid() # After redrawing, ensure the progress is updated to reflect the current download state # (if a download is in progress) # This is a simplified approach; a more robust solution might store the last known percentage # and apply it here. For now, it will just reset to 0% visually if no download is active. 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") # Clear all existing items on the canvas self.grid_rects = [] # Reset the list of rectangle IDs 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: # Fallback for initial calls before widgets are fully rendered # or if the window is minimized. self.root.update_idletasks() # Force update to get actual dimensions 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 # Still zero, exit # Calculate block dimensions based on filling the actual canvas size. # These will be float values. block_width_float = canvas_width_actual / _GRID_COLS block_height_float = canvas_height_actual / _GRID_ROWS # Draw rectangles for r in range(_GRID_ROWS): for c in range(_GRID_COLS): x1 = int(c * block_width_float) y1 = int(r * block_height_float) # For the last column, ensure x2 reaches the absolute right edge of the canvas. # For the last row, ensure y2 reaches the absolute bottom edge of the canvas. 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) # Draw horizontal grid lines for r in range(_GRID_ROWS + 1): y = int(r * block_height_float) if r == _GRID_ROWS: # Ensure the very last line is at the bottom edge y = canvas_height_actual self.grid_canvas.create_line(0, y, canvas_width_actual, y, fill=_DARK_GRAY, width=_GRID_LINE_THICKNESS) # Draw vertical grid lines for c in range(_GRID_COLS + 1): x = int(c * block_width_float) if c == _GRID_COLS: # Ensure the very last line is at the right edge 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") # messagebox.showwarning("Paste Error", "Clipboard is empty or inaccessible.") # Suppress pop-up for smoother start except Exception as e: self._log(f"An error occurred while pasting: {e}", "red") # messagebox.showerror("Paste Error", f"An error occurred while pasting: {e}") # Suppress pop-up def start_process(self): """ Combines pasting the URL and starting the download process. This is the new command for the "START" button. """ self._paste_url() # First, try to paste from clipboard self.start_download() # Then, start the download with the content in the entry 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": # Enable the main OptionMenu widget self.browser_menu.config(state=tk.NORMAL) # Enable its internal menu (the dropdown options) 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) # Explicitly disable the internal menu options when not in use 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) # Explicitly disable the internal menu options when not in use 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) # Enable editing self.log_text.insert(tk.END, message + "\n", color) # Insert message self.log_text.see(tk.END) # Scroll to the end self.log_text.config(state=tk.DISABLED) # Disable editing def _update_progress(self, d): """ Callback function for yt-dlp to update the custom progress bar and label. This function is called frequently by yt-dlp during the download process. """ total_blocks = _GRID_ROWS * _GRID_COLS if d['status'] == 'downloading': total_bytes = d.get('total_bytes') or d.get('total_bytes_estimate') downloaded_bytes = d.get('downloaded_bytes') if total_bytes and downloaded_bytes: percent = (downloaded_bytes / total_bytes) * 100 # Update progress label self.progress_label.config(text=f"{percent:.0f}% completed") # Update small progress bar canvas_width = self.small_progress_bar.winfo_width() if canvas_width == 1: # Initial state before widget is fully rendered self.root.update_idletasks() canvas_width = self.small_progress_bar.winfo_width() if canvas_width == 1: # Still not rendered, skip update for now return fill_width = (percent / 100) * canvas_width self.small_progress_bar.coords(self.small_progress_fill, 0, 0, fill_width, 10) # Update grid blocks completed_blocks = int((percent / 100) * total_blocks) current_block_index = completed_blocks # The block that is currently "in progress" # Ensure grid_rects is not empty before attempting to update if not self.grid_rects: self._log("Warning: grid_rects is empty, cannot update progress visually.", "orange") return for i in range(total_blocks): # Use itemconfig to change fill color of existing rectangle if i < completed_blocks: self.grid_canvas.itemconfig(self.grid_rects[i], fill=_DEFRAGMENTED_COLOR) # Downloaded elif i == current_block_index: self.grid_canvas.itemconfig(self.grid_rects[i], fill=_IN_PROGRESS_COLOR) # In progress else: self.grid_canvas.itemconfig(self.grid_rects[i], fill=_NOT_DEFRAGMENTED_COLOR) # Not downloaded self.root.update_idletasks() # Force GUI update immediately elif d['status'] == 'finished': # Ensure all blocks are "downloaded" and progress is 100% if self.grid_rects: # Check if grid_rects is populated for rect_id in self.grid_rects: # Iterate through rectangle IDs self.grid_canvas.itemconfig(rect_id, fill=_DEFRAGMENTED_COLOR) canvas_width = self.small_progress_bar.winfo_width() self.small_progress_bar.coords(self.small_progress_fill, 0, 0, canvas_width, 10) self.progress_label.config(text="100% completed - Finished") self._log("Download finished successfully.", "green") self.root.update_idletasks() def start_download(self): """ Initiates the video download process. It validates the URL and starts the download in a separate thread to keep the GUI responsive. """ youtube_link = self.url_entry.get().strip() # --- URL Validation --- # Regex to validate YouTube video URLs. # This pattern covers common YouTube video and short URLs. youtube_regex = ( r'(https?://)?(www\.)?' '(youtube|youtu|youtube-nocookie)\.(com|be)/' '(watch\?v=|embed/|v/|.+\?v=|)' '([a-zA-Z0-9_-]{11})' ) if not re.match(youtube_regex, youtube_link): messagebox.showwarning("Input Error", "Please enter a valid YouTube video link.") self._log(f"Invalid URL entered: {youtube_link}", "red") return # Disable action buttons during the download process. self.start_button.config(state=tk.DISABLED) # Changed from download_button self.play_button.config(state=tk.DISABLED) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) # Reset progress bar and grid for a new download. # Call _draw_grid to reset the visual state of the grid self._draw_grid() self.small_progress_bar.coords(self.small_progress_fill, 0, 0, 0, 10) self.progress_label.config(text="0% completed") self._log("\n--- Starting New Download ---", "blue") self._log(f"Downloading: {youtube_link}") # Run the download operation in a separate thread. # This prevents the GUI from freezing while yt-dlp is working. download_thread = threading.Thread(target=self._download_video_thread, args=(youtube_link,)) download_thread.start() def _download_video_thread(self, youtube_link, retry_attempt=0): """ The actual video download logic, executed in a separate thread. Uses the yt-dlp Python library. Includes a retry mechanism for format errors. """ self.downloaded_video_path = None self.suggested_video_title = None # yt-dlp options configuration. ydl_opts = { # Prioritize best quality video (mp4) and audio (m4a), then merge them. # Fallback to best overall mp4, then just best. 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', 'merge_output_format': 'mp4', # Ensure merged output is MP4. # Output template for the downloaded file. # yt-dlp will automatically append the correct extension. 'outtmpl': f'{OUTPUT_BASENAME}.%(ext)s', 'progress_hooks': [self._update_progress], # Callback for progress updates. 'postprocessors': [{ 'key': 'SponsorBlock', 'categories': ['sponsor'] # Remove sponsor segments. }], 'quiet': True, # Suppress most console output from yt-dlp itself. 'no_warnings': True, # Suppress warnings from yt-dlp. } # Apply cookie options based on user selection. cookie_method = self.cookie_method.get() if cookie_method == "browser": browser_name = self.browser_var.get() if browser_name: # Ensure a browser is selected # Pass the browser name as a list, as yt-dlp expects a sequence for cookiesfrombrowser ydl_opts['cookiesfrombrowser'] = [browser_name] self._log(f"Using cookies from browser: {browser_name}", "blue") else: self._log("Warning: No browser selected for cookies. Proceeding without browser cookies.", "orange") # No messagebox here, just log, as it's optional. elif cookie_method == "file": cookie_file = self.cookie_file_entry.get().strip() # Expand user's home directory (e.g., ~/.config/cookies.txt) expanded_cookie_file = os.path.expanduser(cookie_file) if cookie_file and os.path.exists(expanded_cookie_file): ydl_opts['cookiefile'] = 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.")) # Reset cookie options in GUI if file is invalid. self.root.after(0, lambda: self.cookie_method.set("none")) self.root.after(0, self._toggle_cookie_input) # Update GUI on main thread try: with yt_dlp.YoutubeDL(ydl_opts) as ydl: # Extract information and download the video. info_dict = ydl.extract_info(youtube_link, download=True) # Get the video title and sanitize it for use as a filename. self.suggested_video_title = info_dict.get('title', 'downloaded_video') self.suggested_video_title = re.sub(r'[\\/:*?"<>|]', '_', self.suggested_video_title) self.suggested_video_title = self.suggested_video_title.strip() if not self.suggested_video_title: self.suggested_video_title = "youtube_video" # Fallback title # Find the actual downloaded file. # yt-dlp creates a file starting with OUTPUT_BASENAME and an extension. downloaded_files = glob.glob(f"{OUTPUT_BASENAME}.*") for f in downloaded_files: # Prioritize common video extensions. if f.startswith(OUTPUT_BASENAME) and (f.endswith(".mp4") or f.endswith(".webm") or f.endswith(".mkv")): self.downloaded_video_path = f break if not self.downloaded_video_path: raise Exception("Downloaded video file not found after yt-dlp completion.") self._log(f"Video '{self.suggested_video_title}' downloaded to: {self.downloaded_video_path}", "green") self._play_test_sound() # Play the test sound after successful download. # Initiate the post-download flow on the main thread self.root.after(0, self._initiate_post_download_flow) # Enable action buttons on the main thread after download is complete. 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)) except yt_dlp.utils.DownloadError as e: error_message = str(e) if "Requested format is not available" in error_message and retry_attempt == 0: self._log(f"Download Error: {error_message}. Retrying download (Attempt 1)...", "orange") # Clear any partially downloaded files before retrying for f in glob.glob(f"{OUTPUT_BASENAME}.*"): if os.path.exists(f): try: os.remove(f) except Exception as cleanup_e: self._log(f"Warning: Could not clean up partial file {f} before retry: {cleanup_e}", "orange") self.root.after(100, lambda: self._download_video_thread(youtube_link, retry_attempt=1)) # Retry on main thread else: self._log(f"Download Error: {error_message}", "red") self.root.after(0, lambda: messagebox.showerror("Download Error", f"Failed to download video: {e}")) except Exception as e: self._log(f"An unexpected error occurred: {e}", "red") self.root.after(0, lambda: messagebox.showerror("Error", f"An unexpected error occurred during download: {e}")) finally: # Re-enable the start button on the main thread, but only if not retrying if retry_attempt == 0: self.root.after(0, lambda: self.start_button.config(state=tk.NORMAL)) # Changed from download_button 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: # Generate WAV file from text. subprocess.run(["espeak-ng", "-w", TEST_SOUND_FILE, test_text], check=True, capture_output=True) # Play the generated WAV file. 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: # Clean up the temporary sound file. 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.") return self._log(f"Playing video: {self.downloaded_video_path}", "blue") try: # Launch smplayer in a non-blocking way (Popen) so the GUI remains responsive. self.smplayer_process = subprocess.Popen(["smplayer", self.downloaded_video_path]) self._log("smplayer launched. Check your desktop for the video player window.", "green") # Start a thread to monitor SMPlayer for closure and potentially auto-delete/auto-save monitor_thread = threading.Thread(target=self._monitor_playback_and_cleanup) monitor_thread.daemon = True # Allow the main program to exit even if this thread is running 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") except Exception as e: messagebox.showerror("Playback Error", f"An error occurred during playback: {e}") self._log(f"Error during playback: {e}", "red") 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() # Wait until SMPlayer process terminates self.root.after(0, lambda: self._log("SMPlayer process closed.", "blue")) self.smplayer_process = None # Clear the process reference # Perform post-playback actions on the main thread self.root.after(0, self._handle_post_playback_actions) 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: # If playing, the _monitor_playback_and_cleanup will handle subsequent actions self.play_video() else: # If not playing, handle auto-save and auto-delete immediately if save_checked: self.save_video(auto_save=True) # This will COPY the file if delete_checked: # Special message if ONLY auto-delete is checked and no play/save 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) def _handle_post_playback_actions(self): """ Handles auto-save and auto-delete actions after video playback has finished. Called on the main thread. """ # If auto-save is checked, save the video after playback if self.auto_save_after_download_var.get(): self.save_video(auto_save=True) # This will COPY the file # If auto-delete is checked, delete the video after playback if self.auto_delete_after_play_var.get(): self.delete_downloaded_video(confirm=False) 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 moves the file. If auto_save is True, it attempts to save to the last used folder without a dialog, and COPIES the file instead of moving it, so the temporary file remains. """ 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] initial_filename = f"{self.suggested_video_title}{original_ext}" if self.suggested_video_title else os.path.basename(self.downloaded_video_path) save_path = None 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: # Use shutil.copy2 to preserve metadata, keeps original temp file shutil.copy2(self.downloaded_video_path, save_path) self._log(f"Video copied successfully to: {save_path}", "green") # Do NOT set downloaded_video_path to None here, as the temp file still exists 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 # Exit after auto-save attempt, as manual dialog is not needed else: self._log("Auto-save failed: Last save folder not set or invalid. Opening manual save dialog.", "orange") # Fallback to manual save if auto-save path is invalid # Continue to the manual save dialog below # Original manual save logic (using filedialog and shutil.move) initial_dir = self.last_save_folder if os.path.isdir(self.last_save_folder) else os.path.expanduser("~") 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) # For manual save, we MOVE the file shutil.move(self.downloaded_video_path, save_path) self._log(f"Video saved successfully to: {save_path}", "green") self.downloaded_video_path = None # Mark as moved, temp file is gone self.suggested_video_title = None # Disable action buttons as the video has been saved/moved. self.play_button.config(state=tk.DISABLED) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) 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: # Only log if it's an auto-delete attempt on a non-existent file 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: try: os.remove(self.downloaded_video_path) self._log(f"Deleted downloaded video: {self.downloaded_video_path}", "green") self.downloaded_video_path = None # Mark as deleted. self.suggested_video_title = None # Reset title. # Disable action buttons as the video has been deleted. self.play_button.config(state=tk.DISABLED) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) except Exception as e: messagebox.showerror("Delete Error", f"Failed to delete video: {e}") self._log(f"Error deleting video: {e}", "red") elif confirm: # Only log cancellation if it was a user-initiated delete self._log("Video deletion cancelled by user.", "blue") def cleanup_on_exit(self): """ Performs final cleanup of any leftover temporary files (downloaded video, test sound file) when the application window is closed. """ self._log("Performing final cleanup...", "blue") # Clean up any residual 'downloaded_video.*' files that might be left over. for f in glob.glob(f"{OUTPUT_BASENAME}.*"): if os.path.exists(f): # Ensure file still exists before trying to remove. try: os.remove(f) self._log(f"Cleaned up residual temporary file: {f}", "blue") except Exception as e: self._log(f"Warning: Could not clean up temporary file {f}. Reason: {e}", "orange") # Clean up the temporary test 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") self._log("Cleanup complete. Exiting application.", "blue") self.root.destroy() # Destroy the Tkinter root window to close the application. if __name__ == "__main__": # Create the main Tkinter window. root = tk.Tk() # Create an instance of the application. app = YouTubeDownloaderApp(root) # Bind the cleanup function to the window close event. root.protocol("WM_DELETE_WINDOW", app.cleanup_on_exit) # Start the Tkinter event loop. root.mainloop()
Note:
Can you find the easter egg. -
AuthorPosts
- You must be logged in to reply to this topic.