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:
@@ -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
|
|
||||||
if not download_file(ffmpeg_url if use_7zip else zip_url, temp_file,
|
# Download 7z file
|
||||||
progress_callback=lambda msg: print(msg)):
|
if not download_file(ffmpeg_7z_url, temp_file,
|
||||||
raise Exception("Failed to download FFmpeg")
|
progress_callback=lambda msg: logger.debug(msg)):
|
||||||
|
logger.error("Failed to download 7z file, trying zip fallback...")
|
||||||
print("🔧 Extracting FFmpeg components...")
|
use_7zip = False
|
||||||
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
|
||||||
Reference in New Issue
Block a user