Add FFmpeg updater module and GUI integration

Introduces a new FFmpeg updater module for checking and updating FFmpeg to the latest essentials build, with progress reporting and version comparison. Updates language files with new translation keys for the updater in multiple languages. Refactors Windows FFmpeg installation to use the new updater logic. Adds UpdaterTabWidget to the GUI dialogs for integration of the updater into the application's interface.
This commit is contained in:
oop7
2025-11-12 16:04:55 +02:00
parent 683946377e
commit a3a4352f9d
21 changed files with 1150 additions and 135 deletions
+4
View File
@@ -23,6 +23,7 @@ from src.gui.ytsage_gui_dialogs.ytsage_dialogs_selection import (
)
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_settings import AutoUpdateSettingsDialog, DownloadSettingsDialog
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_update import AutoUpdateThread, UpdateThread, VersionCheckThread, YTDLPUpdateDialog
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_updater import UpdaterTabWidget
__all__ = [
# Base dialogs
@@ -52,6 +53,9 @@ __all__ = [
"CustomOptionsDialog",
"TimeRangeDialog",
# Updater widget
"UpdaterTabWidget",
# History dialog
"HistoryDialog",
]
@@ -34,6 +34,7 @@ from src.utils.ytsage_constants import YTDLP_DOCS_URL
from src.utils.ytsage_config_manager import ConfigManager
from src.utils.ytsage_localization import LocalizationManager, _
from src.utils.ytsage_logger import logger
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_updater import UpdaterTabWidget
if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import)
@@ -516,11 +517,15 @@ class CustomOptionsDialog(QDialog):
language_layout.addStretch()
# === Updater Tab ===
updater_tab = UpdaterTabWidget(self)
# Add tabs to the tab widget
self.tab_widget.addTab(cookies_tab, _("tabs.cookies"))
self.tab_widget.addTab(command_tab, _("tabs.custom_command"))
self.tab_widget.addTab(proxy_tab, _("tabs.proxy"))
self.tab_widget.addTab(language_tab, _("tabs.language"))
self.tab_widget.addTab(updater_tab, _("tabs.updater"))
# Dialog buttons
button_box = QDialogButtonBox()
@@ -3,9 +3,7 @@ FFmpeg installation dialogs for YTSage application.
Contains dialogs and threads for checking and installing FFmpeg.
"""
import contextlib
import webbrowser
from io import StringIO
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QIcon
@@ -20,15 +18,12 @@ class FFmpegInstallThread(QThread):
progress = Signal(str)
def run(self) -> None:
# Redirect stdout to capture progress messages
output = StringIO()
with contextlib.redirect_stdout(output):
success = auto_install_ffmpeg()
# Process captured output and emit progress signals
for line in output.getvalue().splitlines():
self.progress.emit(line)
# Use a callback to capture progress instead of stdout redirection
def progress_callback(msg: str):
self.progress.emit(msg)
# Install FFmpeg with progress callback
success = auto_install_ffmpeg(progress_callback=progress_callback)
self.finished.emit(success)
@@ -36,9 +31,9 @@ class FFmpegCheckDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("FFmpeg Installation")
self.setMinimumWidth(450)
self.setMinimumHeight(200)
self.resize(450, 220)
self.setMinimumWidth(500)
self.setMinimumHeight(280)
self.resize(500, 300)
# Set the window icon to match the main app
if parent and parent.windowIcon():
@@ -67,11 +62,12 @@ class FFmpegCheckDialog(QDialog):
self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.message_label)
# Progress label with improved styling and compact height
# Progress label with improved styling
self.progress_label = QLabel("")
self.progress_label.setWordWrap(True)
self.progress_label.setMinimumHeight(60) # Smaller but visible area
self.progress_label.setMaximumHeight(80) # Limit maximum height
self.progress_label.setMinimumHeight(80)
self.progress_label.setMaximumHeight(120)
self.progress_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
self.progress_label.setStyleSheet(
"""
QLabel {
@@ -79,22 +75,22 @@ class FFmpegCheckDialog(QDialog):
color: #cccccc;
border: 1px solid #3d3d3d;
border-radius: 6px;
padding: 8px;
padding: 12px;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 11px;
line-height: 1.2;
line-height: 1.4;
}
"""
)
self.progress_label.hide()
layout.addWidget(self.progress_label)
# Add minimal stretch - just enough to push buttons down slightly
layout.addSpacing(10)
# Add stretch to push buttons to bottom
layout.addStretch()
# Buttons container - simple approach that should work
button_layout = QHBoxLayout()
button_layout.setSpacing(15) # Simple spacing
button_layout.setSpacing(12)
# Install button
self.install_btn = QPushButton("Install FFmpeg")
@@ -148,6 +144,7 @@ class FFmpegCheckDialog(QDialog):
# Initialize installation thread
self.install_thread = None
self.progress_messages = [] # Store progress messages
def start_installation(self) -> None:
self.install_btn.setEnabled(False)
@@ -165,6 +162,7 @@ class FFmpegCheckDialog(QDialog):
return
self.message_label.setText("Installing FFmpeg... Please wait")
self.progress_messages = [] # Clear previous messages
self.progress_label.show()
self.install_thread = FFmpegInstallThread()
@@ -173,7 +171,13 @@ class FFmpegCheckDialog(QDialog):
self.install_thread.start()
def update_progress(self, message) -> None:
self.progress_label.setText(message)
# Keep only the last 5 messages to avoid overflow
self.progress_messages.append(message)
if len(self.progress_messages) > 5:
self.progress_messages.pop(0)
# Display the messages
self.progress_label.setText("\n".join(self.progress_messages))
def installation_finished(self, success) -> None:
if success:
@@ -0,0 +1,362 @@
"""
Updater tab for Custom Options dialog.
Handles checking for and installing FFmpeg updates.
"""
import threading
from typing import TYPE_CHECKING, cast
from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import (
QGroupBox,
QHBoxLayout,
QLabel,
QProgressBar,
QPushButton,
QTextEdit,
QVBoxLayout,
QWidget,
)
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
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."""
progress = Signal(str) # Progress messages
finished = Signal(bool) # Completion status (success/failure)
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)
class UpdaterTabWidget(QWidget):
"""Widget for the Updater tab in Custom Options dialog."""
def __init__(self, parent=None) -> None:
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"
self.update_available = False
self._init_ui()
def _init_ui(self) -> None:
"""Initialize the UI components."""
layout = QVBoxLayout(self)
# Help text
help_text = QLabel(_('ffmpeg_updater.description'))
help_text.setWordWrap(True)
help_text.setStyleSheet("color: #999999; padding: 10px;")
layout.addWidget(help_text)
# FFmpeg Update Section
ffmpeg_group = QGroupBox(_('ffmpeg_updater.title'))
ffmpeg_layout = QVBoxLayout(ffmpeg_group)
# Version information layout
version_layout = QVBoxLayout()
# Current version
current_layout = QHBoxLayout()
current_label = QLabel(_('ffmpeg_updater.current_version'))
current_label.setStyleSheet("font-weight: bold; color: #ffffff;")
current_layout.addWidget(current_label)
self.current_version_label = QLabel("...")
self.current_version_label.setStyleSheet("color: #cccccc;")
current_layout.addWidget(self.current_version_label)
current_layout.addStretch()
version_layout.addLayout(current_layout)
# Latest version
latest_layout = QHBoxLayout()
latest_label = QLabel(_('ffmpeg_updater.latest_version'))
latest_label.setStyleSheet("font-weight: bold; color: #ffffff;")
latest_layout.addWidget(latest_label)
self.latest_version_label = QLabel("...")
self.latest_version_label.setStyleSheet("color: #cccccc;")
latest_layout.addWidget(self.latest_version_label)
latest_layout.addStretch()
version_layout.addLayout(latest_layout)
ffmpeg_layout.addLayout(version_layout)
# Status label
self.status_label = QLabel(_('ffmpeg_updater.status_idle'))
self.status_label.setWordWrap(True)
self.status_label.setStyleSheet(
"color: #888888; font-size: 12px; padding: 8px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
ffmpeg_layout.addWidget(self.status_label)
# Button layout
button_layout = QHBoxLayout()
# Check for updates button
self.check_button = QPushButton(_('ffmpeg_updater.check_updates'))
self.check_button.clicked.connect(self.check_for_updates)
self.check_button.setStyleSheet(
"""
QPushButton {
padding: 8px 15px;
background-color: #444444;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
min-width: 120px;
}
QPushButton:hover {
background-color: #555555;
}
QPushButton:disabled {
background-color: #2a2a2a;
color: #666666;
}
"""
)
button_layout.addWidget(self.check_button)
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(
"""
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
min-width: 120px;
}
QPushButton:hover {
background-color: #a50000;
}
QPushButton:disabled {
background-color: #4a4a4a;
color: #888888;
}
"""
)
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: 12px; font-weight: bold; color: #ffffff; margin-top: 10px;")
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(150)
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)
layout.addWidget(ffmpeg_group)
layout.addStretch()
def check_for_updates(self) -> None:
"""Check if FFmpeg updates are available."""
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 UI in main thread using signals
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}")
self.check_button.setEnabled(True)
self._show_check_error(str(e))
thread = threading.Thread(target=check_thread, daemon=True)
thread.start()
def _update_check_results(self, update_available: bool, current_version: str, latest_version: str) -> None:
"""Handle completion of update check."""
self.update_available = update_available
self.current_version = current_version
self.latest_version = latest_version
# Update version labels
self.current_version_label.setText(current_version)
self.latest_version_label.setText(latest_version)
# Update status
if current_version == "Not installed":
self.status_label.setText(_('ffmpeg_updater.install_first'))
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."""
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)