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.
This commit is contained in:
Your Name
2025-08-16 15:05:27 +03:00
parent 8ee436aa14
commit 0cb40f4338
+137 -35
View File
@@ -4,8 +4,10 @@ import subprocess
import requests import requests
import shutil import shutil
import tempfile import tempfile
import hashlib
from pathlib import Path from pathlib import Path
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
from .ytsage_logging import logger
def check_7zip_installed(): def check_7zip_installed():
"""Check if 7-Zip is installed on Windows.""" """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}%") progress_callback(f"⚡ Downloading FFmpeg components... {progress}%")
return True return True
except requests.RequestException as e: 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 return False
def get_ffmpeg_install_path(): def get_ffmpeg_install_path():
"""Get the FFmpeg installation path.""" """Get the FFmpeg installation path."""
if sys.platform == 'win32': 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': elif sys.platform == 'darwin':
paths = ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin'] paths = ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin']
for path in paths: for path in paths:
@@ -54,6 +88,54 @@ def get_ffmpeg_install_path():
else: else:
return '/usr/bin' # Standard Linux path 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(): def check_ffmpeg_installed():
"""Check if FFmpeg is installed and accessible.""" """Check if FFmpeg is installed and accessible."""
try: try:
@@ -79,55 +161,75 @@ def check_ffmpeg_installed():
return True return True
return False return False
except Exception as e: except Exception as e:
print(f"FFmpeg check error: {str(e)}") logger.info(f"FFmpeg check error: {str(e)}")
return False return False
def install_ffmpeg_windows(): 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() ffmpeg_path = get_ffmpeg_install_path()
# Check if already installed # Check if already installed
if check_ffmpeg_installed(): if check_ffmpeg_installed():
print("FFmpeg is already installed!") logger.info("FFmpeg is already installed!")
return True return True
try: try:
# Define variables # Define variables - prioritize 7z version
ffmpeg_url = "https://github.com/GyanD/codexffmpeg/releases/download/7.1/ffmpeg-7.1-full_build.7z" ffmpeg_7z_url = "https://github.com/GyanD/codexffmpeg/releases/download/7.1.1/ffmpeg-7.1.1-full_build.7z"
zip_url = "https://github.com/GyanD/codexffmpeg/releases/download/7.1/ffmpeg-7.1-full_build.zip" 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') 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') bin_dir = os.path.join(full_build_dir, 'bin')
# Create extraction directory if it doesn't exist # Create extraction directory if it doesn't exist
os.makedirs(extract_dir, exist_ok=True) 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() use_7zip = check_7zip_installed()
temp_file = tempfile.NamedTemporaryFile(delete=False, if use_7zip:
suffix='.7z' if use_7zip else '.zip').name logger.info("Using 7-Zip method (smaller download size)...")
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.7z').name
# Download with progress callback # Download 7z file
if not download_file(ffmpeg_url if use_7zip else zip_url, temp_file, if not download_file(ffmpeg_7z_url, temp_file,
progress_callback=lambda msg: print(msg)): progress_callback=lambda msg: logger.debug(msg)):
raise Exception("Failed to download FFmpeg") logger.error("Failed to download 7z file, trying zip fallback...")
use_7zip = False
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
else: 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 import zipfile
with zipfile.ZipFile(temp_file, 'r') as zip_ref: with zipfile.ZipFile(temp_file, 'r') as zip_ref:
zip_ref.extractall(extract_dir) zip_ref.extractall(extract_dir)
except Exception as e: except Exception as e:
raise Exception(f"Extraction failed: {str(e)}") raise Exception(f"Extraction failed: {str(e)}")
print("⚙️ Configuring system paths...") logger.info("Configuring system paths...")
# Add to System Path # Add to System Path
user_path = os.environ.get('PATH', '') user_path = os.environ.get('PATH', '')
if bin_dir not in user_path: if bin_dir not in user_path:
@@ -145,11 +247,11 @@ def install_ffmpeg_windows():
if not check_ffmpeg_installed(): if not check_ffmpeg_installed():
raise Exception("FFmpeg installation verification failed") raise Exception("FFmpeg installation verification failed")
print("FFmpeg installation completed successfully!") logger.info("FFmpeg installation completed successfully!")
return True return True
except Exception as e: except Exception as e:
print(f"Error installing FFmpeg: {str(e)}") logger.error(f"Error installing FFmpeg: {str(e)}")
return False return False
def install_ffmpeg_macos(): def install_ffmpeg_macos():
@@ -163,12 +265,12 @@ def install_ffmpeg_macos():
check=True, check=True,
timeout=5) timeout=5)
except (subprocess.SubprocessError, FileNotFoundError): 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)"' 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) subprocess.run(brew_install_cmd, shell=True, check=True, timeout=300)
# Install FFmpeg # Install FFmpeg
print("Installing FFmpeg...") logger.info("Installing FFmpeg...")
subprocess.run(['brew', 'install', 'ffmpeg'], check=True, timeout=300) subprocess.run(['brew', 'install', 'ffmpeg'], check=True, timeout=300)
# Verify installation # Verify installation
@@ -178,7 +280,7 @@ def install_ffmpeg_macos():
return True return True
except Exception as e: except Exception as e:
print(f"Error installing FFmpeg: {str(e)}") logger.error(f"Error installing FFmpeg: {str(e)}")
return False return False
def install_ffmpeg_linux(): def install_ffmpeg_linux():
@@ -208,7 +310,7 @@ def install_ffmpeg_linux():
return True return True
except Exception as e: except Exception as e:
print(f"Error installing FFmpeg: {str(e)}") logger.error(f"Error installing FFmpeg: {str(e)}")
return False return False
def auto_install_ffmpeg(): def auto_install_ffmpeg():
@@ -220,5 +322,5 @@ def auto_install_ffmpeg():
elif sys.platform.startswith('linux'): elif sys.platform.startswith('linux'):
return install_ffmpeg_linux() return install_ffmpeg_linux()
else: else:
print(f"Unsupported operating system: {sys.platform}") logger.info(f"Unsupported operating system: {sys.platform}")
return False return False