Refactor FFmpeg updater to version checker

Replaces the FFmpeg updater logic with a version checker across all supported languages and removes the updater module. The Windows installation logic in ytsage_ffmpeg.py is rewritten to use essentials build extraction and path configuration directly, removing dependency on the updater module. Dialogs and UI strings are updated to reflect the new version checking workflow and provide installation guide links.
This commit is contained in:
oop7
2025-11-14 17:10:36 +02:00
parent 4c07423b49
commit bb231ccd12
17 changed files with 419 additions and 772 deletions
+170 -7
View File
@@ -12,6 +12,7 @@ from src.utils.ytsage_constants import (
FFMPEG_7Z_DOWNLOAD_URL,
FFMPEG_7Z_SHA256_URL,
FFMPEG_ZIP_DOWNLOAD_URL,
FFMPEG_ZIP_SHA256_URL,
OS_NAME,
SUBPROCESS_CREATIONFLAGS,
)
@@ -194,7 +195,7 @@ def check_ffmpeg_installed() -> bool:
def install_ffmpeg_windows(progress_callback=None) -> bool:
"""Install FFmpeg on Windows using 7z method primarily, with zip as fallback."""
"""Install FFmpeg on Windows using essentials build with 7z method primarily, with zip as fallback."""
# Check if already installed
if check_ffmpeg_installed():
logger.info("FFmpeg is already installed!")
@@ -203,14 +204,176 @@ def install_ffmpeg_windows(progress_callback=None) -> bool:
return True
try:
# Import the updater for installation
from src.core.ytsage_ffmpeg_updater import update_ffmpeg_windows
# Define variables for essentials build
extract_dir = Path(os.getenv("LOCALAPPDATA")) / "ffmpeg" # type: ignore
# Use the updater to install the latest version
logger.info("Installing FFmpeg using the updater module...")
# Create extraction directory if it doesn't exist
extract_dir.mkdir(exist_ok=True)
# Try 7z method first (smaller size)
use_7zip = check_7zip_installed()
success = False
if use_7zip:
logger.info("Using 7-Zip method (smaller download size)...")
if progress_callback:
progress_callback("⚡ Using 7-Zip method...")
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".7z").name
# Download 7z file
if progress_callback:
progress_callback("⬇ Downloading FFmpeg (7z)...")
if download_file(
FFMPEG_7Z_DOWNLOAD_URL,
temp_file,
progress_callback=progress_callback,
):
# Verify SHA-256 hash for 7z file
if progress_callback:
progress_callback("🔐 Verifying download integrity...")
if verify_sha256(temp_file, FFMPEG_7Z_SHA256_URL):
logger.info("Extracting FFmpeg components from 7z archive...")
if progress_callback:
progress_callback("⚙ Extracting FFmpeg...")
try:
subprocess.run(
["7z", "x", temp_file, f"-o{extract_dir}", "-y"],
creationflags=SUBPROCESS_CREATIONFLAGS,
timeout=300,
check=True,
)
success = True
except Exception as e:
logger.exception(f"7z extraction failed: {e}, trying zip fallback...")
if progress_callback:
progress_callback("❌ 7z extraction failed, trying zip fallback...")
else:
logger.error("SHA-256 verification failed for 7z file, trying zip fallback...")
if progress_callback:
progress_callback("❌ SHA-256 verification failed, trying zip fallback...")
# Clean up temp file
try:
Path(temp_file).unlink(missing_ok=True)
except Exception:
pass
# Fallback to zip method if 7z failed or not available
if not success:
logger.info("Using ZIP method as fallback...")
if progress_callback:
progress_callback("📦 Using ZIP method...")
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip").name
# Download zip file
if progress_callback:
progress_callback("⬇ Downloading FFmpeg (zip)...")
if not download_file(
FFMPEG_ZIP_DOWNLOAD_URL,
temp_file,
progress_callback=progress_callback,
):
logger.error("Failed to download FFmpeg (both 7z and zip methods failed)")
if progress_callback:
progress_callback("❌ Failed to download FFmpeg")
return False
# Verify SHA-256 hash for zip
if progress_callback:
progress_callback("🔐 Verifying download integrity...")
if not verify_sha256(temp_file, FFMPEG_ZIP_SHA256_URL):
logger.warning("SHA-256 verification failed for zip file, proceeding anyway...")
if progress_callback:
progress_callback("⚠️ SHA-256 verification failed, proceeding anyway...")
logger.info("Extracting FFmpeg components from zip archive...")
if progress_callback:
progress_callback("⚙ Extracting FFmpeg...")
try:
import zipfile
with zipfile.ZipFile(temp_file, "r") as zip_ref:
zip_ref.extractall(extract_dir)
success = True
except Exception as e:
logger.exception(f"Extraction failed: {e}")
if progress_callback:
progress_callback(f"❌ Extraction failed: {e}")
return False
finally:
# Clean up temp file
try:
Path(temp_file).unlink(missing_ok=True)
except Exception:
pass
if not success:
logger.error("Both 7z and zip methods failed")
if progress_callback:
progress_callback("❌ Installation failed")
return False
# Find the bin directory in the extracted essentials build
logger.info("Locating FFmpeg binaries...")
if progress_callback:
progress_callback("📦 Installing FFmpeg...")
return update_ffmpeg_windows(progress_callback=progress_callback)
progress_callback("🔍 Locating FFmpeg binaries...")
bin_dir = None
# Look for any directory matching ffmpeg-*-essentials_build pattern
for item in extract_dir.iterdir():
if item.is_dir() and "essentials_build" in item.name.lower():
potential_bin = item / "bin"
if potential_bin.exists():
bin_dir = potential_bin
logger.info(f"Found FFmpeg bin directory: {bin_dir}")
break
if not bin_dir:
logger.error("Could not locate FFmpeg bin directory")
if progress_callback:
progress_callback("❌ Could not locate FFmpeg bin directory")
return False
logger.info("Configuring system paths...")
if progress_callback:
progress_callback("🔧 Configuring system paths...")
# Add to System Path
user_path = os.environ.get("PATH", "")
path_parts = user_path.split(os.pathsep)
# Remove old FFmpeg paths and add new one
cleaned_paths = [p for p in path_parts if "ffmpeg" not in p.lower() or str(bin_dir) in p]
if str(bin_dir) not in cleaned_paths:
cleaned_paths.insert(0, str(bin_dir))
new_path = os.pathsep.join(cleaned_paths)
try:
subprocess.run(
["setx", "PATH", new_path],
creationflags=SUBPROCESS_CREATIONFLAGS,
timeout=30,
check=True,
)
os.environ["PATH"] = new_path
except Exception as e:
logger.warning(f"Failed to update PATH permanently: {e}")
# Still update for current session
os.environ["PATH"] = new_path
# Verify installation
if progress_callback:
progress_callback("✅ Verifying installation...")
if not check_ffmpeg_installed():
logger.error("FFmpeg installation verification failed")
if progress_callback:
progress_callback("⚠️ Installation completed but verification failed")
return True # Still return True as files were extracted
logger.info("FFmpeg installation completed successfully!")
if progress_callback:
progress_callback("✅ FFmpeg installation completed successfully!")
return True
except Exception as e:
logger.exception(f"Error installing FFmpeg: {e}")
-413
View File
@@ -1,413 +0,0 @@
"""
FFmpeg updater module for YTSage application.
Handles checking for updates and updating FFmpeg to the latest Essentials build.
"""
import hashlib
import os
import re
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Optional, Tuple, Callable
import requests
from src.utils.ytsage_logger import logger
from src.utils.ytsage_constants import (
FFMPEG_7Z_DOWNLOAD_URL,
FFMPEG_7Z_SHA256_URL,
FFMPEG_7Z_VERSION_URL,
FFMPEG_ZIP_DOWNLOAD_URL,
FFMPEG_ZIP_SHA256_URL,
FFMPEG_ZIP_VERSION_URL,
OS_NAME,
SUBPROCESS_CREATIONFLAGS,
)
from src.core.ytsage_ffmpeg import (
check_7zip_installed,
download_file,
get_file_sha256,
verify_sha256,
get_ffmpeg_install_path,
)
from src.core.ytsage_utils import get_ffmpeg_version_direct
def get_latest_ffmpeg_version() -> Optional[str]:
"""
Fetch the latest FFmpeg version from the version URL.
Returns:
str: Version string (e.g., "8.0") or None if fetch failed
"""
try:
# Try 7z version URL first
response = requests.get(FFMPEG_7Z_VERSION_URL, timeout=10)
response.raise_for_status()
version = response.text.strip()
# Validate version format (should be something like "8.0" or "7.1.1")
if re.match(r'^\d+\.\d+(\.\d+)?$', version):
logger.info(f"Latest FFmpeg version: {version}")
return version
else:
logger.warning(f"Unexpected version format: {version}")
return None
except requests.RequestException as e:
logger.error(f"Failed to fetch latest FFmpeg version: {e}")
return None
except Exception as e:
logger.exception(f"Unexpected error fetching FFmpeg version: {e}")
return None
def parse_version(version_str: str) -> Tuple[int, ...]:
"""
Parse version string into tuple of integers for comparison.
Args:
version_str: Version string like "8.0" or "7.1.1"
Returns:
Tuple of integers (e.g., (8, 0) or (7, 1, 1))
"""
try:
# Extract version numbers from string
# Handles formats like "8.0", "7.1.1", "ffmpeg version 8.0", etc.
match = re.search(r'(\d+\.\d+(?:\.\d+)?)', version_str)
if match:
version_str = match.group(1)
parts = version_str.split('.')
return tuple(int(p) for p in parts)
except (ValueError, AttributeError):
logger.warning(f"Could not parse version: {version_str}")
return (0,)
def compare_versions(current: str, latest: str) -> bool:
"""
Compare two version strings.
Args:
current: Current version string
latest: Latest version string
Returns:
True if update is needed (latest > current), False otherwise
"""
try:
current_tuple = parse_version(current)
latest_tuple = parse_version(latest)
logger.info(f"Comparing versions - Current: {current_tuple}, Latest: {latest_tuple}")
# Pad shorter version with zeros for comparison
max_len = max(len(current_tuple), len(latest_tuple))
current_padded = current_tuple + (0,) * (max_len - len(current_tuple))
latest_padded = latest_tuple + (0,) * (max_len - len(latest_tuple))
return latest_padded > current_padded
except Exception as e:
logger.exception(f"Error comparing versions: {e}")
return False
def check_ffmpeg_update_available() -> Tuple[bool, str, str]:
"""
Check if FFmpeg update is available.
Returns:
Tuple of (update_available, current_version, latest_version)
- update_available: True if update is needed
- current_version: Currently installed version or "Not installed"
- latest_version: Latest available version or "Unknown"
"""
try:
# Get current version
current_version = get_ffmpeg_version_direct()
if current_version in ["Not found", "Error getting version", "Unknown version"]:
current_version = "Not installed"
# Get latest version
latest_version = get_latest_ffmpeg_version()
if latest_version is None:
latest_version = "Unknown"
return False, current_version, latest_version
# If not installed, update is needed
if current_version == "Not installed":
return True, current_version, latest_version
# Compare versions
update_needed = compare_versions(current_version, latest_version)
return update_needed, current_version, latest_version
except Exception as e:
logger.exception(f"Error checking for FFmpeg updates: {e}")
return False, "Error", "Error"
def get_ffmpeg_extract_dir() -> Path:
"""Get the directory where FFmpeg should be extracted."""
if OS_NAME == "Windows":
return Path(os.getenv("LOCALAPPDATA")) / "ffmpeg" # type: ignore
elif OS_NAME == "Darwin":
return Path.home() / "Library" / "Application Support" / "ffmpeg"
else:
return Path.home() / ".local" / "share" / "ffmpeg"
def find_ffmpeg_bin_dir(extract_dir: Path, version: str) -> Optional[Path]:
"""
Find the FFmpeg bin directory after extraction.
Args:
extract_dir: Directory where FFmpeg was extracted
version: Version string (e.g., "8.0")
Returns:
Path to bin directory or None if not found
"""
# Expected pattern: ffmpeg-{version}-essentials_build
pattern = f"ffmpeg-{version}-essentials_build"
# Look for the directory
for item in extract_dir.iterdir():
if item.is_dir() and item.name.startswith(f"ffmpeg-{version}"):
bin_dir = item / "bin"
if bin_dir.exists():
logger.info(f"Found FFmpeg bin directory: {bin_dir}")
return bin_dir
logger.warning(f"Could not find FFmpeg bin directory with pattern: {pattern}")
return None
def update_ffmpeg_windows(progress_callback: Optional[Callable[[str], None]] = None) -> bool:
"""
Update FFmpeg on Windows to the latest Essentials build.
Args:
progress_callback: Optional callback function for progress updates
Returns:
True if update was successful, False otherwise
"""
if OS_NAME != "Windows":
logger.error("This update method is only for Windows")
return False
try:
# Track last progress percentage to avoid spam
last_percent = -1
def report_progress(msg: str, force: bool = False):
"""Helper to report progress (always logged, callback only if force=True or status message)."""
logger.info(msg)
# Only send callback for non-percentage messages or when forced
if progress_callback and (force or not msg.strip().endswith('%')):
progress_callback(msg)
def download_progress_wrapper(msg: str):
"""Wrapper for download progress that throttles percentage updates."""
nonlocal last_percent
# Extract percentage if present
percent_match = re.search(r'(\d+)%', msg)
if percent_match:
current_percent = int(percent_match.group(1))
# Only report every 10% and to logger only
if current_percent != last_percent and current_percent % 10 == 0:
logger.info(msg)
if progress_callback:
progress_callback(msg)
last_percent = current_percent
else:
# Non-percentage messages get reported normally
report_progress(msg, force=True)
# Get latest version
report_progress("🔍 Checking latest FFmpeg version...", force=True)
latest_version = get_latest_ffmpeg_version()
if not latest_version:
report_progress("❌ Failed to determine latest version", force=True)
return False
report_progress(f"📦 Latest version: {latest_version}", force=True)
extract_dir = get_ffmpeg_extract_dir()
extract_dir.mkdir(exist_ok=True)
# Try 7z method first (smaller size)
use_7zip = check_7zip_installed()
success = False
if use_7zip:
report_progress("⚡ Using 7-Zip method (smaller download)...", force=True)
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".7z").name
# Download 7z file
report_progress("⬇ Downloading FFmpeg (7z)...", force=True)
last_percent = -1 # Reset for this download
if download_file(
FFMPEG_7Z_DOWNLOAD_URL,
temp_file,
progress_callback=download_progress_wrapper,
):
# Verify SHA-256 hash
report_progress("🔐 Verifying download integrity...", force=True)
if verify_sha256(temp_file, FFMPEG_7Z_SHA256_URL):
report_progress("⚙ Extracting FFmpeg...", force=True)
try:
subprocess.run(
["7z", "x", temp_file, f"-o{extract_dir}", "-y"],
creationflags=SUBPROCESS_CREATIONFLAGS,
timeout=300,
check=True,
)
success = True
except Exception as e:
logger.exception(f"7z extraction failed: {e}")
report_progress(f"❌ 7z extraction failed, trying zip fallback...", force=True)
else:
report_progress("❌ SHA-256 verification failed, trying zip fallback...", force=True)
# Clean up temp file
try:
Path(temp_file).unlink(missing_ok=True)
except Exception:
pass
# Fallback to zip method
if not success:
report_progress("📦 Using ZIP method...", force=True)
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip").name
# Download zip file
report_progress("⬇ Downloading FFmpeg (zip)...", force=True)
last_percent = -1 # Reset for this download
if not download_file(
FFMPEG_ZIP_DOWNLOAD_URL,
temp_file,
progress_callback=download_progress_wrapper,
):
report_progress("❌ Failed to download FFmpeg", force=True)
return False
# Verify SHA-256 hash for zip
report_progress("🔐 Verifying download integrity...", force=True)
if not verify_sha256(temp_file, FFMPEG_ZIP_SHA256_URL):
report_progress("⚠️ SHA-256 verification failed, proceeding anyway...", force=True)
report_progress("⚙ Extracting FFmpeg...", force=True)
try:
import zipfile
with zipfile.ZipFile(temp_file, "r") as zip_ref:
zip_ref.extractall(extract_dir)
success = True
except Exception as e:
logger.exception(f"Extraction failed: {e}")
report_progress(f"❌ Extraction failed: {e}", force=True)
return False
finally:
# Clean up temp file
try:
Path(temp_file).unlink(missing_ok=True)
except Exception:
pass
if not success:
report_progress("❌ Both 7z and zip methods failed", force=True)
return False
# Find the bin directory
report_progress("🔍 Locating FFmpeg binaries...", force=True)
bin_dir = find_ffmpeg_bin_dir(extract_dir, latest_version)
if not bin_dir:
report_progress("❌ Could not locate FFmpeg bin directory", force=True)
return False
# Add to System Path
report_progress("🔧 Configuring system paths...", force=True)
user_path = os.environ.get("PATH", "")
# Remove old FFmpeg paths from PATH
path_parts = user_path.split(os.pathsep)
cleaned_paths = [p for p in path_parts if "ffmpeg" not in p.lower() or str(bin_dir) in p]
# Add new path if not already present
if str(bin_dir) not in cleaned_paths:
cleaned_paths.insert(0, str(bin_dir))
new_path = os.pathsep.join(cleaned_paths)
try:
subprocess.run(
["setx", "PATH", new_path],
creationflags=SUBPROCESS_CREATIONFLAGS,
timeout=30,
check=True,
)
os.environ["PATH"] = new_path
except Exception as e:
logger.warning(f"Failed to update PATH permanently: {e}")
# Still update for current session
os.environ["PATH"] = new_path
# Verify installation
report_progress("✅ Verifying installation...", force=True)
new_version = get_ffmpeg_version_direct()
if new_version not in ["Not found", "Error getting version", "Unknown version"]:
report_progress(f"✅ FFmpeg successfully updated to version {new_version}!", force=True)
return True
else:
report_progress("⚠️ Update completed but verification failed", force=True)
return True # Still return True as files were extracted
except Exception as e:
logger.exception(f"Error updating FFmpeg: {e}")
if progress_callback:
progress_callback(f"❌ Error: {e}")
return False
def update_ffmpeg(progress_callback: Optional[Callable[[str], None]] = None) -> bool:
"""
Update FFmpeg based on the operating system.
Args:
progress_callback: Optional callback function for progress updates
Returns:
True if update was successful, False otherwise
"""
if OS_NAME == "Windows":
return update_ffmpeg_windows(progress_callback)
elif OS_NAME == "Darwin":
# macOS update via Homebrew
if progress_callback:
progress_callback("🍺 Updating FFmpeg via Homebrew...")
try:
subprocess.run(["brew", "upgrade", "ffmpeg"], check=True, timeout=300)
if progress_callback:
progress_callback("✅ FFmpeg updated successfully!")
return True
except Exception as e:
logger.exception(f"Error updating FFmpeg on macOS: {e}")
if progress_callback:
progress_callback(f"❌ Error: {e}")
return False
elif OS_NAME == "Linux":
# Linux update via package manager
if progress_callback:
progress_callback("📦 Please use your package manager to update FFmpeg")
return False
else:
logger.error(f"Unsupported operating system: {OS_NAME}")
return False
@@ -1,19 +1,20 @@
"""
Updater tab for Custom Options dialog.
Handles checking for and installing FFmpeg updates and yt-dlp auto-update settings.
Handles FFmpeg version checking and yt-dlp auto-update settings.
"""
import re
import subprocess
import threading
from typing import TYPE_CHECKING, cast
from typing import Optional, Tuple, TYPE_CHECKING, cast
from PySide6.QtCore import QObject, Signal
import requests
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import (
QCheckBox,
QGroupBox,
QHBoxLayout,
QLabel,
QProgressBar,
QPushButton,
QRadioButton,
QScrollArea,
@@ -24,6 +25,7 @@ from PySide6.QtWidgets import (
from src.core.ytsage_utils import (
get_auto_update_settings,
get_ffmpeg_version_direct,
update_auto_update_settings,
)
from src.core.ytsage_yt_dlp import get_yt_dlp_path
@@ -31,35 +33,127 @@ from src.gui.ytsage_gui_dialogs.ytsage_dialogs_update import YTDLPUpdateDialog
from src.utils.ytsage_config_manager import ConfigManager
from src.utils.ytsage_localization import _
from src.utils.ytsage_logger import logger
from src.core.ytsage_ffmpeg_updater import check_ffmpeg_update_available, update_ffmpeg
from src.utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS
from src.utils.ytsage_constants import OS_NAME
from src.utils.ytsage_constants import (
FFMPEG_7Z_VERSION_URL,
OS_NAME,
SUBPROCESS_CREATIONFLAGS,
)
if TYPE_CHECKING:
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_custom import CustomOptionsDialog
class UpdateWorker(QObject):
"""Worker class for running FFmpeg updates in a separate thread."""
# Helper functions for FFmpeg version checking (copied from removed ytsage_ffmpeg_updater.py)
def get_latest_ffmpeg_version() -> Optional[str]:
"""
Fetch the latest FFmpeg version from the version URL.
progress = Signal(str) # Progress messages
finished = Signal(bool) # Completion status (success/failure)
Returns:
str: Version string (e.g., "8.0") or None if fetch failed
"""
try:
response = requests.get(FFMPEG_7Z_VERSION_URL, timeout=10)
response.raise_for_status()
version = response.text.strip()
# Validate version format (should be something like "8.0" or "7.1.1")
if re.match(r'^\d+\.\d+(\.\d+)?$', version):
logger.info(f"Latest FFmpeg version: {version}")
return version
else:
logger.warning(f"Unexpected version format: {version}")
return None
except requests.RequestException as e:
logger.error(f"Failed to fetch latest FFmpeg version: {e}")
return None
except Exception as e:
logger.exception(f"Unexpected error fetching FFmpeg version: {e}")
return None
def parse_version(version_str: str) -> Tuple[int, ...]:
"""
Parse version string into tuple of integers for comparison.
def run_update(self):
"""Run the FFmpeg update process."""
try:
def progress_callback(msg: str):
"""Callback for progress updates."""
self.progress.emit(msg)
# Run the update
success = update_ffmpeg(progress_callback=progress_callback)
self.finished.emit(success)
except Exception as e:
logger.exception(f"Error during FFmpeg update: {e}")
self.progress.emit(f"❌ Error: {e}")
self.finished.emit(False)
Args:
version_str: Version string like "8.0" or "7.1.1"
Returns:
Tuple of integers (e.g., (8, 0) or (7, 1, 1))
"""
try:
# Extract version numbers from string
match = re.search(r'(\d+\.\d+(?:\.\d+)?)', version_str)
if match:
version_str = match.group(1)
parts = version_str.split('.')
return tuple(int(p) for p in parts)
except (ValueError, AttributeError):
logger.warning(f"Could not parse version: {version_str}")
return (0,)
def compare_versions(current: str, latest: str) -> bool:
"""
Compare two version strings.
Args:
current: Current version string
latest: Latest version string
Returns:
True if update is needed (latest > current), False otherwise
"""
try:
current_tuple = parse_version(current)
latest_tuple = parse_version(latest)
logger.info(f"Comparing versions - Current: {current_tuple}, Latest: {latest_tuple}")
# Pad shorter version with zeros for comparison
max_len = max(len(current_tuple), len(latest_tuple))
current_padded = current_tuple + (0,) * (max_len - len(current_tuple))
latest_padded = latest_tuple + (0,) * (max_len - len(latest_tuple))
return latest_padded > current_padded
except Exception as e:
logger.exception(f"Error comparing versions: {e}")
return False
def check_ffmpeg_version() -> Tuple[bool, str, str]:
"""
Check FFmpeg version and compare with latest.
Returns:
Tuple of (update_available, current_version, latest_version)
"""
try:
# Get current version
current_version = get_ffmpeg_version_direct()
if current_version in ["Not found", "Error getting version", "Unknown version"]:
current_version = "Not installed"
# Get latest version
latest_version = get_latest_ffmpeg_version()
if latest_version is None:
latest_version = "Unknown"
return False, current_version, latest_version
# If not installed, update is available
if current_version == "Not installed":
return True, current_version, latest_version
# Compare versions
update_needed = compare_versions(current_version, latest_version)
return update_needed, current_version, latest_version
except Exception as e:
logger.exception(f"Error checking FFmpeg version: {e}")
return False, "Error", "Error"
class UpdaterTabWidget(QWidget):
@@ -69,9 +163,6 @@ class UpdaterTabWidget(QWidget):
super().__init__(parent)
self._parent: "CustomOptionsDialog" = cast("CustomOptionsDialog", self.parent())
self.update_thread = None
self.update_worker = None
# State variables
self.current_version = "Unknown"
self.latest_version = "Unknown"
@@ -102,7 +193,7 @@ class UpdaterTabWidget(QWidget):
help_text.setStyleSheet("color: #999999; padding: 10px;")
layout.addWidget(help_text)
# FFmpeg Update Section
# FFmpeg Version Check Section
ffmpeg_group = QGroupBox(_('ffmpeg_updater.title'))
ffmpeg_layout = QVBoxLayout(ffmpeg_group)
@@ -144,10 +235,8 @@ class UpdaterTabWidget(QWidget):
)
ffmpeg_layout.addWidget(self.status_label)
# Button layout
button_layout = QHBoxLayout()
# Check for updates button
# Check version button
check_button_layout = QHBoxLayout()
self.check_button = QPushButton(_('ffmpeg_updater.check_updates'))
self.check_button.clicked.connect(self.check_for_updates)
self.check_button.setStyleSheet(
@@ -170,84 +259,34 @@ class UpdaterTabWidget(QWidget):
}
"""
)
button_layout.addWidget(self.check_button)
check_button_layout.addWidget(self.check_button)
check_button_layout.addStretch()
ffmpeg_layout.addLayout(check_button_layout)
button_layout.addStretch()
# Update button
self.update_button = QPushButton(_('ffmpeg_updater.update_button'))
self.update_button.clicked.connect(self.start_update)
self.update_button.setEnabled(False)
self.update_button.setStyleSheet(
# Installation guide info
guide_label = QLabel(_('ffmpeg_updater.guide_info'))
guide_label.setWordWrap(True)
guide_label.setOpenExternalLinks(True) # Enable clickable links
guide_label.setTextFormat(Qt.TextFormat.RichText) # Enable HTML formatting
guide_label.setStyleSheet(
"""
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
QLabel {
color: #cccccc;
font-size: 11px;
padding: 8px;
background-color: #1a1d20;
border-radius: 4px;
color: white;
font-weight: bold;
min-width: 120px;
}
QPushButton:hover {
background-color: #a50000;
QLabel a {
color: #4da6ff;
text-decoration: underline;
}
QPushButton:disabled {
background-color: #4a4a4a;
color: #888888;
QLabel a:hover {
color: #66b3ff;
}
"""
)
button_layout.addWidget(self.update_button)
ffmpeg_layout.addLayout(button_layout)
# Progress bar
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 0) # Indeterminate progress
self.progress_bar.setVisible(False)
self.progress_bar.setStyleSheet(
"""
QProgressBar {
border: 2px solid #2a2d36;
border-radius: 4px;
background-color: #1d1e22;
text-align: center;
color: #ffffff;
height: 20px;
}
QProgressBar::chunk {
background-color: #c90000;
border-radius: 2px;
}
"""
)
ffmpeg_layout.addWidget(self.progress_bar)
# Log output
log_label = QLabel("Update Log:")
log_label.setStyleSheet("font-size: 11px; font-weight: bold; color: #ffffff; margin-top: 5px;")
ffmpeg_layout.addWidget(log_label)
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setPlaceholderText("Update logs will appear here...")
self.log_output.setMinimumHeight(60)
self.log_output.setMaximumHeight(100)
self.log_output.setStyleSheet(
"""
QTextEdit {
background-color: #1d1e22;
color: #ffffff;
border: 2px solid #2a2d36;
border-radius: 6px;
padding: 8px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 11px;
}
"""
)
ffmpeg_layout.addWidget(self.log_output)
ffmpeg_layout.addWidget(guide_label)
layout.addWidget(ffmpeg_group)
@@ -596,27 +635,25 @@ class UpdaterTabWidget(QWidget):
dialog.show() # Use show() instead of exec() to avoid blocking
def check_for_updates(self) -> None:
"""Check if FFmpeg updates are available."""
"""Check FFmpeg version and compare with latest."""
self.check_button.setEnabled(False)
self.update_button.setEnabled(False)
self.status_label.setText(_('ffmpeg_updater.status_checking'))
self.status_label.setStyleSheet(
"color: #ffaa00; font-size: 12px; padding: 8px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
self.log_output.append("🔍 Checking for FFmpeg updates...")
# Run check in background thread
def check_thread():
try:
update_available, current_version, latest_version = check_ffmpeg_update_available()
update_available, current_version, latest_version = check_ffmpeg_version()
# Update UI in main thread using signals
# Update UI in main thread
self.check_button.setEnabled(True)
self._update_check_results(update_available, current_version, latest_version)
except Exception as e:
logger.exception(f"Error checking for updates: {e}")
logger.exception(f"Error checking FFmpeg version: {e}")
self.check_button.setEnabled(True)
self._show_check_error(str(e))
@@ -624,7 +661,7 @@ class UpdaterTabWidget(QWidget):
thread.start()
def _update_check_results(self, update_available: bool, current_version: str, latest_version: str) -> None:
"""Handle completion of update check."""
"""Handle completion of version check."""
self.update_available = update_available
self.current_version = current_version
self.latest_version = latest_version
@@ -635,97 +672,28 @@ class UpdaterTabWidget(QWidget):
# Update status
if current_version == "Not installed":
self.status_label.setText(_('ffmpeg_updater.install_first'))
self.status_label.setText(_('ffmpeg_updater.status_not_installed'))
self.status_label.setStyleSheet(
"color: #ff6666; font-size: 12px; padding: 8px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
self.update_button.setEnabled(True if OS_NAME == "Windows" else False)
self.log_output.append(f"❌ FFmpeg is not installed. Latest version available: {latest_version}")
elif update_available:
self.status_label.setText(_('ffmpeg_updater.status_update_available'))
self.status_label.setStyleSheet(
"color: #ffaa00; font-size: 12px; padding: 8px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
self.update_button.setEnabled(True if OS_NAME == "Windows" else False)
self.log_output.append(f"⚠ Update available: {current_version}{latest_version}")
else:
self.status_label.setText(_('ffmpeg_updater.status_up_to_date'))
self.status_label.setStyleSheet(
"color: #00cc00; font-size: 12px; padding: 8px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
self.update_button.setEnabled(False)
self.log_output.append(f"✅ FFmpeg is up to date (version {current_version})")
# Show message for non-Windows systems
if OS_NAME != "Windows" and update_available:
self.log_output.append("️ Automatic updates are only available on Windows.")
self.log_output.append(" Please use your system's package manager to update FFmpeg.")
def _show_check_error(self, error: str) -> None:
"""Handle error during update check."""
"""Handle error during version check."""
self.status_label.setText(_('ffmpeg_updater.check_failed'))
self.status_label.setStyleSheet(
"color: #ff6666; font-size: 12px; padding: 8px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
self.log_output.append(f"❌ Error checking for updates: {error}")
def start_update(self) -> None:
"""Start the FFmpeg update process."""
if OS_NAME != "Windows":
self.log_output.append("❌ Automatic updates are only supported on Windows.")
return
# Disable buttons during update
self.check_button.setEnabled(False)
self.update_button.setEnabled(False)
self.progress_bar.setVisible(True)
self.log_output.append("=" * 60)
self.log_output.append("🚀 Starting FFmpeg update process...")
self.log_output.append("=" * 60)
# Create worker and thread
self.update_worker = UpdateWorker()
self.update_thread = threading.Thread(target=self.update_worker.run_update, daemon=True)
# Connect signals
self.update_worker.progress.connect(self._on_update_progress)
self.update_worker.finished.connect(self._on_update_finished)
# Start update
self.update_thread.start()
def _on_update_progress(self, message: str) -> None:
"""Handle progress updates from the update worker."""
self.log_output.append(message)
# Auto-scroll to bottom
cursor = self.log_output.textCursor()
cursor.movePosition(cursor.MoveOperation.End)
self.log_output.setTextCursor(cursor)
def _on_update_finished(self, success: bool) -> None:
"""Handle completion of the update process."""
self.progress_bar.setVisible(False)
self.check_button.setEnabled(True)
self.log_output.append("=" * 60)
if success:
self.log_output.append(_('ffmpeg_updater.update_success', version=self.latest_version))
self.status_label.setText(_('ffmpeg_updater.status_up_to_date'))
self.status_label.setStyleSheet(
"color: #00cc00; font-size: 12px; padding: 8px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
# Re-check to update version info
self.check_for_updates()
else:
self.log_output.append(_('ffmpeg_updater.update_failed'))
self.status_label.setText(_('ffmpeg_updater.status_update_available'))
self.update_button.setEnabled(True)
self.log_output.append("=" * 60)