From 0cb40f43386db1bfa9e476a29d362194392321d4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 16 Aug 2025 15:05:27 +0300 Subject: [PATCH] Refactor and enhance FFmpeg installer module Moved ytsage_ffmpeg.py to src/core and refactored for improved logging and error handling. Added SHA-256 verification for downloads, updated FFmpeg URLs to 7.1.1, and implemented fallback extraction methods. Introduced get_ffmpeg_path utility and replaced print statements with logger calls for better integration. --- ytsage_ffmpeg.py => src/core/ytsage_ffmpeg.py | 174 ++++++++++++++---- 1 file changed, 138 insertions(+), 36 deletions(-) rename ytsage_ffmpeg.py => src/core/ytsage_ffmpeg.py (52%) diff --git a/ytsage_ffmpeg.py b/src/core/ytsage_ffmpeg.py similarity index 52% rename from ytsage_ffmpeg.py rename to src/core/ytsage_ffmpeg.py index 9441cb7..4a423f2 100644 --- a/ytsage_ffmpeg.py +++ b/src/core/ytsage_ffmpeg.py @@ -4,8 +4,10 @@ import subprocess import requests import shutil import tempfile +import hashlib from pathlib import Path from PySide6.QtGui import QIcon +from .ytsage_logging import logger def check_7zip_installed(): """Check if 7-Zip is installed on Windows.""" @@ -38,13 +40,45 @@ def download_file(url, dest_path, progress_callback=None): progress_callback(f"⚡ Downloading FFmpeg components... {progress}%") return True except requests.RequestException as e: - print(f"Download error: {str(e)}") + logger.info(f"Download error: {str(e)}") + return False + +def get_file_sha256(file_path): + """Calculate SHA-256 hash of a file.""" + sha256_hash = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + sha256_hash.update(chunk) + return sha256_hash.hexdigest() + +def verify_sha256(file_path, expected_hash_url): + """Verify file SHA-256 hash against expected hash from URL.""" + try: + # Download the SHA-256 hash + response = requests.get(expected_hash_url, timeout=10) + response.raise_for_status() + expected_hash = response.text.strip().split()[0] # Get just the hash part + + # Calculate actual hash + actual_hash = get_file_sha256(file_path) + + # Compare hashes + if actual_hash.lower() == expected_hash.lower(): + logger.info("SHA-256 verification successful!") + return True + else: + logger.error(f"SHA-256 verification failed!") + logger.info(f"Expected: {expected_hash}") + logger.info(f"Actual: {actual_hash}") + return False + except Exception as e: + logger.info(f"⚠️ SHA-256 verification error: {str(e)}") return False def get_ffmpeg_install_path(): """Get the FFmpeg installation path.""" if sys.platform == 'win32': - return os.path.join(os.getenv('LOCALAPPDATA'), 'ffmpeg', 'ffmpeg-7.1-full_build', 'bin') + return os.path.join(os.getenv('LOCALAPPDATA'), 'ffmpeg', 'ffmpeg-7.1.1-full_build', 'bin') elif sys.platform == 'darwin': paths = ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin'] for path in paths: @@ -54,6 +88,54 @@ def get_ffmpeg_install_path(): else: return '/usr/bin' # Standard Linux path +def get_ffmpeg_path(): + """ + Get the FFmpeg executable path, either from PATH or installation directory. + Returns: + str: Path to FFmpeg executable or 'ffmpeg' if found in PATH but path unknown + """ + try: + # First try to find ffmpeg in PATH using 'where' on Windows or 'which' on Unix + if sys.platform == 'win32': + # On Windows, use 'where' command and hide console window + startupinfo = None + if hasattr(subprocess, 'STARTUPINFO'): + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = 0 # SW_HIDE + + result = subprocess.run( + ['where', 'ffmpeg'], + capture_output=True, + text=True, + check=False, + startupinfo=startupinfo + ) + if result.returncode == 0 and result.stdout.strip(): + ffmpeg_path = result.stdout.strip().split('\n')[0] + return ffmpeg_path + else: + # On Unix systems, use 'which' command + result = subprocess.run(['which', 'ffmpeg'], capture_output=True, text=True, check=False) + if result.returncode == 0 and result.stdout.strip(): + ffmpeg_path = result.stdout.strip() + return ffmpeg_path + except Exception as e: + logger.error(f"Error finding ffmpeg in PATH: {e}") + + # If not found in PATH, check the installation directory + ffmpeg_install_path = get_ffmpeg_install_path() + if sys.platform == 'win32': + ffmpeg_exe = os.path.join(ffmpeg_install_path, 'ffmpeg.exe') + else: + ffmpeg_exe = os.path.join(ffmpeg_install_path, 'ffmpeg') + + if os.path.exists(ffmpeg_exe): + return ffmpeg_exe + + # Return command name as fallback + return "ffmpeg" + def check_ffmpeg_installed(): """Check if FFmpeg is installed and accessible.""" try: @@ -79,55 +161,75 @@ def check_ffmpeg_installed(): return True return False except Exception as e: - print(f"FFmpeg check error: {str(e)}") + logger.info(f"FFmpeg check error: {str(e)}") return False def install_ffmpeg_windows(): - """Install FFmpeg on Windows using either 7z or zip method.""" + """Install FFmpeg on Windows using 7z method primarily, with zip as fallback.""" ffmpeg_path = get_ffmpeg_install_path() # Check if already installed if check_ffmpeg_installed(): - print("✨ FFmpeg is already installed!") + logger.info("FFmpeg is already installed!") return True try: - # Define variables - ffmpeg_url = "https://github.com/GyanD/codexffmpeg/releases/download/7.1/ffmpeg-7.1-full_build.7z" - zip_url = "https://github.com/GyanD/codexffmpeg/releases/download/7.1/ffmpeg-7.1-full_build.zip" + # Define variables - prioritize 7z version + ffmpeg_7z_url = "https://github.com/GyanD/codexffmpeg/releases/download/7.1.1/ffmpeg-7.1.1-full_build.7z" + ffmpeg_zip_url = "https://github.com/GyanD/codexffmpeg/releases/download/7.1.1/ffmpeg-7.1.1-full_build.zip" + sha256_url = "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-7.1.1-full_build.7z.sha256" extract_dir = os.path.join(os.getenv('LOCALAPPDATA'), 'ffmpeg') - full_build_dir = os.path.join(extract_dir, 'ffmpeg-7.1-full_build') + full_build_dir = os.path.join(extract_dir, 'ffmpeg-7.1.1-full_build') bin_dir = os.path.join(full_build_dir, 'bin') # Create extraction directory if it doesn't exist os.makedirs(extract_dir, exist_ok=True) - # Choose installation method based on 7-Zip availability + # Try 7z method first (smaller size) use_7zip = check_7zip_installed() - temp_file = tempfile.NamedTemporaryFile(delete=False, - suffix='.7z' if use_7zip else '.zip').name - - # Download with progress callback - if not download_file(ffmpeg_url if use_7zip else zip_url, temp_file, - progress_callback=lambda msg: print(msg)): - raise Exception("Failed to download FFmpeg") - - print("🔧 Extracting FFmpeg components...") - try: - if use_7zip: - # Extract using 7-Zip - subprocess.run(['7z', 'x', temp_file, f'-o{extract_dir}', '-y'], - creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0, - timeout=300) # 5-minute timeout + if use_7zip: + logger.info("Using 7-Zip method (smaller download size)...") + temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.7z').name + + # Download 7z file + if not download_file(ffmpeg_7z_url, temp_file, + progress_callback=lambda msg: logger.debug(msg)): + logger.error("Failed to download 7z file, trying zip fallback...") + use_7zip = False else: - # Extract using built-in Windows tools + # Verify SHA-256 hash for 7z file + if verify_sha256(temp_file, sha256_url): + logger.info("Extracting FFmpeg components from 7z archive...") + try: + subprocess.run(['7z', 'x', temp_file, f'-o{extract_dir}', '-y'], + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0, + timeout=300) # 5-minute timeout + except Exception as e: + logger.error(f"7z extraction failed: {str(e)}, trying zip fallback...") + use_7zip = False + else: + logger.error("SHA-256 verification failed for 7z file, trying zip fallback...") + use_7zip = False + + # Fallback to zip method if 7z failed or not available + if not use_7zip: + logger.info("Using ZIP method as fallback...") + temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.zip').name + + # Download zip file + if not download_file(ffmpeg_zip_url, temp_file, + progress_callback=lambda msg: logger.debug(msg)): + raise Exception("Failed to download FFmpeg (both 7z and zip methods failed)") + + logger.info("Extracting FFmpeg components from zip archive...") + try: import zipfile with zipfile.ZipFile(temp_file, 'r') as zip_ref: zip_ref.extractall(extract_dir) - except Exception as e: - raise Exception(f"Extraction failed: {str(e)}") + except Exception as e: + raise Exception(f"Extraction failed: {str(e)}") - print("⚙️ Configuring system paths...") + logger.info("Configuring system paths...") # Add to System Path user_path = os.environ.get('PATH', '') if bin_dir not in user_path: @@ -145,11 +247,11 @@ def install_ffmpeg_windows(): if not check_ffmpeg_installed(): raise Exception("FFmpeg installation verification failed") - print("✨ FFmpeg installation completed successfully!") + logger.info("FFmpeg installation completed successfully!") return True except Exception as e: - print(f"❌ Error installing FFmpeg: {str(e)}") + logger.error(f"Error installing FFmpeg: {str(e)}") return False def install_ffmpeg_macos(): @@ -163,12 +265,12 @@ def install_ffmpeg_macos(): check=True, timeout=5) except (subprocess.SubprocessError, FileNotFoundError): - print("Installing Homebrew...") + logger.info("Installing Homebrew...") brew_install_cmd = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' subprocess.run(brew_install_cmd, shell=True, check=True, timeout=300) # Install FFmpeg - print("Installing FFmpeg...") + logger.info("Installing FFmpeg...") subprocess.run(['brew', 'install', 'ffmpeg'], check=True, timeout=300) # Verify installation @@ -178,7 +280,7 @@ def install_ffmpeg_macos(): return True except Exception as e: - print(f"Error installing FFmpeg: {str(e)}") + logger.error(f"Error installing FFmpeg: {str(e)}") return False def install_ffmpeg_linux(): @@ -208,7 +310,7 @@ def install_ffmpeg_linux(): return True except Exception as e: - print(f"Error installing FFmpeg: {str(e)}") + logger.error(f"Error installing FFmpeg: {str(e)}") return False def auto_install_ffmpeg(): @@ -220,5 +322,5 @@ def auto_install_ffmpeg(): elif sys.platform.startswith('linux'): return install_ffmpeg_linux() else: - print(f"Unsupported operating system: {sys.platform}") + logger.info(f"Unsupported operating system: {sys.platform}") return False \ No newline at end of file