Add localization support and language selection

Introduces English and Spanish translation files, integrates localization throughout dialogs, and adds a language selection tab to the custom options dialog. UI strings are now loaded via the localization manager, enabling dynamic language switching and improved internationalization.
This commit is contained in:
Your Name
2025-09-30 20:53:59 +03:00
parent 0376c1c4cd
commit c5916d57c0
12 changed files with 1293 additions and 311 deletions
@@ -19,6 +19,8 @@ from PySide6.QtWidgets import (
QWidget,
)
from src.utils.ytsage_localization import _
from src.core.ytsage_ffmpeg import get_ffmpeg_path
from src.core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, refresh_version_cache
from src.core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path
@@ -60,7 +62,7 @@ class AboutDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self._parent = parent # Store parent to access version etc.
self.setWindowTitle("About YTSage")
self.setWindowTitle(_("about.title"))
self.setMinimumSize(460, 420) # Slightly increased to accommodate paths
self.resize(460, 440) # Slightly increased initial size
self.setMaximumSize(500, 480) # Reasonable maximum size
@@ -157,13 +159,13 @@ class AboutDialog(QDialog):
layout.addWidget(title_label)
version_label = QLabel(
f"<span style='color: #cccccc; font-size: 13px; font-weight: normal;'>Version {getattr(self._parent, 'version', '4.9.0b')}</span>"
f"<span style='color: #cccccc; font-size: 13px; font-weight: normal;'>{_('about.version', version=getattr(self._parent, 'version', '4.9.0b'))}</span>"
)
version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(version_label)
# Description - more compact
description_label = QLabel("Modern YouTube downloader with a clean PySide6 interface.")
description_label = QLabel(_("about.description"))
description_label.setWordWrap(True)
description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
description_label.setStyleSheet("color: #ffffff; font-size: 11px; margin: 6px 0;")
@@ -174,13 +176,13 @@ class AboutDialog(QDialog):
info_layout.setSpacing(15)
author_label = QLabel(
"By: <a href='https://github.com/oop7/' style='color: #c90000; text-decoration: none; font-size: 10px;'>oop7</a>"
f"{_('about.author', author='<a href=\'https://github.com/oop7/\' style=\'color: #c90000; text-decoration: none; font-size: 10px;\'>oop7</a>')}"
)
author_label.setOpenExternalLinks(True)
info_layout.addWidget(author_label)
repo_label = QLabel(
"GitHub: <a href='https://github.com/oop7/YTSage/' style='color: #c90000; text-decoration: none; font-size: 10px;'>YTSage</a>"
f"{_('about.github', repo='<a href=\'https://github.com/oop7/YTSage/\' style=\'color: #c90000; text-decoration: none; font-size: 10px;\'>YTSage</a>')}"
)
repo_label.setOpenExternalLinks(True)
info_layout.addWidget(repo_label)
@@ -219,7 +221,7 @@ class AboutDialog(QDialog):
header_layout.setContentsMargins(0, 0, 0, 5)
# System Information title
title_label = QLabel("System Information")
title_label = QLabel(_("about.system_info"))
title_label.setStyleSheet(
"""
QLabel {
@@ -237,7 +239,7 @@ class AboutDialog(QDialog):
header_layout.addStretch()
# Create refresh button
self.refresh_btn = QPushButton("🔄")
self.refresh_btn = QPushButton(_("about.refresh"))
self.refresh_btn.setFixedSize(16, 16)
self.refresh_btn.setStyleSheet(
"""
@@ -283,7 +285,7 @@ class AboutDialog(QDialog):
def _show_loading_message(self) -> None:
"""Show a compact loading message while system information is being gathered."""
loading_label = QLabel("🔄 Loading system information...")
loading_label = QLabel(_("about.loading"))
loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
loading_label.setStyleSheet(
"""
@@ -381,7 +383,7 @@ class AboutDialog(QDialog):
# yt-dlp Status - compact version with path
ytdlp_found = check_ytdlp_installed()
ytdlp_status_text = (
"<span style='color: #4CAF50;'>✓ Detected</span>" if ytdlp_found else "<span style='color: #F44336;'>✗ Missing</span>"
f"<span style='color: #4CAF50;'>{_('about.detected')}</span>" if ytdlp_found else f"<span style='color: #F44336;'>{_('about.missing')}</span>"
)
ytdlp_version = get_ytdlp_version()
@@ -409,11 +411,11 @@ class AboutDialog(QDialog):
# FFmpeg Status - compact version with path
ffmpeg_found = check_ffmpeg()
ffmpeg_status_text = (
"<span style='color: #4CAF50;'>✓ Detected</span>"
f"<span style='color: #4CAF50;'>{_('about.detected')}</span>"
if ffmpeg_found
else "<span style='color: #F44336;'>✗ Missing</span>"
else f"<span style='color: #F44336;'>{_('about.missing')}</span>"
)
ffmpeg_version = get_ffmpeg_version() if ffmpeg_found else "Not Available"
ffmpeg_version = get_ffmpeg_version() if ffmpeg_found else _('about.not_available')
# Get FFmpeg path
ffmpeg_path_text = None
@@ -440,7 +442,7 @@ class AboutDialog(QDialog):
def refresh_version_info(self) -> None:
"""Refresh version information manually."""
self.refresh_btn.setText("🔄 Refreshing...")
self.refresh_btn.setText(_('about.refreshing'))
self.refresh_btn.setEnabled(False)
# Perform refresh in a separate thread to avoid blocking UI
@@ -457,7 +459,7 @@ class AboutDialog(QDialog):
def on_refresh_finished(self, success) -> None:
"""Handle refresh completion."""
self.refresh_btn.setText("🔄 Refresh")
self.refresh_btn.setText(_('about.refresh'))
self.refresh_btn.setEnabled(True)
if success:
@@ -466,8 +468,8 @@ class AboutDialog(QDialog):
# Show error message with proper styling
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Icon.Warning)
msg_box.setWindowTitle("Refresh Failed")
msg_box.setText("Failed to refresh version information.")
msg_box.setWindowTitle(_('about.refresh_failed'))
msg_box.setText(_('about.refresh_failed_message'))
msg_box.setWindowIcon(self.windowIcon())
msg_box.setStyleSheet(
"""
@@ -31,6 +31,8 @@ from PySide6.QtWidgets import (
from src.core.ytsage_yt_dlp import get_yt_dlp_path
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
if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import)
@@ -68,7 +70,7 @@ class CommandWorker(QObject):
base_cmd.append(self.url)
# Emit the full command
self.output_received.emit(f"🔧 Full command: {' '.join(str(cmd) for cmd in base_cmd)}")
self.output_received.emit(_('custom_command.full_command', command=' '.join(str(cmd) for cmd in base_cmd)))
self.output_received.emit("=" * 50)
# Run the command
@@ -90,22 +92,22 @@ class CommandWorker(QObject):
self.output_received.emit("=" * 50)
if ret != 0:
self.output_received.emit(f"❌ Command failed with exit code {ret}")
self.output_received.emit(_('custom_command.command_failed', code=ret))
self.command_finished.emit(False, ret)
else:
self.output_received.emit("✅ Command completed successfully!")
self.output_received.emit(_('custom_command.command_success'))
self.command_finished.emit(True, ret)
except Exception as e:
self.output_received.emit("=" * 50)
self.error_occurred.emit(f"❌ Error executing command: {str(e)}")
self.error_occurred.emit(_('custom_command.command_error', error=str(e)))
class CustomOptionsDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self._parent: YTSageApp = cast("YTSageApp", self.parent()) # cast will help with auto complete and type hint checking.
self.setWindowTitle("Custom Options")
self.setWindowTitle(_("dialogs.custom_options"))
self.setMinimumSize(550, 400) # Made even shorter
layout = QVBoxLayout(self)
@@ -118,38 +120,35 @@ class CustomOptionsDialog(QDialog):
cookies_layout = QVBoxLayout(cookies_tab)
# Help text
help_text = QLabel(
"Choose how to provide cookies for logging in.\n"
"This allows downloading of private videos and premium quality audio."
)
help_text = QLabel(_('cookies.help_text'))
help_text.setWordWrap(True)
help_text.setStyleSheet("color: #999999; padding: 10px;")
cookies_layout.addWidget(help_text)
# Cookie source selection
cookie_source_group = QGroupBox("Cookie Source")
cookie_source_group = QGroupBox(_('cookies.cookie_source'))
cookie_source_layout = QVBoxLayout(cookie_source_group)
# Radio buttons for cookie source
self.cookie_file_radio = QRadioButton("Use cookie file (Netscape format)")
self.cookie_file_radio = QRadioButton(_('cookies.use_cookie_file'))
self.cookie_file_radio.setChecked(True)
self.cookie_file_radio.toggled.connect(self.on_cookie_source_changed)
cookie_source_layout.addWidget(self.cookie_file_radio)
self.cookie_browser_radio = QRadioButton("Extract cookies from browser")
self.cookie_browser_radio = QRadioButton(_('cookies.extract_from_browser'))
self.cookie_browser_radio.toggled.connect(self.on_cookie_source_changed)
cookie_source_layout.addWidget(self.cookie_browser_radio)
cookies_layout.addWidget(cookie_source_group)
# Cookie file section
self.cookie_file_group = QGroupBox("Cookie File")
self.cookie_file_group = QGroupBox(_('cookies.cookie_file'))
file_layout = QVBoxLayout(self.cookie_file_group)
# File path input and browse button
path_layout = QHBoxLayout()
self.cookie_path_input = QLineEdit()
self.cookie_path_input.setPlaceholderText("Path to cookies file (Netscape format)")
self.cookie_path_input.setPlaceholderText(_('cookies.cookie_file_placeholder'))
if hasattr(self._parent, "cookie_file_path") and self._parent.cookie_file_path:
# Convert Path to string properly and validate
cookie_path_str = str(self._parent.cookie_file_path)
@@ -158,7 +157,7 @@ class CustomOptionsDialog(QDialog):
self.cookie_path_input.setText(cookie_path_str)
path_layout.addWidget(self.cookie_path_input)
self.browse_button = QPushButton("Browse")
self.browse_button = QPushButton(_('buttons.browse'))
self.browse_button.clicked.connect(self.browse_cookie_file)
path_layout.addWidget(self.browse_button)
file_layout.addLayout(path_layout)
@@ -166,16 +165,16 @@ class CustomOptionsDialog(QDialog):
cookies_layout.addWidget(self.cookie_file_group)
# Browser selection section
self.cookie_browser_group = QGroupBox("Browser Selection")
self.cookie_browser_group = QGroupBox(_('cookies.browser_selection'))
browser_layout = QVBoxLayout(self.cookie_browser_group)
browser_help = QLabel("Select the browser to extract cookies from. Make sure the browser is closed before extraction.")
browser_help = QLabel(_('cookies.browser_help'))
browser_help.setWordWrap(True)
browser_help.setStyleSheet("color: #999999; font-size: 11px;")
browser_layout.addWidget(browser_help)
browser_select_layout = QHBoxLayout()
browser_select_layout.addWidget(QLabel("Browser:"))
browser_select_layout.addWidget(QLabel(_('cookies.browser_label')))
self.browser_combo = QComboBox()
self.browser_combo.addItems(["chrome", "firefox", "safari", "edge", "opera", "brave", "chromium", "vivaldi"])
@@ -184,9 +183,9 @@ class CustomOptionsDialog(QDialog):
# Optional profile field
profile_layout = QHBoxLayout()
profile_layout.addWidget(QLabel("Profile (optional):"))
profile_layout.addWidget(QLabel(_('cookies.profile_label')))
self.profile_input = QLineEdit()
self.profile_input.setPlaceholderText("Profile name or path (leave empty for default)")
self.profile_input.setPlaceholderText(_('cookies.profile_placeholder'))
profile_layout.addWidget(self.profile_input)
browser_layout.addLayout(profile_layout)
@@ -207,12 +206,7 @@ class CustomOptionsDialog(QDialog):
command_layout = QVBoxLayout(command_tab)
# Improved help text
cmd_help_text = QLabel(
"Enter your custom yt-dlp command below. The current URL will be automatically appended.<br><br>"
"For complete list of options and usage examples, "
f'<a href="{YTDLP_DOCS_URL}">click here to view the official yt-dlp documentation</a>.<br><br>'
"Note: Download path and output filename template will be automatically handled."
)
cmd_help_text = QLabel(_('custom_command.help_text', docs_url=YTDLP_DOCS_URL))
cmd_help_text.setWordWrap(True)
cmd_help_text.setOpenExternalLinks(True) # Enable clicking links
cmd_help_text.setTextFormat(Qt.TextFormat.RichText) # Enable HTML rendering
@@ -238,13 +232,13 @@ class CustomOptionsDialog(QDialog):
command_layout.addWidget(cmd_help_text)
# Command input label
input_label = QLabel("yt-dlp Arguments:")
input_label = QLabel(_('custom_command.input_label'))
input_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 10px;")
command_layout.addWidget(input_label)
# Command input
self.command_input = QPlainTextEdit()
self.command_input.setPlaceholderText("Enter yt-dlp arguments here...\n\n" "e.g. --extract-audio --audio-format mp3")
self.command_input.setPlaceholderText(_('custom_command.input_placeholder'))
self.command_input.setMinimumHeight(80) # Reduced further from 100
self.command_input.setStyleSheet(
"""
@@ -269,7 +263,7 @@ class CustomOptionsDialog(QDialog):
button_layout = QHBoxLayout()
button_layout.setSpacing(10)
clear_btn = QPushButton("Clear")
clear_btn = QPushButton(_('buttons.clear'))
clear_btn.clicked.connect(lambda: self.command_input.clear())
clear_btn.setStyleSheet(
"""
@@ -292,7 +286,7 @@ class CustomOptionsDialog(QDialog):
button_layout.addStretch() # Push run button to the right
# Run command button
self.run_btn = QPushButton("Run Command")
self.run_btn = QPushButton(_('buttons.run_command'))
self.run_btn.clicked.connect(self.run_custom_command)
self.run_btn.setDefault(True)
button_layout.addWidget(self.run_btn)
@@ -300,14 +294,14 @@ class CustomOptionsDialog(QDialog):
command_layout.addLayout(button_layout)
# Output label
output_label = QLabel("Command Output:")
output_label = QLabel(_('custom_command.output_label'))
output_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 15px;")
command_layout.addWidget(output_label)
# Log output
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setPlaceholderText("Command output will appear here...")
self.log_output.setPlaceholderText(_('custom_command.output_placeholder'))
self.log_output.setMinimumHeight(100) # Reduced further from 120
self.log_output.setStyleSheet(
"""
@@ -332,56 +326,50 @@ class CustomOptionsDialog(QDialog):
proxy_layout = QVBoxLayout(proxy_tab)
# Help text
proxy_help_text = QLabel(
"Configure proxy settings for network connections and geo-verification.\n"
"Proxy can help bypass regional restrictions and improve download performance."
)
proxy_help_text = QLabel(_('proxy.help_text'))
proxy_help_text.setWordWrap(True)
proxy_help_text.setStyleSheet("color: #999999; padding: 10px;")
proxy_layout.addWidget(proxy_help_text)
# Main Proxy section
main_proxy_group = QGroupBox("Main Proxy")
main_proxy_group = QGroupBox(_('proxy.main_proxy'))
main_proxy_layout = QVBoxLayout(main_proxy_group)
main_proxy_help = QLabel("Use the specified HTTP/HTTPS/SOCKS proxy for all connections.")
main_proxy_help = QLabel(_('proxy.main_proxy_help'))
main_proxy_help.setWordWrap(True)
main_proxy_help.setStyleSheet("color: #999999; font-size: 11px;")
main_proxy_layout.addWidget(main_proxy_help)
# Main proxy input
main_proxy_input_layout = QHBoxLayout()
main_proxy_input_layout.addWidget(QLabel("Proxy URL:"))
main_proxy_input_layout.addWidget(QLabel(_('proxy.proxy_url_label')))
self.proxy_url_input = QLineEdit()
self.proxy_url_input.setPlaceholderText("e.g., http://proxy.example.com:8080 or socks5://user:pass@127.0.0.1:1080")
self.proxy_url_input.setPlaceholderText(_('proxy.proxy_url_placeholder'))
self.proxy_url_input.textChanged.connect(self.validate_proxy_inputs)
main_proxy_input_layout.addWidget(self.proxy_url_input)
main_proxy_layout.addLayout(main_proxy_input_layout)
# Example text
example_label = QLabel("Examples: http://proxy.com:8080, https://proxy.com:8080, socks5://127.0.0.1:1080")
example_label = QLabel(_('proxy.proxy_examples'))
example_label.setStyleSheet("color: #888888; font-size: 10px; font-style: italic;")
main_proxy_layout.addWidget(example_label)
proxy_layout.addWidget(main_proxy_group)
# Geo-verification Proxy section
geo_proxy_group = QGroupBox("Geo-verification Proxy")
geo_proxy_group = QGroupBox(_('proxy.geo_proxy'))
geo_proxy_layout = QVBoxLayout(geo_proxy_group)
geo_proxy_help = QLabel(
"Use this proxy to verify IP address for geo-restricted sites. "
"The main proxy (if set) is used for actual downloading."
)
geo_proxy_help = QLabel(_('proxy.geo_proxy_help'))
geo_proxy_help.setWordWrap(True)
geo_proxy_help.setStyleSheet("color: #999999; font-size: 11px;")
geo_proxy_layout.addWidget(geo_proxy_help)
# Geo proxy input
geo_proxy_input_layout = QHBoxLayout()
geo_proxy_input_layout.addWidget(QLabel("Geo Proxy URL:"))
geo_proxy_input_layout.addWidget(QLabel(_('proxy.geo_proxy_url_label')))
self.geo_proxy_url_input = QLineEdit()
self.geo_proxy_url_input.setPlaceholderText("e.g., http://us-proxy.example.com:8080")
self.geo_proxy_url_input.setPlaceholderText(_('proxy.geo_proxy_url_placeholder'))
self.geo_proxy_url_input.textChanged.connect(self.validate_proxy_inputs)
geo_proxy_input_layout.addWidget(self.geo_proxy_url_input)
geo_proxy_layout.addLayout(geo_proxy_input_layout)
@@ -395,7 +383,7 @@ class CustomOptionsDialog(QDialog):
# Clear buttons
clear_layout = QHBoxLayout()
clear_main_proxy_btn = QPushButton("Clear Main Proxy")
clear_main_proxy_btn = QPushButton(_('proxy.clear_main_proxy'))
clear_main_proxy_btn.clicked.connect(lambda: self.proxy_url_input.clear())
clear_main_proxy_btn.setStyleSheet(
"""
@@ -414,7 +402,7 @@ class CustomOptionsDialog(QDialog):
)
clear_layout.addWidget(clear_main_proxy_btn)
clear_geo_proxy_btn = QPushButton("Clear Geo Proxy")
clear_geo_proxy_btn = QPushButton(_('proxy.clear_geo_proxy'))
clear_geo_proxy_btn.clicked.connect(lambda: self.geo_proxy_url_input.clear())
clear_geo_proxy_btn.setStyleSheet(
"""
@@ -438,13 +426,75 @@ class CustomOptionsDialog(QDialog):
proxy_layout.addStretch()
# === Language Tab ===
language_tab = QWidget()
language_layout = QVBoxLayout(language_tab)
# Help text
language_help_text = QLabel(_("language.select_language"))
language_help_text.setWordWrap(True)
language_help_text.setStyleSheet("color: #ffffff; font-size: 14px; font-weight: bold; padding: 10px;")
language_layout.addWidget(language_help_text)
# Current language info
current_lang = ConfigManager.get("language") or "en"
available_languages = LocalizationManager.get_available_languages()
current_lang_display = available_languages.get(current_lang, current_lang.upper())
current_lang_label = QLabel(_("language.current_language", language=current_lang_display))
current_lang_label.setWordWrap(True)
current_lang_label.setStyleSheet("color: #999999; padding: 10px;")
language_layout.addWidget(current_lang_label)
# Language selection group
language_group = QGroupBox(_("language.select_language"))
language_group_layout = QVBoxLayout(language_group)
# Language selection combo box
language_select_layout = QHBoxLayout()
language_select_layout.addWidget(QLabel(_("language.select_language") + ":"))
self.language_combo = QComboBox()
# Populate language combo with available languages
for lang_code, display_name in available_languages.items():
self.language_combo.addItem(display_name, lang_code)
# Set current selection
current_index = self.language_combo.findData(current_lang)
if current_index >= 0:
self.language_combo.setCurrentIndex(current_index)
# Connect language change event
self.language_combo.currentIndexChanged.connect(self.on_language_changed)
language_select_layout.addWidget(self.language_combo)
language_group_layout.addLayout(language_select_layout)
language_layout.addWidget(language_group)
# Restart notice
self.restart_notice = QLabel(_("language.restart_required"))
self.restart_notice.setWordWrap(True)
self.restart_notice.setStyleSheet(
"color: #ffaa00; font-style: italic; padding: 10px; "
"background-color: #2a2d36; border-radius: 6px; margin: 10px;"
)
self.restart_notice.setVisible(False) # Initially hidden
language_layout.addWidget(self.restart_notice)
language_layout.addStretch()
# Add tabs to the tab widget
self.tab_widget.addTab(cookies_tab, "Login with Cookies")
self.tab_widget.addTab(command_tab, "Custom Command")
self.tab_widget.addTab(proxy_tab, "Proxy")
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"))
# Dialog buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
@@ -524,7 +574,6 @@ class CustomOptionsDialog(QDialog):
border: none;
width: 12px;
height: 12px;
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgNEw2IDdMOSA0IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);
}
QComboBox QAbstractItemView {
background-color: #1d1e22;
@@ -610,18 +659,18 @@ class CustomOptionsDialog(QDialog):
else:
self.cookie_file_group.setVisible(False)
self.cookie_browser_group.setVisible(True)
self.cookie_status.setText("Browser cookies will be extracted when applied")
self.cookie_status.setText(_("cookies.browser_extract_message"))
self.cookie_status.setStyleSheet("color: #ffaa00; font-style: italic;")
def browse_cookie_file(self) -> None:
# Open file dialog to select cookie file
selected_files, _ = QFileDialog.getOpenFileName(self, "Select Cookie File", "", "Cookies files (*.txt *.lwp)")
selected_files, _ = QFileDialog.getOpenFileName(self, _("cookies.select_file_title"), "", _("cookies.file_filter"))
if selected_files:
# Ensure we have a valid full path
cookie_path = Path(selected_files).resolve()
self.cookie_path_input.setText(str(cookie_path))
self.cookie_status.setText("Cookie file selected - Click OK to apply")
self.cookie_status.setText(_("cookies.file_selected_message"))
self.cookie_status.setStyleSheet("color: #00cc00; font-style: italic;")
def get_cookie_file_path(self) -> Path | None:
@@ -760,7 +809,7 @@ class CustomOptionsDialog(QDialog):
self.log_output.append(f"📁 Download path: {path}")
self.log_output.append("=" * 50)
self.run_btn.setEnabled(False)
self.run_btn.setText("Running...")
self.run_btn.setText(_("command.running"))
# Create worker and thread
self.worker = CommandWorker(command, url, path)
@@ -781,49 +830,63 @@ class CustomOptionsDialog(QDialog):
def on_command_finished(self, success: bool, exit_code: int):
"""Slot for when command finishes"""
self.run_btn.setEnabled(True)
self.run_btn.setText("Run Command")
self.run_btn.setText(_("command.run_command"))
def on_error_occurred(self, error_msg: str):
"""Slot for handling errors"""
self.log_output.append(error_msg)
self.run_btn.setEnabled(True)
self.run_btn.setText("Run Command")
self.run_btn.setText(_("buttons.run_command"))
def on_language_changed(self) -> None:
"""Handle language selection change"""
selected_lang_code = self.language_combo.currentData()
if selected_lang_code:
current_lang = ConfigManager.get("language") or "en"
if selected_lang_code != current_lang:
# Save the new language preference
ConfigManager.set("language", selected_lang_code)
# Update LocalizationManager
LocalizationManager.set_language(selected_lang_code)
# Show restart notice
self.restart_notice.setVisible(True)
logger.info(f"Language changed to: {selected_lang_code}")
class TimeRangeDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("Download Video Section")
self.setWindowTitle(_('time_range.title'))
self.setMinimumWidth(400)
layout = QVBoxLayout(self)
# Help text explaining the feature
help_text = QLabel(
"Download only specific parts of a video by specifying time ranges.\n"
"Use HH:MM:SS format or seconds. Leave start or end empty to download from beginning or to end."
)
help_text = QLabel(_('time_range.help_text'))
help_text.setWordWrap(True)
help_text.setStyleSheet("color: #999999; padding: 10px;")
layout.addWidget(help_text)
# Time range section
time_group = QGroupBox("Time Range")
time_group = QGroupBox(_('time_range.time_range_group'))
time_layout = QVBoxLayout()
# Start time row
start_layout = QHBoxLayout()
start_layout.addWidget(QLabel("Start Time:"))
start_layout.addWidget(QLabel(_('time_range.start_time')))
self.start_time_input = QLineEdit()
self.start_time_input.setPlaceholderText("00:00:00 (or leave empty for start)")
self.start_time_input.setPlaceholderText(_('time_range.start_time_placeholder'))
start_layout.addWidget(self.start_time_input)
time_layout.addLayout(start_layout)
# End time row
end_layout = QHBoxLayout()
end_layout.addWidget(QLabel("End Time:"))
end_layout.addWidget(QLabel(_('time_range.end_time')))
self.end_time_input = QLineEdit()
self.end_time_input.setPlaceholderText("00:10:00 (or leave empty for end)")
self.end_time_input.setPlaceholderText(_('time_range.end_time_placeholder'))
end_layout.addWidget(self.end_time_input)
time_layout.addLayout(end_layout)
@@ -831,7 +894,7 @@ class TimeRangeDialog(QDialog):
layout.addWidget(time_group)
# Force keyframes option
self.force_keyframes = QCheckBox("Force keyframes at cuts (better accuracy, slower)")
self.force_keyframes = QCheckBox(_('time_range.force_keyframes'))
self.force_keyframes.setChecked(True)
self.force_keyframes.setStyleSheet(
"""
@@ -859,7 +922,9 @@ class TimeRangeDialog(QDialog):
layout.addWidget(self.force_keyframes)
# Buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
@@ -17,11 +17,13 @@ from PySide6.QtWidgets import (
QWidget,
)
from src.utils.ytsage_localization import _
class SubtitleSelectionDialog(QDialog):
def __init__(self, available_manual, available_auto, previously_selected, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("Select Subtitles")
self.setWindowTitle(_("dialogs.select_subtitles"))
self.setMinimumWidth(400)
self.setMinimumHeight(300)
@@ -35,7 +37,7 @@ class SubtitleSelectionDialog(QDialog):
# Filter input
self.filter_input = QLineEdit()
self.filter_input.setPlaceholderText("Filter languages (e.g., en, es)...")
self.filter_input.setPlaceholderText(_("dialogs.filter_languages_placeholder"))
self.filter_input.textChanged.connect(self.filter_list)
self.filter_input.setStyleSheet(
"""
@@ -72,7 +74,9 @@ class SubtitleSelectionDialog(QDialog):
self.populate_list()
# OK and Cancel buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
@@ -128,7 +132,8 @@ class SubtitleSelectionDialog(QDialog):
combined_subs[lang_code] = f"{lang_code} - Auto-generated"
if not combined_subs:
no_subs_label = QLabel("No subtitles available" + (f" matching '{filter_text}'" if filter_text else ""))
matching_text = _("dialogs.matching") if filter_text else ""
no_subs_label = QLabel(_("dialogs.no_subtitles_available") + (f" {matching_text} '{filter_text}'" if filter_text else ""))
no_subs_label.setStyleSheet("color: #aaaaaa; padding: 10px;")
self.list_layout.addWidget(no_subs_label)
return
@@ -205,8 +210,8 @@ class PlaylistSelectionDialog(QDialog):
# Top buttons (Select/Deselect All)
button_layout = QHBoxLayout()
select_all_btn = QPushButton("Select All")
deselect_all_btn = QPushButton("Deselect All")
select_all_btn = QPushButton(_("buttons.select_all"))
deselect_all_btn = QPushButton(_("buttons.deselect_all"))
select_all_btn.clicked.connect(self._select_all)
deselect_all_btn.clicked.connect(self._deselect_all)
# Style the buttons to match the subtitle dialog
@@ -250,7 +255,9 @@ class PlaylistSelectionDialog(QDialog):
self._populate_list(previously_selected_string)
# Dialog buttons (OK/Cancel)
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
@@ -428,50 +435,50 @@ class SponsorBlockCategoryDialog(QDialog):
# Default SponsorBlock categories with descriptions
SPONSORBLOCK_CATEGORIES = {
"sponsor": {
"name": "Sponsor",
"description": "Paid promotion, paid referrals and direct advertisements",
"name_key": "sponsorblock.sponsor",
"description_key": "sponsorblock.sponsor_desc",
"default": True,
},
"selfpromo": {
"name": "Unpaid/Self Promotion",
"description": "Unpaid promotion of creators' own content",
"name_key": "sponsorblock.selfpromo",
"description_key": "sponsorblock.selfpromo_desc",
"default": True,
},
"interaction": {
"name": "Interaction Reminder",
"description": "Asking viewers to like, subscribe, or follow social media",
"name_key": "sponsorblock.interaction",
"description_key": "sponsorblock.interaction_desc",
"default": True,
},
"intro": {
"name": "Intro",
"description": "Video introduction that can be skipped",
"name_key": "sponsorblock.intro",
"description_key": "sponsorblock.intro_desc",
"default": False,
},
"outro": {
"name": "Outro/End Cards",
"description": "Credits or when the video ends",
"name_key": "sponsorblock.outro",
"description_key": "sponsorblock.outro_desc",
"default": False,
},
"preview": {
"name": "Preview/Recap",
"description": "Quick recap of previous videos or preview of what's coming up",
"name_key": "sponsorblock.preview",
"description_key": "sponsorblock.preview_desc",
"default": False,
},
"music_offtopic": {
"name": "Non-Music Section",
"description": "Only for music videos. Marks non-music sections",
"name_key": "sponsorblock.music_offtopic",
"description_key": "sponsorblock.music_offtopic_desc",
"default": False,
},
"filler": {
"name": "Filler Tangent",
"description": "Tangential scenes added only for filler or humor",
"name_key": "sponsorblock.filler",
"description_key": "sponsorblock.filler_desc",
"default": False,
},
}
def __init__(self, previously_selected=None, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("SponsorBlock Categories")
self.setWindowTitle(_("dialogs.sponsorblock_categories"))
self.setMinimumWidth(500)
self.setMinimumHeight(400)
@@ -489,15 +496,12 @@ class SponsorBlockCategoryDialog(QDialog):
layout = QVBoxLayout(self)
# Title and description
title_label = QLabel("SponsorBlock Categories")
title_label = QLabel(_("dialogs.sponsorblock_categories"))
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; margin: 10px;")
layout.addWidget(title_label)
desc_label = QLabel(
"Select which types of video segments to automatically remove during download.\n"
"SponsorBlock uses community-submitted data to identify these segments."
)
desc_label = QLabel(_("dialogs.sponsorblock_description"))
desc_label.setWordWrap(True)
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
@@ -521,8 +525,8 @@ class SponsorBlockCategoryDialog(QDialog):
category_layout.setContentsMargins(0, 0, 0, 0)
category_layout.setSpacing(2)
# Create checkbox with just the name
checkbox = QCheckBox(category_info["name"])
# Create checkbox with localized name
checkbox = QCheckBox(_(category_info["name_key"]))
checkbox.setProperty("category_id", category_id)
# Determine if this category should be checked
@@ -559,8 +563,8 @@ class SponsorBlockCategoryDialog(QDialog):
"""
)
# Create description label
desc_label = QLabel(category_info["description"])
# Create description label with localized text
desc_label = QLabel(_(category_info["description_key"]))
desc_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-left: 28px; margin-bottom: 8px;")
desc_label.setWordWrap(True)
@@ -577,15 +581,15 @@ class SponsorBlockCategoryDialog(QDialog):
# Quick selection buttons
button_layout = QHBoxLayout()
select_defaults_btn = QPushButton("Select Defaults")
select_defaults_btn = QPushButton(_("buttons.select_defaults"))
select_defaults_btn.clicked.connect(self.select_defaults)
select_defaults_btn.setStyleSheet(self._get_button_style())
select_all_btn = QPushButton("Select All")
select_all_btn = QPushButton(_("buttons.select_all"))
select_all_btn.clicked.connect(self.select_all)
select_all_btn.setStyleSheet(self._get_button_style())
deselect_all_btn = QPushButton("Deselect All")
deselect_all_btn = QPushButton(_("buttons.deselect_all"))
deselect_all_btn.clicked.connect(self.deselect_all)
deselect_all_btn.setStyleSheet(self._get_button_style())
@@ -597,7 +601,9 @@ class SponsorBlockCategoryDialog(QDialog):
layout.addLayout(button_layout)
# Dialog buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
@@ -34,12 +34,13 @@ from src.core.ytsage_utils import (
update_auto_update_settings,
)
from src.utils.ytsage_logger import logger
from src.utils.ytsage_localization import _
class DownloadSettingsDialog(QDialog):
def __init__(self, current_path, current_limit, current_unit_index, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("Download Settings")
self.setWindowTitle(_("settings.title"))
self.setMinimumWidth(450)
self.setMinimumHeight(400)
self.current_path = current_path
@@ -163,7 +164,7 @@ class DownloadSettingsDialog(QDialog):
layout = QVBoxLayout(self)
# --- Download Path Section ---
path_group_box = QGroupBox("Download Path")
path_group_box = QGroupBox(_("settings.download_path"))
path_layout = QVBoxLayout()
self.path_display = QLabel(str(self.current_path))
@@ -173,7 +174,8 @@ class DownloadSettingsDialog(QDialog):
)
path_layout.addWidget(self.path_display)
browse_button = QPushButton("Browse...")
browse_button = QPushButton(_("settings.browse"))
browse_button.clicked.connect(self.browse_new_path)
path_layout.addWidget(browse_button)
@@ -181,11 +183,11 @@ class DownloadSettingsDialog(QDialog):
layout.addWidget(path_group_box)
# --- Speed Limit Section ---
speed_group_box = QGroupBox("Speed Limit")
speed_group_box = QGroupBox(_("settings.speed_limit"))
speed_layout = QHBoxLayout()
self.speed_limit_input = QLineEdit(str(self.current_limit))
self.speed_limit_input.setPlaceholderText("None")
self.speed_limit_input.setPlaceholderText(_("settings.speed_limit_placeholder"))
speed_layout.addWidget(self.speed_limit_input)
self.speed_limit_unit = QComboBox()
@@ -197,25 +199,25 @@ class DownloadSettingsDialog(QDialog):
layout.addWidget(speed_group_box)
# --- Auto-Update yt-dlp Section ---
auto_update_group_box = QGroupBox("Auto-Update yt-dlp")
auto_update_group_box = QGroupBox(_("settings.auto_update_ytdlp"))
auto_update_layout = QVBoxLayout()
# Load current auto-update settings
auto_settings = get_auto_update_settings()
# Enable/Disable auto-update checkbox
self.auto_update_enabled = QCheckBox("Enable automatic yt-dlp updates")
self.auto_update_enabled = QCheckBox(_("settings.enable_auto_updates"))
self.auto_update_enabled.setChecked(auto_settings["enabled"])
auto_update_layout.addWidget(self.auto_update_enabled)
# Frequency options
frequency_label = QLabel("Update frequency:")
frequency_label = QLabel(_("settings.update_frequency"))
frequency_label.setStyleSheet("color: #ffffff; margin-top: 10px;")
auto_update_layout.addWidget(frequency_label)
self.startup_radio = QRadioButton("Check on every startup (minimum 1 hour between checks)")
self.daily_radio = QRadioButton("Check daily")
self.weekly_radio = QRadioButton("Check weekly")
self.startup_radio = QRadioButton(_("settings.check_startup"))
self.daily_radio = QRadioButton(_("settings.check_daily"))
self.weekly_radio = QRadioButton(_("settings.check_weekly"))
# Set current selection based on saved settings
current_frequency = auto_settings["frequency"]
@@ -232,7 +234,7 @@ class DownloadSettingsDialog(QDialog):
# Test update button
test_update_layout = QHBoxLayout()
test_update_button = QPushButton("Check for Updates Now")
test_update_button = QPushButton(_("settings.check_updates_now"))
test_update_button.clicked.connect(self.test_update_check)
test_update_layout.addWidget(test_update_button)
test_update_layout.addStretch()
@@ -242,13 +244,15 @@ class DownloadSettingsDialog(QDialog):
layout.addWidget(auto_update_group_box)
# Dialog buttons (OK/Cancel)
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box = QDialogButtonBox()
ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole)
cancel_button = button_box.addButton(_("buttons.cancel"), QDialogButtonBox.ButtonRole.RejectRole)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
def browse_new_path(self) -> None:
new_path = QFileDialog.getExistingDirectory(self, "Select Download Directory", str(self.current_path))
new_path = QFileDialog.getExistingDirectory(self, _("dialogs.select_folder"), str(self.current_path))
if new_path:
self.current_path = new_path
self.path_display.setText(self.current_path)
@@ -316,8 +320,8 @@ class DownloadSettingsDialog(QDialog):
if "Error" in current_version:
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning,
"Update Check",
"Could not determine current yt-dlp version.",
_("settings.update_check_title"),
_("settings.could_not_determine_version"),
)
msg_box.exec()
return
@@ -334,22 +338,22 @@ class DownloadSettingsDialog(QDialog):
if version_parser.parse(latest_version) > version_parser.parse(current_version):
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information,
"Update Check",
f"Update available!\n\nCurrent: {current_version}\nLatest: {latest_version}\n\nUse the 'Update yt-dlp' button in the main window to update.",
_("settings.update_check_title"),
_("settings.update_available_dialog", current=current_version, latest=latest_version),
)
msg_box.exec()
else:
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information,
"Update Check",
f"yt-dlp is up to date!\n\nCurrent version: {current_version}",
_("settings.update_check_title"),
_("settings.up_to_date_dialog", version=current_version),
)
msg_box.exec()
except Exception as e:
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning,
"Update Check",
f"Error checking for updates: {e}",
_("settings.update_check_title"),
_("settings.error_checking_updates", error=str(e)),
)
msg_box.exec()
@@ -375,13 +379,13 @@ class DownloadSettingsDialog(QDialog):
if update_auto_update_settings(enabled, frequency):
QMessageBox.information(
self,
"Settings Saved",
"Auto-update settings have been saved successfully!",
_("settings.settings_saved_title"),
_("settings.settings_saved_message"),
)
else:
QMessageBox.warning(self, "Error", "Failed to save auto-update settings.")
QMessageBox.warning(self, _("settings.error_title"), _("settings.failed_save_settings"))
except Exception as e:
QMessageBox.critical(self, "Error", f"Error saving auto-update settings: {e}")
QMessageBox.critical(self, _("settings.error_title"), _("settings.error_saving_settings", error=str(e)))
# Call the parent accept method to close the dialog
super().accept()
@@ -390,7 +394,7 @@ class DownloadSettingsDialog(QDialog):
class AutoUpdateSettingsDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("Auto-Update Settings")
self.setWindowTitle(_("settings.auto_update_title"))
self.setMinimumWidth(400)
self.setMinimumHeight(300)
@@ -406,32 +410,32 @@ class AutoUpdateSettingsDialog(QDialog):
layout = QVBoxLayout(self)
# Title
title_label = QLabel("<h2>🔄 Auto-Update Settings</h2>")
title_label = QLabel(f"<h2>{_("settings.auto_update_header")}</h2>")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label)
# Description
desc_label = QLabel("Configure automatic updates for yt-dlp to ensure you always have the latest features and bug fixes.")
desc_label = QLabel(_("settings.auto_update_description"))
desc_label.setWordWrap(True)
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
layout.addWidget(desc_label)
# Enable/Disable auto-update
self.enable_checkbox = QCheckBox("Enable automatic yt-dlp updates")
self.enable_checkbox = QCheckBox(_("settings.enable_auto_updates"))
self.enable_checkbox.setChecked(True) # Default enabled
self.enable_checkbox.toggled.connect(self.on_enable_toggled)
layout.addWidget(self.enable_checkbox)
# Frequency options
frequency_group = QGroupBox("Update Frequency")
frequency_group = QGroupBox(_("settings.update_frequency_group"))
frequency_layout = QVBoxLayout()
self.frequency_group = QButtonGroup(self)
self.startup_radio = QRadioButton("Check on every startup (minimum 1 hour between checks)")
self.daily_radio = QRadioButton("Check daily")
self.weekly_radio = QRadioButton("Check weekly")
self.startup_radio = QRadioButton(_("settings.check_startup"))
self.daily_radio = QRadioButton(_("settings.check_daily"))
self.weekly_radio = QRadioButton(_("settings.check_weekly"))
self.daily_radio.setChecked(True) # Default to daily
@@ -447,12 +451,12 @@ class AutoUpdateSettingsDialog(QDialog):
layout.addWidget(frequency_group)
# Current status
status_group = QGroupBox("Current Status")
status_group = QGroupBox(_("settings.current_status"))
status_layout = QVBoxLayout()
self.current_version_label = QLabel("Current yt-dlp version: Checking...")
self.last_check_label = QLabel("Last update check: Never")
self.next_check_label = QLabel("Next check: Based on settings")
self.current_version_label = QLabel(_("settings.current_version_label"))
self.last_check_label = QLabel(_("settings.last_check_label"))
self.next_check_label = QLabel(_("settings.next_check_label"))
status_layout.addWidget(self.current_version_label)
status_layout.addWidget(self.last_check_label)
@@ -462,17 +466,17 @@ class AutoUpdateSettingsDialog(QDialog):
layout.addWidget(status_group)
# Manual check button
self.manual_check_btn = QPushButton("🔍 Check for Updates Now")
self.manual_check_btn = QPushButton(_("settings.manual_check_button"))
self.manual_check_btn.clicked.connect(self.manual_check)
layout.addWidget(self.manual_check_btn)
# Buttons
button_layout = QHBoxLayout()
self.save_btn = QPushButton("Save Settings")
self.save_btn = QPushButton(_("settings.save_settings"))
self.save_btn.clicked.connect(self.save_settings)
self.cancel_btn = QPushButton("Cancel")
self.cancel_btn = QPushButton(_("buttons.cancel"))
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.save_btn)
@@ -721,19 +725,19 @@ class AutoUpdateSettingsDialog(QDialog):
if update_auto_update_settings(enabled, frequency):
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information,
"Settings Saved",
"✅ Auto-update settings have been saved successfully!",
_("settings.settings_saved_title"),
_("settings.settings_saved_successfully"),
)
msg_box.exec()
self.accept()
else:
msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning,
"Error",
"❌ Failed to save auto-update settings.\nPlease try again.",
_("settings.error_title"),
_("settings.failed_save_settings"),
)
msg_box.exec()
except Exception as e:
logger.exception(f"Error saving auto-update settings: {e}")
msg_box = self._create_styled_message_box(QMessageBox.Icon.Critical, "Error", f"❌ Error saving settings: {e}")
msg_box = self._create_styled_message_box(QMessageBox.Icon.Critical, _("settings.error_title"), _("settings", "error_saving", error=str(e)))
msg_box.exec()
@@ -18,6 +18,7 @@ from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QPushB
from src.core.ytsage_utils import get_ytdlp_version, load_config, save_config
from src.core.ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL
from src.utils.ytsage_localization import _
from src.utils.ytsage_logger import logger
try:
@@ -118,15 +119,15 @@ class UpdateThread(QThread):
error_message = ""
success = False
try:
self.update_status.emit("🔍 Checking current installation...")
self.update_status.emit(_('update.checking_current'))
self.update_progress.emit(10)
# Get the yt-dlp path
try:
yt_dlp_path = get_yt_dlp_path()
self.update_status.emit(f"📍 Found yt-dlp at: {yt_dlp_path}")
self.update_status.emit(_('update.found_at', path=yt_dlp_path))
except Exception as e:
self.update_status.emit(f"❌ Error getting yt-dlp path: {e}")
self.update_status.emit(_('update.error_getting_path', error=e))
self.update_finished.emit(False, f"❌ Error getting yt-dlp path: {e}")
return
@@ -152,17 +153,17 @@ class UpdateThread(QThread):
is_app_managed = False
if is_app_managed:
self.update_status.emit("📦 Updating app-managed yt-dlp binary...")
self.update_status.emit(_('update.updating_binary'))
success = self._update_binary(yt_dlp_path)
else:
self.update_status.emit("🐍 Updating system yt-dlp via pip...")
self.update_status.emit(_('update.updating_pip'))
success = self._update_via_pip()
if success:
self.update_progress.emit(100)
error_message = "✅ yt-dlp has been successfully updated!"
else:
error_message = "❌ Failed to update yt-dlp. Please try again or check your internet connection."
error_message = _('update.update_failed')
except requests.RequestException as e:
error_message = f"❌ Network error during update: {e}"
@@ -196,61 +197,61 @@ class UpdateThread(QThread):
logger.info("UpdateThread: yt-dlp update completed successfully.")
if result.stdout:
logger.debug(f"yt-dlp output: {result.stdout.strip()}")
self.update_status.emit("✅ Binary successfully updated!")
self.update_status.emit(_('update.binary_updated'))
self.update_progress.emit(95)
return True
else:
logger.error(f"UpdateThread: yt-dlp update failed. {result.stderr.strip()}")
self.update_status.emit(f"❌ yt-dlp update failed: {result.stderr.strip()}")
self.update_status.emit(_('update.update_failed_stderr', error=result.stderr.strip()))
return False
except subprocess.TimeoutExpired:
logger.error("UpdateThread: yt-dlp update timed out.")
self.update_status.emit("❌ yt-dlp update timed out.")
self.update_status.emit(_('update.update_timeout'))
return False
except Exception as e:
logger.exception(f"UpdateThread: Unexpected error during update: {e}")
self.update_status.emit(f"❌ Unexpected error during update: {e}")
self.update_status.emit(_('update.unexpected_error', error=e))
return False
def _update_via_pip(self) -> bool:
"""Update yt-dlp via pip."""
try:
self.update_status.emit("🔍 Checking current pip installation...")
self.update_status.emit(_('update.checking_pip'))
self.update_progress.emit(30)
# Get current version
try:
current_version = get_version("yt-dlp")
self.update_status.emit(f"📋 Current version: {current_version}")
self.update_status.emit(_('update.current_version', version=current_version))
except PackageNotFoundError:
self.update_status.emit("⚠️ yt-dlp not found via pip, attempting installation...")
self.update_status.emit(_('update.not_found_pip'))
current_version = "0.0.0"
self.update_progress.emit(40)
# Get the latest version from PyPI
self.update_status.emit("🌐 Checking for latest version...")
self.update_status.emit(_('update.checking_latest'))
response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
if response.status_code != 200:
self.update_status.emit("❌ Failed to check for updates")
self.update_status.emit(_('update.failed_check_updates'))
return False
data = response.json()
latest_version = data["info"]["version"]
self.update_status.emit(f"🆕 Latest version: {latest_version}")
self.update_status.emit(_('update.latest_version', version=latest_version))
self.update_progress.emit(50)
# Compare versions
if version.parse(latest_version) > version.parse(current_version):
self.update_status.emit(f"⬆️ Updating from {current_version} to {latest_version}...")
self.update_status.emit(_('update.updating_from_to', current=current_version, latest=latest_version))
self.update_progress.emit(60)
try:
# Run pip update with timeout
self.update_status.emit("📦 Running pip install --upgrade...")
self.update_status.emit(_('update.running_pip_install'))
update_result = subprocess.run(
[sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"],
capture_output=True,
@@ -263,11 +264,11 @@ class UpdateThread(QThread):
self.update_progress.emit(85)
if update_result.returncode == 0:
self.update_status.emit("✅ Pip update completed successfully!")
self.update_status.emit(_('update.pip_completed'))
self.update_progress.emit(95)
return True
else:
self.update_status.emit(f"❌ Pip update failed: {update_result.stderr}")
self.update_status.emit(_('update.pip_failed', error=update_result.stderr))
return False
except subprocess.TimeoutExpired:
@@ -289,7 +290,7 @@ class UpdateThread(QThread):
class YTDLPUpdateDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("Update yt-dlp")
self.setWindowTitle(_('update.title'))
self.setMinimumWidth(450)
self.setMinimumHeight(200)
self._closing = False # Flag to track if dialog is closing
@@ -297,7 +298,7 @@ class YTDLPUpdateDialog(QDialog):
layout = QVBoxLayout(self)
# Status label
self.status_label = QLabel("Checking for updates...")
self.status_label = QLabel(_('update.checking'))
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setWordWrap(True)
self.status_label.setMinimumHeight(60)
@@ -310,11 +311,11 @@ class YTDLPUpdateDialog(QDialog):
# Buttons
button_layout = QHBoxLayout()
self.update_btn = QPushButton("Update")
self.update_btn = QPushButton(_('buttons.update'))
self.update_btn.clicked.connect(self.perform_update)
self.update_btn.setEnabled(False)
self.close_btn = QPushButton("Close")
self.close_btn = QPushButton(_('buttons.close'))
self.close_btn.clicked.connect(self.close)
button_layout.addWidget(self.update_btn)
@@ -370,7 +371,7 @@ class YTDLPUpdateDialog(QDialog):
self.check_version()
def check_version(self) -> None:
self.status_label.setText("Checking for updates...")
self.status_label.setText(_('update.checking'))
self.update_btn.setEnabled(False)
self.version_check_thread = VersionCheckThread()
self.version_check_thread.finished.connect(self.on_version_check_finished)
@@ -387,7 +388,7 @@ class YTDLPUpdateDialog(QDialog):
return
if not current_version or not latest_version:
self.status_label.setText("Could not determine versions.")
self.status_label.setText(_('update.could_not_determine'))
self.update_btn.setEnabled(False)
return
@@ -398,7 +399,7 @@ class YTDLPUpdateDialog(QDialog):
if current_ver < latest_ver:
self.status_label.setText(
f"Update available!\nCurrent version: {current_version}\nLatest version: {latest_version}"
_('update.update_available', current=current_version, latest=latest_version)
)
self.update_btn.setEnabled(True)
else:
@@ -408,22 +409,22 @@ class YTDLPUpdateDialog(QDialog):
# If version parsing fails, do a simple string comparison
if current_version != latest_version:
self.status_label.setText(
f"Update available! (Comparison failed)\nCurrent: {current_version}\nLatest: {latest_version}"
_('update.update_available_failed', current=current_version, latest=latest_version)
)
self.update_btn.setEnabled(True)
else:
self.status_label.setText(f"yt-dlp is up to date (version {current_version})")
self.status_label.setText(_('update.up_to_date', version=current_version))
self.update_btn.setEnabled(False)
except Exception as e:
self.status_label.setText(f"Error comparing versions: {e}")
self.status_label.setText(_('update.error_comparing', error=e))
self.update_btn.setEnabled(False)
def perform_update(self) -> None:
# Immediate visual feedback
self.update_btn.setEnabled(False)
self.close_btn.setEnabled(False)
self.update_btn.setText("Updating...")
self.status_label.setText("🚀 Initializing update process...")
self.update_btn.setText(_('update.updating'))
self.status_label.setText(_('update.initializing'))
# Show progress bar immediately
self.progress_bar.setRange(0, 100)
@@ -461,7 +462,7 @@ class YTDLPUpdateDialog(QDialog):
self.progress_bar.setValue(100)
self.status_label.setText(message)
self.close_btn.setEnabled(True)
self.update_btn.setText("Update") # Reset button text
self.update_btn.setText(_('buttons.update')) # Reset button text
if success:
# Show success briefly then auto-check version