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
+316
View File
@@ -0,0 +1,316 @@
{
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Ready"
},
"formats": {
"show_formats": "Show Formats:",
"video_format": "Video Format:",
"audio_format": "Audio Format:",
"no_formats": "No formats available",
"loading": "Loading formats...",
"select": "Select",
"quality": "Quality",
"extension": "Extension",
"resolution": "Resolution",
"file_size": "File Size",
"codec": "Codec",
"audio": "Audio",
"notes": "Notes",
"will_merge_audio": "Will merge audio",
"has_audio": "✓ Has Audio",
"audio_only": "Audio Only",
"large_size": "Large size",
"medium_size": "Medium size",
"standard_size": "Standard size",
"compatible": "Compatible",
"efficient": "Efficient",
"high_quality": "High quality",
"best_4k": "Best (4K)",
"best_2k": "Best (2K)",
"high_1080p": "High (1080p)",
"high_720p": "High (720p)",
"medium_480p": "Medium (480p)",
"low_quality": "Low Quality",
"best_audio": "Best Audio",
"high_audio": "High Audio",
"medium_audio": "Medium Audio",
"low_audio": "Low Audio",
"audio_only_resolution": "Audio only"
},
"buttons": {
"download": "Download",
"pause": "Pause",
"resume": "Resume",
"cancel": "Cancel",
"browse": "Browse",
"clear": "Clear",
"ok": "OK",
"apply": "Apply",
"close": "Close",
"run_command": "Run Command",
"custom_command_help": "Help",
"about": "About",
"analyze": "Analyze",
"paste_url": "Paste URL",
"select_videos": "Select Videos...",
"video": "Video",
"audio_only": "Audio Only",
"custom_options": "Custom Options",
"trim_video": "Trim Video",
"download_settings": "Download Settings",
"update": "Update yt-dlp",
"select_defaults": "Select Defaults",
"select_all": "Select All",
"deselect_all": "Deselect All"
},
"dialogs": {
"custom_options": "Custom Options",
"settings": "Settings",
"select_folder": "Select Download Folder",
"sponsorblock_categories": "SponsorBlock Categories",
"sponsorblock_description": "Select which types of video segments to automatically remove during download.\nSponsorBlock uses community-submitted data to identify these segments.",
"select_subtitles": "Select Subtitles",
"filter_languages_placeholder": "Filter languages (e.g., en, es)...",
"no_subtitles_available": "No subtitles available",
"matching": "matching"
},
"tabs": {
"cookies": "Login with Cookies",
"custom_command": "Custom Command",
"proxy": "Proxy",
"language": "Language"
},
"cookies": {
"help_text": "Choose how to provide cookies for logging in.\nThis allows downloading of private videos and premium quality audio.",
"cookie_source": "Cookie Source",
"use_cookie_file": "Use cookie file",
"extract_from_browser": "Extract from browser",
"cookie_file": "Cookie File",
"cookie_file_placeholder": "Path to cookies.txt file...",
"browser_selection": "Browser Selection",
"browser_help": "Select the browser to extract cookies from:",
"browser_label": "Browser:",
"profile_label": "Profile:",
"profile_placeholder": "default",
"browser_extract_message": "Browser cookies will be extracted when applied",
"file_selected_message": "Cookie file selected - Click OK to apply",
"select_file_title": "Select Cookie File",
"file_filter": "Cookies files (*.txt *.lwp)"
},
"custom_command": {
"help_text": "Enter your custom yt-dlp command below. The current URL will be appended automatically.<br><br>For the full list of options and usage examples, <a href=\"{docs_url}\">click here to view the official yt-dlp documentation</a>.<br><br>Note: Download path and filename template will be handled automatically.",
"input_label": "yt-dlp Arguments:",
"input_placeholder": "Enter yt-dlp arguments here...\n\ne.g. --extract-audio --audio-format mp3",
"output_label": "Command Output:",
"output_placeholder": "Command output will appear here...",
"command_placeholder": "Enter custom yt-dlp command...",
"command_help": "Available placeholders:\n{url} - Video URL\n{output} - Output directory\n\nExample: --write-info-json --write-thumbnail",
"full_command": "🔧 Full command: {command}",
"command_success": "✅ Custom command completed successfully!",
"command_failed": "❌ Command failed with exit code {code}",
"command_error": "❌ Error running custom command: {error}"
},
"proxy": {
"help_text": "Configure proxy settings for downloading. Leave empty to use direct connection.",
"main_proxy": "Main Proxy",
"main_proxy_help": "Primary proxy server for all downloads. Supports HTTP/HTTPS and SOCKS5 protocols.",
"proxy_url_label": "Proxy URL:",
"proxy_url_placeholder": "http://proxy:port or socks5://proxy:port",
"proxy_examples": "Examples:\n• HTTP: http://proxy.example.com:8080\n• SOCKS5: socks5://proxy.example.com:1080",
"geo_proxy": "Geo-bypass Proxy",
"geo_proxy_help": "Secondary proxy specifically for bypassing geographic restrictions.",
"geo_proxy_url_label": "Geo-bypass Proxy URL:",
"geo_proxy_url_placeholder": "http://geo-proxy:port",
"clear_main_proxy": "Clear Main Proxy",
"clear_geo_proxy": "Clear Geo Proxy",
"proxy_url": "Proxy URL",
"proxy_placeholder": "http://proxy:port or socks5://proxy:port",
"geo_bypass": "Geo-bypass proxy (for geographic restrictions)",
"geo_bypass_placeholder": "http://proxy:port for bypassing geo-blocks",
"invalid_main_url": "Invalid main proxy URL format",
"invalid_geo_url": "Invalid geo proxy URL format",
"main_configured": "Main proxy configured",
"geo_configured": "Geo proxy configured"
},
"language": {
"help_text": "Select your preferred language for the interface.",
"current_language": "Current Language:",
"restart_notice": "Language change will take effect after restarting the application."
},
"download": {
"preparing": "Preparing download...",
"please_enter_url": "Please enter a URL first.",
"please_set_path": "Please set a download path first.",
"please_enter_url_and_path": "Please enter a URL and set download path.",
"please_select_format": "Please select a format first."
},
"update": {
"title": "Update yt-dlp",
"checking": "Checking for updates...",
"update_available": "Update available!\nCurrent version: {current}\nLatest version: {latest}",
"up_to_date": "yt-dlp is up to date (version {version})",
"could_not_determine": "Could not determine versions.",
"error_comparing": "Error comparing versions: {error}",
"update_available_failed": "Update available! (Comparison failed)\nCurrent: {current}\nLatest: {latest}",
"updating": "Updating...",
"initializing": "🚀 Initializing update process...",
"checking_current": "🔍 Checking current installation...",
"found_at": "📍 Found yt-dlp at: {path}",
"error_getting_path": "❌ Error getting yt-dlp path: {error}",
"updating_binary": "📦 Updating app-managed yt-dlp binary...",
"updating_pip": "🐍 Updating system yt-dlp via pip...",
"update_failed": "❌ Failed to update yt-dlp. Please try again or check your internet connection.",
"binary_updated": "✅ Binary successfully updated!",
"update_failed_stderr": "❌ yt-dlp update failed: {error}",
"update_timeout": "❌ yt-dlp update timed out.",
"unexpected_error": "❌ Unexpected error during update: {error}",
"checking_pip": "🔍 Checking current pip installation...",
"current_version": "📋 Current version: {version}",
"not_found_pip": "⚠️ yt-dlp not found via pip, attempting installation...",
"checking_latest": "🌐 Checking for latest version...",
"failed_check_updates": "❌ Failed to check for updates",
"latest_version": "🆕 Latest version: {version}",
"updating_from_to": "⬆️ Updating from {current} to {latest}...",
"running_pip_install": "📦 Running pip install --upgrade...",
"pip_completed": "✅ Pip update completed successfully!",
"pip_failed": "❌ Pip update failed: {error}"
},
"about": {
"title": "About YTSage",
"version": "Version: {version}",
"description": "A modern YouTube downloader with advanced features",
"author": "Created with ❤️",
"license": "License: MIT",
"system_info": "System Information",
"python_version": "Python Version: {version}",
"pyside_version": "PySide6 Version: {version}",
"ytdlp_version": "yt-dlp Version: {version}",
"ffmpeg_status": "FFmpeg Status: {status}",
"ffmpeg_available": "Available",
"ffmpeg_not_available": "Not Available",
"error_getting_info": "Error getting system information: {error}"
},
"time_range": {
"title": "Trim Video",
"start_time": "Start Time (HH:MM:SS)",
"end_time": "End Time (HH:MM:SS)",
"start_placeholder": "00:00:00",
"end_placeholder": "00:00:00",
"help_text": "Set the start and end times to trim the video.\nLeave empty to download the full video.",
"invalid_format": "Invalid time format. Use HH:MM:SS format.",
"start_after_end": "Start time cannot be after end time."
},
"settings": {
"title": "Download Settings",
"download_path": "Download Path",
"browse": "Browse...",
"speed_limit": "Speed Limit",
"speed_limit_placeholder": "None",
"auto_update_ytdlp": "Auto-Update yt-dlp",
"enable_auto_updates": "Enable automatic yt-dlp updates",
"update_frequency": "Update frequency:",
"check_startup": "Check on every startup (minimum 1 hour between checks)",
"check_daily": "Check daily",
"check_weekly": "Check weekly",
"check_updates_now": "Check for Updates Now",
"update_check_title": "Update Check",
"could_not_determine_version": "Could not determine current yt-dlp version.",
"update_available_dialog": "Update available!\n\nCurrent: {current}\nLatest: {latest}\n\nUse the 'Update yt-dlp' button in the main window to update.",
"up_to_date_dialog": "yt-dlp is up to date!\n\nCurrent version: {version}",
"error_checking_updates": "Error checking for updates: {error}",
"settings_saved_title": "Settings Saved",
"settings_saved_message": "Auto-update settings have been saved successfully!",
"error_title": "Error",
"failed_save_settings": "Failed to save auto-update settings.",
"error_saving_settings": "Error saving auto-update settings: {error}",
"auto_update_title": "Auto-Update Settings",
"auto_update_header": "🔄 Auto-Update Settings",
"auto_update_description": "Configure automatic updates for yt-dlp to ensure you always have the latest features and bug fixes.",
"current_status": "Current Status",
"current_version_label": "Current yt-dlp version: Checking...",
"last_check_label": "Last update check: Never",
"next_check_label": "Next check: Based on settings",
"manual_check_button": "🔍 Check for Updates Now",
"update_frequency_group": "Update Frequency",
"save_settings": "Save Settings",
"settings_saved_successfully": "✅ Settings saved successfully!",
"error_saving": "❌ Error saving settings: {error}"
},
"main_ui": {
"url_placeholder": "Enter YouTube video or playlist URL",
"merge_subtitles": "Merge Subtitles",
"save_thumbnail": "Save Thumbnail",
"save_description": "Save Description",
"embed_chapters": "Embed Chapters",
"subtitles_selected": "{count} selected",
"all_selected": "All selected",
"select_videos_all": "Select Videos... (All selected)",
"please_enter_url": "Please enter a URL first",
"cookie_file_selected_title": "Cookie File Selected",
"cookie_file_selected_message": "Cookie file selected: {path}",
"browser_cookies_selected_title": "Browser Cookies Selected",
"browser_cookies_selected_message": "Browser cookies will be extracted from: {browser}",
"error_no_format_info": "Error: No format information available.",
"error_extract_info": "Error: Could not extract basic video information. Please check your link.",
"analyzing_preparing": "Analyzing (0%)... Preparing request",
"analyzing_extracting_basic": "Analyzing (15%)... Extracting basic info",
"analyzing_extracting_detailed": "Analyzing (30%)... Extracting detailed info",
"analyzing_processing_video": "Analyzing (45%)... Processing video data",
"analyzing_processing_formats": "Analyzing (60%)... Processing formats",
"analyzing_loading_thumbnail": "Analyzing (75%)... Loading thumbnail",
"analyzing_processing_subtitles": "Analyzing (85%)... Processing subtitles",
"analyzing_updating_table": "Analyzing (95%)... Updating format table",
"analysis_complete": "Analysis complete!",
"analyzing_extracting_ytdlp": "Analyzing (30%)... Extracting info with yt-dlp executable",
"analyzing_processing_data": "Analyzing (60%)... Processing data",
"analyzing_processing_formats_ytdlp": "Analyzing (75%)... Processing formats",
"analyzing_loading_thumbnail_ytdlp": "Analyzing (85%)... Loading thumbnail",
"analyzing_processing_subtitles_ytdlp": "Analyzing (90%)... Processing subtitles",
"select_subtitles": "Select Subtitles...",
"sponsorblock_categories": "SponsorBlock Categories..."
},
"sponsorblock": {
"sponsor": "Sponsor",
"sponsor_desc": "Paid promotion, paid referrals and direct advertisements",
"selfpromo": "Unpaid/Self Promotion",
"selfpromo_desc": "Unpaid promotion of creators' own content",
"interaction": "Interaction Reminder",
"interaction_desc": "Asking viewers to like, subscribe, or follow social media",
"intro": "Intro",
"intro_desc": "Video introduction that can be skipped",
"outro": "Outro/End Cards",
"outro_desc": "Credits or when the video ends",
"preview": "Preview/Recap",
"preview_desc": "Quick recap of previous videos or preview of what's coming up",
"music_offtopic": "Non-Music Section",
"music_offtopic_desc": "Only for music videos. Marks non-music sections",
"filler": "Filler Tangent",
"filler_desc": "Tangential scenes added only for filler or humor"
},
"video_info": {
"channel": "Channel",
"views": "Views",
"likes": "Likes",
"upload_date": "Upload date",
"duration": "Duration",
"unknown_channel": "Unknown channel",
"unknown_date": "Unknown date",
"unknown_title": "Unknown title"
},
"command": {
"running": "Running...",
"run_command": "Run Command"
},
"selection": {
"none_selected": "0 selected",
"one_selected": "1 category selected",
"count_selected": "{count} selected"
},
"status": {
"ready": "Ready",
"file_exists": "⚠️ File already exists",
"cancelling": "Cancelling download..."
}
}
+313
View File
@@ -0,0 +1,313 @@
{
"language": {
"display_name": "Español (Spanish)",
"select_language": "Seleccionar idioma:",
"current_language": "Idioma actual: {language}",
"restart_required": "Los cambios de idioma tendrán efecto después de reiniciar la aplicación.",
"english": "Inglés",
"spanish": "Español (Spanish)"
},
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Listo"
},
"buttons": {
"download": "Descargar",
"pause": "Pausar",
"resume": "Reanudar",
"cancel": "Cancelar",
"browse": "Examinar",
"clear": "Limpiar",
"ok": "Aceptar",
"apply": "Aplicar",
"close": "Cerrar",
"run_command": "Ejecutar Comando",
"about": "Acerca de",
"update": "Actualizar",
"analyze": "Analizar",
"paste_url": "Pegar URL",
"select_videos": "Seleccionar Videos...",
"video": "Video",
"audio_only": "Solo Audio",
"custom_options": "Opciones Personalizadas",
"trim_video": "Recortar Video",
"download_settings": "Configuración de Descarga",
"select_defaults": "Seleccionar Predeterminados",
"select_all": "Seleccionar Todos",
"deselect_all": "Deseleccionar Todos"
},
"dialogs": {
"custom_options": "Opciones Personalizadas",
"settings": "Configuración",
"select_folder": "Seleccionar Carpeta de Descarga",
"sponsorblock_categories": "Categorías SponsorBlock",
"sponsorblock_description": "Selecciona qué tipos de segmentos de video se eliminarán automáticamente durante la descarga.\nSponsorBlock utiliza datos enviados por la comunidad para identificar estos segmentos.",
"select_subtitles": "Seleccionar Subtítulos",
"filter_languages_placeholder": "Filtrar idiomas (ej., en, es)...",
"no_subtitles_available": "No hay subtítulos disponibles",
"matching": "que coincidan con"
},
"tabs": {
"cookies": "Iniciar sesión con Cookies",
"custom_command": "Comando Personalizado",
"proxy": "Proxy",
"language": "Idioma"
},
"cookies": {
"help_text": "Elige cómo proporcionar cookies para iniciar sesión.\nEsto permite la descarga de videos privados y audio de calidad premium.",
"cookie_source": "Origen de Cookie",
"use_cookie_file": "Usar archivo de cookie (formato Netscape)",
"extract_from_browser": "Extraer cookies del navegador",
"cookie_file": "Archivo de Cookie",
"cookie_file_placeholder": "Ruta al archivo de cookies (formato Netscape)",
"browser_selection": "Selección de Navegador",
"browser_help": "Selecciona el navegador del cual extraer cookies. Asegúrate de que el navegador esté cerrado antes de la extracción.",
"browser_label": "Navegador:",
"profile_label": "Perfil (opcional):",
"profile_placeholder": "Nombre o ruta del perfil (dejar vacío para el predeterminado)",
"browser_extract_message": "Las cookies del navegador se extraerán al aplicar",
"file_selected_message": "Archivo de cookies seleccionado - Haz clic en Aceptar para aplicar",
"select_file_title": "Seleccionar Archivo de Cookies",
"file_filter": "Archivos de cookies (*.txt *.lwp)"
},
"custom_command": {
"help_text": "Ingresa tu comando yt-dlp personalizado a continuación. La URL actual se añadirá automáticamente.<br><br>Para ver la lista completa de opciones y ejemplos de uso, <a href=\"{docs_url}\">haz clic aquí para ver la documentación oficial de yt-dlp</a>.<br><br>Nota: La ruta de descarga y la plantilla de nombre de archivo se manejarán automáticamente.",
"input_label": "Argumentos de yt-dlp:",
"input_placeholder": "Ingresa argumentos de yt-dlp aquí...\n\nej. --extract-audio --audio-format mp3",
"output_label": "Salida del Comando:",
"output_placeholder": "La salida del comando aparecerá aquí...",
"full_command": "🔧 Comando completo: {command}",
"command_failed": "❌ El comando falló con código de salida {code}",
"command_success": "✅ ¡Comando completado exitosamente!",
"command_error": "❌ Error ejecutando comando: {error}"
},
"proxy": {
"help_text": "Configurar ajustes de proxy para conexiones de red y geo-verificación.\nEl proxy puede ayudar a evitar restricciones regionales y mejorar el rendimiento de descarga.",
"main_proxy": "Proxy Principal",
"main_proxy_help": "Usar el proxy HTTP/HTTPS/SOCKS especificado para todas las conexiones.",
"proxy_url_label": "URL del Proxy:",
"proxy_url_placeholder": "ej., http://proxy.example.com:8080 o socks5://user:pass@127.0.0.1:1080",
"proxy_examples": "Ejemplos: http://proxy.com:8080, https://proxy.com:8080, socks5://127.0.0.1:1080",
"geo_proxy": "Proxy de Geo-verificación",
"geo_proxy_help": "Usar este proxy para verificar dirección IP para sitios con restricciones geográficas. El proxy principal (si está configurado) se usa para la descarga real.",
"geo_proxy_url_label": "URL del Geo Proxy:",
"geo_proxy_url_placeholder": "ej., http://us-proxy.example.com:8080",
"clear_main_proxy": "Limpiar Proxy Principal",
"clear_geo_proxy": "Limpiar Geo Proxy",
"invalid_main_url": "Formato de URL de proxy principal inválido",
"invalid_geo_url": "Formato de URL de geo proxy inválido",
"main_configured": "Proxy principal configurado",
"geo_configured": "Geo proxy configurado"
},
"download": {
"preparing": "🚀 Preparando tu descarga...",
"completed": "✅ ¡Descarga completada!",
"video_completed": "✅ ¡Descarga de video completada!",
"audio_completed": "✅ ¡Descarga de audio completada!",
"subtitle_completed": "✅ ¡Descarga de subtítulos completada!",
"please_set_path": "Por favor establece una ruta de descarga usando 'Cambiar Ruta'",
"please_enter_url": "Por favor ingresa una URL",
"please_enter_url_and_path": "Por favor ingresa URL y establece ruta de descarga",
"please_select_format": "Por favor selecciona un formato"
},
"formats": {
"show_formats": "Mostrar formatos:",
"select": "Seleccionar",
"quality": "Calidad",
"extension": "Extensión",
"resolution": "Resolución",
"file_size": "Tamaño",
"codec": "Códec",
"audio": "Audio",
"notes": "Notas",
"will_merge_audio": "Se combinará audio",
"has_audio": "✓ Tiene Audio",
"audio_only": "Solo Audio",
"large_size": "Tamaño grande",
"medium_size": "Tamaño mediano",
"standard_size": "Tamaño estándar",
"compatible": "Compatible",
"efficient": "Eficiente",
"high_quality": "Alta calidad",
"best_4k": "Óptima (4K)",
"best_2k": "Óptima (2K)",
"high_1080p": "Alta (1080p)",
"high_720p": "Alta (720p)",
"medium_480p": "Media (480p)",
"low_quality": "Baja Calidad",
"best_audio": "Mejor Audio",
"high_audio": "Audio Alto",
"medium_audio": "Audio Medio",
"low_audio": "Audio Bajo",
"audio_only_resolution": "Solo audio"
},
"about": {
"title": "Acerca de YTSage",
"version": "Versión {version}",
"description": "Descargador moderno de YouTube con una interfaz limpia de PySide6.",
"author": "Por: {author}",
"github": "GitHub: {repo}",
"system_info": "Información del Sistema",
"loading": "🔄 Cargando información del sistema...",
"refresh": "🔄",
"refreshing": "🔄 Actualizando...",
"refresh_failed": "Actualización Fallida",
"refresh_failed_message": "No se pudo actualizar la información de versión.",
"detected": "✓ Detectado",
"missing": "✗ Faltante",
"not_available": "No Disponible"
},
"time_range": {
"title": "Descargar Sección del Video",
"help_text": "Descarga solo partes específicas de un video especificando rangos de tiempo.\nUsa formato HH:MM:SS o segundos. Deja inicio o fin vacío para descargar desde el principio o hasta el final.",
"time_range_group": "Rango de Tiempo",
"start_time": "Tiempo de Inicio:",
"end_time": "Tiempo de Fin:",
"start_time_placeholder": "00:00:00 (o dejar vacío para inicio)",
"end_time_placeholder": "00:10:00 (o dejar vacío para fin)",
"force_keyframes": "Forzar fotogramas clave en cortes (mejor precisión, más lento)"
},
"update": {
"title": "Actualizar yt-dlp",
"checking": "Verificando actualizaciones...",
"update_available": "¡Actualización disponible!\nVersión actual: {current}\nÚltima versión: {latest}",
"up_to_date": "yt-dlp está actualizado (versión {version})",
"could_not_determine": "No se pudieron determinar las versiones.",
"error_comparing": "Error al comparar versiones: {error}",
"update_available_failed": "¡Actualización disponible! (Comparación fallida)\nActual: {current}\nÚltima: {latest}",
"updating": "Actualizando...",
"initializing": "🚀 Inicializando proceso de actualización...",
"checking_current": "🔍 Verificando instalación actual...",
"found_at": "📍 Se encontró yt-dlp en: {path}",
"error_getting_path": "❌ Error al obtener la ruta de yt-dlp: {error}",
"updating_binary": "📦 Actualizando binario de yt-dlp administrado por la app...",
"updating_pip": "🐍 Actualizando yt-dlp del sistema vía pip...",
"update_failed": "❌ Falló la actualización de yt-dlp. Inténtalo de nuevo o verifica tu conexión a internet.",
"binary_updated": "✅ ¡Binario actualizado exitosamente!",
"update_failed_stderr": "❌ Falló la actualización de yt-dlp: {error}",
"update_timeout": "❌ Tiempo agotado en la actualización de yt-dlp.",
"unexpected_error": "❌ Error inesperado durante la actualización: {error}",
"checking_pip": "🔍 Verificando instalación actual de pip...",
"current_version": "📋 Versión actual: {version}",
"not_found_pip": "⚠️ yt-dlp no encontrado vía pip, intentando instalación...",
"checking_latest": "🌐 Verificando última versión...",
"failed_check_updates": "❌ Falló la verificación de actualizaciones",
"latest_version": "🆕 Última versión: {version}",
"updating_from_to": "⬆️ Actualizando de {current} a {latest}...",
"running_pip_install": "📦 Ejecutando pip install --upgrade...",
"pip_completed": "✅ ¡Actualización de pip completada exitosamente!",
"pip_failed": "❌ Actualización de pip falló: {error}"
},
"settings": {
"title": "Configuración de Descarga",
"download_path": "Ruta de Descarga",
"browse": "Examinar...",
"speed_limit": "Límite de Velocidad",
"speed_limit_placeholder": "Ninguno",
"auto_update_ytdlp": "Auto-Actualizar yt-dlp",
"enable_auto_updates": "Habilitar actualizaciones automáticas de yt-dlp",
"update_frequency": "Frecuencia de actualización:",
"check_startup": "Verificar en cada inicio (mínimo 1 hora entre verificaciones)",
"check_daily": "Verificar diariamente",
"check_weekly": "Verificar semanalmente",
"check_updates_now": "Verificar Actualizaciones Ahora",
"update_check_title": "Verificación de Actualizaciones",
"could_not_determine_version": "No se pudo determinar la versión actual de yt-dlp.",
"update_available_dialog": "¡Actualización disponible!\n\nActual: {current}\nÚltima: {latest}\n\nUsa el botón 'Actualizar yt-dlp' en la ventana principal para actualizar.",
"up_to_date_dialog": "¡yt-dlp está actualizado!\n\nVersión actual: {version}",
"error_checking_updates": "Error al verificar actualizaciones: {error}",
"settings_saved_title": "Configuración Guardada",
"settings_saved_message": "¡La configuración de auto-actualización se ha guardado exitosamente!",
"error_title": "Error",
"failed_save_settings": "Falló al guardar la configuración de auto-actualización.",
"error_saving_settings": "Error al guardar la configuración de auto-actualización: {error}",
"auto_update_title": "Configuración de Auto-Actualización",
"auto_update_header": "🔄 Configuración de Auto-Actualización",
"auto_update_description": "Configura las actualizaciones automáticas de yt-dlp para asegurar que siempre tengas las últimas funciones y correcciones de errores.",
"current_status": "Estado Actual",
"current_version_label": "Versión actual de yt-dlp: Verificando...",
"last_check_label": "Última verificación de actualización: Nunca",
"next_check_label": "Próxima verificación: Basado en configuración",
"manual_check_button": "🔍 Verificar Actualizaciones Ahora",
"update_frequency_group": "Frecuencia de Actualización",
"save_settings": "Guardar Configuración",
"settings_saved_successfully": "✅ ¡Configuración guardada exitosamente!",
"error_saving": "❌ Error al guardar configuración: {error}"
},
"main_ui": {
"url_placeholder": "Ingresa URL de video o lista de YouTube",
"merge_subtitles": "Combinar Subtítulos",
"save_thumbnail": "Guardar Miniatura",
"save_description": "Guardar Descripción",
"embed_chapters": "Incrustar Capítulos",
"subtitles_selected": "{count} seleccionados",
"all_selected": "Todos seleccionados",
"select_videos_all": "Seleccionar Videos... (Todos seleccionados)",
"please_enter_url": "Por favor ingresa primero una URL",
"cookie_file_selected_title": "Archivo de Cookies Seleccionado",
"cookie_file_selected_message": "Archivo de cookies seleccionado: {path}",
"browser_cookies_selected_title": "Cookies de Navegador Seleccionadas",
"browser_cookies_selected_message": "Las cookies del navegador serán extraídas de: {browser}",
"error_no_format_info": "Error: No hay información de formato disponible.",
"error_extract_info": "Error: No se pudo extraer la información básica del video. Por favor verifica tu enlace.",
"analyzing_preparing": "Analizando (0%)... Preparando solicitud",
"analyzing_extracting_basic": "Analizando (15%)... Extrayendo información básica",
"analyzing_extracting_detailed": "Analizando (30%)... Extrayendo información detallada",
"analyzing_processing_video": "Analizando (45%)... Procesando datos de video",
"analyzing_processing_formats": "Analizando (60%)... Procesando formatos",
"analyzing_loading_thumbnail": "Analizando (75%)... Cargando miniatura",
"analyzing_processing_subtitles": "Analizando (85%)... Procesando subtítulos",
"analyzing_updating_table": "Analizando (95%)... Actualizando tabla de formatos",
"analysis_complete": "¡Análisis completo!",
"analyzing_extracting_ytdlp": "Analizando (30%)... Extrayendo información con ejecutable yt-dlp",
"analyzing_processing_data": "Analizando (60%)... Procesando datos",
"analyzing_processing_formats_ytdlp": "Analizando (75%)... Procesando formatos",
"analyzing_loading_thumbnail_ytdlp": "Analizando (85%)... Cargando miniatura",
"analyzing_processing_subtitles_ytdlp": "Analizando (90%)... Procesando subtítulos",
"select_subtitles": "Seleccionar Subtítulos...",
"sponsorblock_categories": "Categorías SponsorBlock..."
},
"sponsorblock": {
"sponsor": "Patrocinador",
"sponsor_desc": "Promoción pagada, referencias pagadas y anuncios directos",
"selfpromo": "Autopromoción No Pagada",
"selfpromo_desc": "Promoción no pagada del propio contenido del creador",
"interaction": "Recordatorio de Interacción",
"interaction_desc": "Pedir a los espectadores que den like, se suscriban o sigan en redes sociales",
"intro": "Introducción",
"intro_desc": "Introducción del video que se puede omitir",
"outro": "Outro/Tarjetas Finales",
"outro_desc": "Créditos o cuando termina el video",
"preview": "Vista Previa/Resumen",
"preview_desc": "Resumen rápido de videos anteriores o vista previa de lo que viene",
"music_offtopic": "Sección No Musical",
"music_offtopic_desc": "Solo para videos musicales. Marca secciones no musicales",
"filler": "Relleno/Tangente",
"filler_desc": "Escenas tangenciales agregadas solo como relleno o humor"
},
"video_info": {
"channel": "Canal",
"views": "Visualizaciones",
"likes": "Me gusta",
"upload_date": "Fecha de subida",
"duration": "Duración",
"unknown_channel": "Canal desconocido",
"unknown_date": "Fecha desconocida",
"unknown_title": "Título desconocido"
},
"command": {
"running": "Ejecutando...",
"run_command": "Ejecutar Comando"
},
"selection": {
"none_selected": "0 seleccionados",
"one_selected": "1 categoría seleccionada",
"count_selected": "{count} seleccionados"
},
"status": {
"ready": "Listo",
"file_exists": "⚠️ El archivo ya existe",
"cancelling": "Cancelando descarga..."
}
}
@@ -19,6 +19,8 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from src.utils.ytsage_localization import _
from src.core.ytsage_ffmpeg import get_ffmpeg_path 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_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 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: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self._parent = parent # Store parent to access version etc. 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.setMinimumSize(460, 420) # Slightly increased to accommodate paths
self.resize(460, 440) # Slightly increased initial size self.resize(460, 440) # Slightly increased initial size
self.setMaximumSize(500, 480) # Reasonable maximum size self.setMaximumSize(500, 480) # Reasonable maximum size
@@ -157,13 +159,13 @@ class AboutDialog(QDialog):
layout.addWidget(title_label) layout.addWidget(title_label)
version_label = QLabel( 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) version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(version_label) layout.addWidget(version_label)
# Description - more compact # 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.setWordWrap(True)
description_label.setAlignment(Qt.AlignmentFlag.AlignCenter) description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
description_label.setStyleSheet("color: #ffffff; font-size: 11px; margin: 6px 0;") description_label.setStyleSheet("color: #ffffff; font-size: 11px; margin: 6px 0;")
@@ -174,13 +176,13 @@ class AboutDialog(QDialog):
info_layout.setSpacing(15) info_layout.setSpacing(15)
author_label = QLabel( 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) author_label.setOpenExternalLinks(True)
info_layout.addWidget(author_label) info_layout.addWidget(author_label)
repo_label = QLabel( 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) repo_label.setOpenExternalLinks(True)
info_layout.addWidget(repo_label) info_layout.addWidget(repo_label)
@@ -219,7 +221,7 @@ class AboutDialog(QDialog):
header_layout.setContentsMargins(0, 0, 0, 5) header_layout.setContentsMargins(0, 0, 0, 5)
# System Information title # System Information title
title_label = QLabel("System Information") title_label = QLabel(_("about.system_info"))
title_label.setStyleSheet( title_label.setStyleSheet(
""" """
QLabel { QLabel {
@@ -237,7 +239,7 @@ class AboutDialog(QDialog):
header_layout.addStretch() header_layout.addStretch()
# Create refresh button # Create refresh button
self.refresh_btn = QPushButton("🔄") self.refresh_btn = QPushButton(_("about.refresh"))
self.refresh_btn.setFixedSize(16, 16) self.refresh_btn.setFixedSize(16, 16)
self.refresh_btn.setStyleSheet( self.refresh_btn.setStyleSheet(
""" """
@@ -283,7 +285,7 @@ class AboutDialog(QDialog):
def _show_loading_message(self) -> None: def _show_loading_message(self) -> None:
"""Show a compact loading message while system information is being gathered.""" """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.setAlignment(Qt.AlignmentFlag.AlignCenter)
loading_label.setStyleSheet( loading_label.setStyleSheet(
""" """
@@ -381,7 +383,7 @@ class AboutDialog(QDialog):
# yt-dlp Status - compact version with path # yt-dlp Status - compact version with path
ytdlp_found = check_ytdlp_installed() ytdlp_found = check_ytdlp_installed()
ytdlp_status_text = ( 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() ytdlp_version = get_ytdlp_version()
@@ -409,11 +411,11 @@ class AboutDialog(QDialog):
# FFmpeg Status - compact version with path # FFmpeg Status - compact version with path
ffmpeg_found = check_ffmpeg() ffmpeg_found = check_ffmpeg()
ffmpeg_status_text = ( ffmpeg_status_text = (
"<span style='color: #4CAF50;'>✓ Detected</span>" f"<span style='color: #4CAF50;'>{_('about.detected')}</span>"
if ffmpeg_found 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 # Get FFmpeg path
ffmpeg_path_text = None ffmpeg_path_text = None
@@ -440,7 +442,7 @@ class AboutDialog(QDialog):
def refresh_version_info(self) -> None: def refresh_version_info(self) -> None:
"""Refresh version information manually.""" """Refresh version information manually."""
self.refresh_btn.setText("🔄 Refreshing...") self.refresh_btn.setText(_('about.refreshing'))
self.refresh_btn.setEnabled(False) self.refresh_btn.setEnabled(False)
# Perform refresh in a separate thread to avoid blocking UI # Perform refresh in a separate thread to avoid blocking UI
@@ -457,7 +459,7 @@ class AboutDialog(QDialog):
def on_refresh_finished(self, success) -> None: def on_refresh_finished(self, success) -> None:
"""Handle refresh completion.""" """Handle refresh completion."""
self.refresh_btn.setText("🔄 Refresh") self.refresh_btn.setText(_('about.refresh'))
self.refresh_btn.setEnabled(True) self.refresh_btn.setEnabled(True)
if success: if success:
@@ -466,8 +468,8 @@ class AboutDialog(QDialog):
# Show error message with proper styling # Show error message with proper styling
msg_box = QMessageBox(self) msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Icon.Warning) msg_box.setIcon(QMessageBox.Icon.Warning)
msg_box.setWindowTitle("Refresh Failed") msg_box.setWindowTitle(_('about.refresh_failed'))
msg_box.setText("Failed to refresh version information.") msg_box.setText(_('about.refresh_failed_message'))
msg_box.setWindowIcon(self.windowIcon()) msg_box.setWindowIcon(self.windowIcon())
msg_box.setStyleSheet( msg_box.setStyleSheet(
""" """
@@ -31,6 +31,8 @@ from PySide6.QtWidgets import (
from src.core.ytsage_yt_dlp import get_yt_dlp_path from src.core.ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import YTDLP_DOCS_URL from src.utils.ytsage_constants import YTDLP_DOCS_URL
from src.utils.ytsage_config_manager import ConfigManager 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: if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import) 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) base_cmd.append(self.url)
# Emit the full command # 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) self.output_received.emit("=" * 50)
# Run the command # Run the command
@@ -90,22 +92,22 @@ class CommandWorker(QObject):
self.output_received.emit("=" * 50) self.output_received.emit("=" * 50)
if ret != 0: 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) self.command_finished.emit(False, ret)
else: else:
self.output_received.emit("✅ Command completed successfully!") self.output_received.emit(_('custom_command.command_success'))
self.command_finished.emit(True, ret) self.command_finished.emit(True, ret)
except Exception as e: except Exception as e:
self.output_received.emit("=" * 50) 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): class CustomOptionsDialog(QDialog):
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self._parent: YTSageApp = cast("YTSageApp", self.parent()) # cast will help with auto complete and type hint checking. 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 self.setMinimumSize(550, 400) # Made even shorter
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
@@ -118,38 +120,35 @@ class CustomOptionsDialog(QDialog):
cookies_layout = QVBoxLayout(cookies_tab) cookies_layout = QVBoxLayout(cookies_tab)
# Help text # Help text
help_text = QLabel( help_text = QLabel(_('cookies.help_text'))
"Choose how to provide cookies for logging in.\n"
"This allows downloading of private videos and premium quality audio."
)
help_text.setWordWrap(True) help_text.setWordWrap(True)
help_text.setStyleSheet("color: #999999; padding: 10px;") help_text.setStyleSheet("color: #999999; padding: 10px;")
cookies_layout.addWidget(help_text) cookies_layout.addWidget(help_text)
# Cookie source selection # Cookie source selection
cookie_source_group = QGroupBox("Cookie Source") cookie_source_group = QGroupBox(_('cookies.cookie_source'))
cookie_source_layout = QVBoxLayout(cookie_source_group) cookie_source_layout = QVBoxLayout(cookie_source_group)
# Radio buttons for cookie source # 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.setChecked(True)
self.cookie_file_radio.toggled.connect(self.on_cookie_source_changed) self.cookie_file_radio.toggled.connect(self.on_cookie_source_changed)
cookie_source_layout.addWidget(self.cookie_file_radio) 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) self.cookie_browser_radio.toggled.connect(self.on_cookie_source_changed)
cookie_source_layout.addWidget(self.cookie_browser_radio) cookie_source_layout.addWidget(self.cookie_browser_radio)
cookies_layout.addWidget(cookie_source_group) cookies_layout.addWidget(cookie_source_group)
# Cookie file section # 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_layout = QVBoxLayout(self.cookie_file_group)
# File path input and browse button # File path input and browse button
path_layout = QHBoxLayout() path_layout = QHBoxLayout()
self.cookie_path_input = QLineEdit() 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: if hasattr(self._parent, "cookie_file_path") and self._parent.cookie_file_path:
# Convert Path to string properly and validate # Convert Path to string properly and validate
cookie_path_str = str(self._parent.cookie_file_path) cookie_path_str = str(self._parent.cookie_file_path)
@@ -158,7 +157,7 @@ class CustomOptionsDialog(QDialog):
self.cookie_path_input.setText(cookie_path_str) self.cookie_path_input.setText(cookie_path_str)
path_layout.addWidget(self.cookie_path_input) 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) self.browse_button.clicked.connect(self.browse_cookie_file)
path_layout.addWidget(self.browse_button) path_layout.addWidget(self.browse_button)
file_layout.addLayout(path_layout) file_layout.addLayout(path_layout)
@@ -166,16 +165,16 @@ class CustomOptionsDialog(QDialog):
cookies_layout.addWidget(self.cookie_file_group) cookies_layout.addWidget(self.cookie_file_group)
# Browser selection section # 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_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.setWordWrap(True)
browser_help.setStyleSheet("color: #999999; font-size: 11px;") browser_help.setStyleSheet("color: #999999; font-size: 11px;")
browser_layout.addWidget(browser_help) browser_layout.addWidget(browser_help)
browser_select_layout = QHBoxLayout() 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 = QComboBox()
self.browser_combo.addItems(["chrome", "firefox", "safari", "edge", "opera", "brave", "chromium", "vivaldi"]) self.browser_combo.addItems(["chrome", "firefox", "safari", "edge", "opera", "brave", "chromium", "vivaldi"])
@@ -184,9 +183,9 @@ class CustomOptionsDialog(QDialog):
# Optional profile field # Optional profile field
profile_layout = QHBoxLayout() profile_layout = QHBoxLayout()
profile_layout.addWidget(QLabel("Profile (optional):")) profile_layout.addWidget(QLabel(_('cookies.profile_label')))
self.profile_input = QLineEdit() 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) profile_layout.addWidget(self.profile_input)
browser_layout.addLayout(profile_layout) browser_layout.addLayout(profile_layout)
@@ -207,12 +206,7 @@ class CustomOptionsDialog(QDialog):
command_layout = QVBoxLayout(command_tab) command_layout = QVBoxLayout(command_tab)
# Improved help text # Improved help text
cmd_help_text = QLabel( cmd_help_text = QLabel(_('custom_command.help_text', docs_url=YTDLP_DOCS_URL))
"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.setWordWrap(True) cmd_help_text.setWordWrap(True)
cmd_help_text.setOpenExternalLinks(True) # Enable clicking links cmd_help_text.setOpenExternalLinks(True) # Enable clicking links
cmd_help_text.setTextFormat(Qt.TextFormat.RichText) # Enable HTML rendering cmd_help_text.setTextFormat(Qt.TextFormat.RichText) # Enable HTML rendering
@@ -238,13 +232,13 @@ class CustomOptionsDialog(QDialog):
command_layout.addWidget(cmd_help_text) command_layout.addWidget(cmd_help_text)
# Command input label # 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;") input_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 10px;")
command_layout.addWidget(input_label) command_layout.addWidget(input_label)
# Command input # Command input
self.command_input = QPlainTextEdit() 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.setMinimumHeight(80) # Reduced further from 100
self.command_input.setStyleSheet( self.command_input.setStyleSheet(
""" """
@@ -269,7 +263,7 @@ class CustomOptionsDialog(QDialog):
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
button_layout.setSpacing(10) button_layout.setSpacing(10)
clear_btn = QPushButton("Clear") clear_btn = QPushButton(_('buttons.clear'))
clear_btn.clicked.connect(lambda: self.command_input.clear()) clear_btn.clicked.connect(lambda: self.command_input.clear())
clear_btn.setStyleSheet( clear_btn.setStyleSheet(
""" """
@@ -292,7 +286,7 @@ class CustomOptionsDialog(QDialog):
button_layout.addStretch() # Push run button to the right button_layout.addStretch() # Push run button to the right
# Run command button # 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.clicked.connect(self.run_custom_command)
self.run_btn.setDefault(True) self.run_btn.setDefault(True)
button_layout.addWidget(self.run_btn) button_layout.addWidget(self.run_btn)
@@ -300,14 +294,14 @@ class CustomOptionsDialog(QDialog):
command_layout.addLayout(button_layout) command_layout.addLayout(button_layout)
# Output label # 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;") output_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 15px;")
command_layout.addWidget(output_label) command_layout.addWidget(output_label)
# Log output # Log output
self.log_output = QTextEdit() self.log_output = QTextEdit()
self.log_output.setReadOnly(True) 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.setMinimumHeight(100) # Reduced further from 120
self.log_output.setStyleSheet( self.log_output.setStyleSheet(
""" """
@@ -332,56 +326,50 @@ class CustomOptionsDialog(QDialog):
proxy_layout = QVBoxLayout(proxy_tab) proxy_layout = QVBoxLayout(proxy_tab)
# Help text # Help text
proxy_help_text = QLabel( proxy_help_text = QLabel(_('proxy.help_text'))
"Configure proxy settings for network connections and geo-verification.\n"
"Proxy can help bypass regional restrictions and improve download performance."
)
proxy_help_text.setWordWrap(True) proxy_help_text.setWordWrap(True)
proxy_help_text.setStyleSheet("color: #999999; padding: 10px;") proxy_help_text.setStyleSheet("color: #999999; padding: 10px;")
proxy_layout.addWidget(proxy_help_text) proxy_layout.addWidget(proxy_help_text)
# Main Proxy section # 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_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.setWordWrap(True)
main_proxy_help.setStyleSheet("color: #999999; font-size: 11px;") main_proxy_help.setStyleSheet("color: #999999; font-size: 11px;")
main_proxy_layout.addWidget(main_proxy_help) main_proxy_layout.addWidget(main_proxy_help)
# Main proxy input # Main proxy input
main_proxy_input_layout = QHBoxLayout() 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 = 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) self.proxy_url_input.textChanged.connect(self.validate_proxy_inputs)
main_proxy_input_layout.addWidget(self.proxy_url_input) main_proxy_input_layout.addWidget(self.proxy_url_input)
main_proxy_layout.addLayout(main_proxy_input_layout) main_proxy_layout.addLayout(main_proxy_input_layout)
# Example text # 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;") example_label.setStyleSheet("color: #888888; font-size: 10px; font-style: italic;")
main_proxy_layout.addWidget(example_label) main_proxy_layout.addWidget(example_label)
proxy_layout.addWidget(main_proxy_group) proxy_layout.addWidget(main_proxy_group)
# Geo-verification Proxy section # 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_layout = QVBoxLayout(geo_proxy_group)
geo_proxy_help = QLabel( geo_proxy_help = QLabel(_('proxy.geo_proxy_help'))
"Use this proxy to verify IP address for geo-restricted sites. "
"The main proxy (if set) is used for actual downloading."
)
geo_proxy_help.setWordWrap(True) geo_proxy_help.setWordWrap(True)
geo_proxy_help.setStyleSheet("color: #999999; font-size: 11px;") geo_proxy_help.setStyleSheet("color: #999999; font-size: 11px;")
geo_proxy_layout.addWidget(geo_proxy_help) geo_proxy_layout.addWidget(geo_proxy_help)
# Geo proxy input # Geo proxy input
geo_proxy_input_layout = QHBoxLayout() 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 = 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) self.geo_proxy_url_input.textChanged.connect(self.validate_proxy_inputs)
geo_proxy_input_layout.addWidget(self.geo_proxy_url_input) geo_proxy_input_layout.addWidget(self.geo_proxy_url_input)
geo_proxy_layout.addLayout(geo_proxy_input_layout) geo_proxy_layout.addLayout(geo_proxy_input_layout)
@@ -395,7 +383,7 @@ class CustomOptionsDialog(QDialog):
# Clear buttons # Clear buttons
clear_layout = QHBoxLayout() 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.clicked.connect(lambda: self.proxy_url_input.clear())
clear_main_proxy_btn.setStyleSheet( clear_main_proxy_btn.setStyleSheet(
""" """
@@ -414,7 +402,7 @@ class CustomOptionsDialog(QDialog):
) )
clear_layout.addWidget(clear_main_proxy_btn) 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.clicked.connect(lambda: self.geo_proxy_url_input.clear())
clear_geo_proxy_btn.setStyleSheet( clear_geo_proxy_btn.setStyleSheet(
""" """
@@ -438,13 +426,75 @@ class CustomOptionsDialog(QDialog):
proxy_layout.addStretch() 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 # Add tabs to the tab widget
self.tab_widget.addTab(cookies_tab, "Login with Cookies") self.tab_widget.addTab(cookies_tab, _("tabs.cookies"))
self.tab_widget.addTab(command_tab, "Custom Command") self.tab_widget.addTab(command_tab, _("tabs.custom_command"))
self.tab_widget.addTab(proxy_tab, "Proxy") self.tab_widget.addTab(proxy_tab, _("tabs.proxy"))
self.tab_widget.addTab(language_tab, _("tabs.language"))
# Dialog buttons # 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.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
layout.addWidget(button_box) layout.addWidget(button_box)
@@ -524,7 +574,6 @@ class CustomOptionsDialog(QDialog):
border: none; border: none;
width: 12px; width: 12px;
height: 12px; height: 12px;
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgNEw2IDdMOSA0IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);
} }
QComboBox QAbstractItemView { QComboBox QAbstractItemView {
background-color: #1d1e22; background-color: #1d1e22;
@@ -610,18 +659,18 @@ class CustomOptionsDialog(QDialog):
else: else:
self.cookie_file_group.setVisible(False) self.cookie_file_group.setVisible(False)
self.cookie_browser_group.setVisible(True) 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;") self.cookie_status.setStyleSheet("color: #ffaa00; font-style: italic;")
def browse_cookie_file(self) -> None: def browse_cookie_file(self) -> None:
# Open file dialog to select cookie file # 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: if selected_files:
# Ensure we have a valid full path # Ensure we have a valid full path
cookie_path = Path(selected_files).resolve() cookie_path = Path(selected_files).resolve()
self.cookie_path_input.setText(str(cookie_path)) 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;") self.cookie_status.setStyleSheet("color: #00cc00; font-style: italic;")
def get_cookie_file_path(self) -> Path | None: 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(f"📁 Download path: {path}")
self.log_output.append("=" * 50) self.log_output.append("=" * 50)
self.run_btn.setEnabled(False) self.run_btn.setEnabled(False)
self.run_btn.setText("Running...") self.run_btn.setText(_("command.running"))
# Create worker and thread # Create worker and thread
self.worker = CommandWorker(command, url, path) self.worker = CommandWorker(command, url, path)
@@ -781,49 +830,63 @@ class CustomOptionsDialog(QDialog):
def on_command_finished(self, success: bool, exit_code: int): def on_command_finished(self, success: bool, exit_code: int):
"""Slot for when command finishes""" """Slot for when command finishes"""
self.run_btn.setEnabled(True) 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): def on_error_occurred(self, error_msg: str):
"""Slot for handling errors""" """Slot for handling errors"""
self.log_output.append(error_msg) self.log_output.append(error_msg)
self.run_btn.setEnabled(True) 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): class TimeRangeDialog(QDialog):
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Download Video Section") self.setWindowTitle(_('time_range.title'))
self.setMinimumWidth(400) self.setMinimumWidth(400)
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Help text explaining the feature # Help text explaining the feature
help_text = QLabel( help_text = QLabel(_('time_range.help_text'))
"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.setWordWrap(True) help_text.setWordWrap(True)
help_text.setStyleSheet("color: #999999; padding: 10px;") help_text.setStyleSheet("color: #999999; padding: 10px;")
layout.addWidget(help_text) layout.addWidget(help_text)
# Time range section # Time range section
time_group = QGroupBox("Time Range") time_group = QGroupBox(_('time_range.time_range_group'))
time_layout = QVBoxLayout() time_layout = QVBoxLayout()
# Start time row # Start time row
start_layout = QHBoxLayout() 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 = 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) start_layout.addWidget(self.start_time_input)
time_layout.addLayout(start_layout) time_layout.addLayout(start_layout)
# End time row # End time row
end_layout = QHBoxLayout() 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 = 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) end_layout.addWidget(self.end_time_input)
time_layout.addLayout(end_layout) time_layout.addLayout(end_layout)
@@ -831,7 +894,7 @@ class TimeRangeDialog(QDialog):
layout.addWidget(time_group) layout.addWidget(time_group)
# Force keyframes option # 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.setChecked(True)
self.force_keyframes.setStyleSheet( self.force_keyframes.setStyleSheet(
""" """
@@ -859,7 +922,9 @@ class TimeRangeDialog(QDialog):
layout.addWidget(self.force_keyframes) layout.addWidget(self.force_keyframes)
# Buttons # 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.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
layout.addWidget(button_box) layout.addWidget(button_box)
@@ -17,11 +17,13 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from src.utils.ytsage_localization import _
class SubtitleSelectionDialog(QDialog): class SubtitleSelectionDialog(QDialog):
def __init__(self, available_manual, available_auto, previously_selected, parent=None) -> None: def __init__(self, available_manual, available_auto, previously_selected, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Select Subtitles") self.setWindowTitle(_("dialogs.select_subtitles"))
self.setMinimumWidth(400) self.setMinimumWidth(400)
self.setMinimumHeight(300) self.setMinimumHeight(300)
@@ -35,7 +37,7 @@ class SubtitleSelectionDialog(QDialog):
# Filter input # Filter input
self.filter_input = QLineEdit() 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.textChanged.connect(self.filter_list)
self.filter_input.setStyleSheet( self.filter_input.setStyleSheet(
""" """
@@ -72,7 +74,9 @@ class SubtitleSelectionDialog(QDialog):
self.populate_list() self.populate_list()
# OK and Cancel buttons # 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.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
@@ -128,7 +132,8 @@ class SubtitleSelectionDialog(QDialog):
combined_subs[lang_code] = f"{lang_code} - Auto-generated" combined_subs[lang_code] = f"{lang_code} - Auto-generated"
if not combined_subs: 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;") no_subs_label.setStyleSheet("color: #aaaaaa; padding: 10px;")
self.list_layout.addWidget(no_subs_label) self.list_layout.addWidget(no_subs_label)
return return
@@ -205,8 +210,8 @@ class PlaylistSelectionDialog(QDialog):
# Top buttons (Select/Deselect All) # Top buttons (Select/Deselect All)
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
select_all_btn = QPushButton("Select All") select_all_btn = QPushButton(_("buttons.select_all"))
deselect_all_btn = QPushButton("Deselect All") deselect_all_btn = QPushButton(_("buttons.deselect_all"))
select_all_btn.clicked.connect(self._select_all) select_all_btn.clicked.connect(self._select_all)
deselect_all_btn.clicked.connect(self._deselect_all) deselect_all_btn.clicked.connect(self._deselect_all)
# Style the buttons to match the subtitle dialog # Style the buttons to match the subtitle dialog
@@ -250,7 +255,9 @@ class PlaylistSelectionDialog(QDialog):
self._populate_list(previously_selected_string) self._populate_list(previously_selected_string)
# Dialog buttons (OK/Cancel) # 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.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
@@ -428,50 +435,50 @@ class SponsorBlockCategoryDialog(QDialog):
# Default SponsorBlock categories with descriptions # Default SponsorBlock categories with descriptions
SPONSORBLOCK_CATEGORIES = { SPONSORBLOCK_CATEGORIES = {
"sponsor": { "sponsor": {
"name": "Sponsor", "name_key": "sponsorblock.sponsor",
"description": "Paid promotion, paid referrals and direct advertisements", "description_key": "sponsorblock.sponsor_desc",
"default": True, "default": True,
}, },
"selfpromo": { "selfpromo": {
"name": "Unpaid/Self Promotion", "name_key": "sponsorblock.selfpromo",
"description": "Unpaid promotion of creators' own content", "description_key": "sponsorblock.selfpromo_desc",
"default": True, "default": True,
}, },
"interaction": { "interaction": {
"name": "Interaction Reminder", "name_key": "sponsorblock.interaction",
"description": "Asking viewers to like, subscribe, or follow social media", "description_key": "sponsorblock.interaction_desc",
"default": True, "default": True,
}, },
"intro": { "intro": {
"name": "Intro", "name_key": "sponsorblock.intro",
"description": "Video introduction that can be skipped", "description_key": "sponsorblock.intro_desc",
"default": False, "default": False,
}, },
"outro": { "outro": {
"name": "Outro/End Cards", "name_key": "sponsorblock.outro",
"description": "Credits or when the video ends", "description_key": "sponsorblock.outro_desc",
"default": False, "default": False,
}, },
"preview": { "preview": {
"name": "Preview/Recap", "name_key": "sponsorblock.preview",
"description": "Quick recap of previous videos or preview of what's coming up", "description_key": "sponsorblock.preview_desc",
"default": False, "default": False,
}, },
"music_offtopic": { "music_offtopic": {
"name": "Non-Music Section", "name_key": "sponsorblock.music_offtopic",
"description": "Only for music videos. Marks non-music sections", "description_key": "sponsorblock.music_offtopic_desc",
"default": False, "default": False,
}, },
"filler": { "filler": {
"name": "Filler Tangent", "name_key": "sponsorblock.filler",
"description": "Tangential scenes added only for filler or humor", "description_key": "sponsorblock.filler_desc",
"default": False, "default": False,
}, },
} }
def __init__(self, previously_selected=None, parent=None) -> None: def __init__(self, previously_selected=None, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("SponsorBlock Categories") self.setWindowTitle(_("dialogs.sponsorblock_categories"))
self.setMinimumWidth(500) self.setMinimumWidth(500)
self.setMinimumHeight(400) self.setMinimumHeight(400)
@@ -489,15 +496,12 @@ class SponsorBlockCategoryDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Title and description # Title and description
title_label = QLabel("SponsorBlock Categories") title_label = QLabel(_("dialogs.sponsorblock_categories"))
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; margin: 10px;") title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; margin: 10px;")
layout.addWidget(title_label) layout.addWidget(title_label)
desc_label = QLabel( desc_label = QLabel(_("dialogs.sponsorblock_description"))
"Select which types of video segments to automatically remove during download.\n"
"SponsorBlock uses community-submitted data to identify these segments."
)
desc_label.setWordWrap(True) desc_label.setWordWrap(True)
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter) desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;") 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.setContentsMargins(0, 0, 0, 0)
category_layout.setSpacing(2) category_layout.setSpacing(2)
# Create checkbox with just the name # Create checkbox with localized name
checkbox = QCheckBox(category_info["name"]) checkbox = QCheckBox(_(category_info["name_key"]))
checkbox.setProperty("category_id", category_id) checkbox.setProperty("category_id", category_id)
# Determine if this category should be checked # Determine if this category should be checked
@@ -559,8 +563,8 @@ class SponsorBlockCategoryDialog(QDialog):
""" """
) )
# Create description label # Create description label with localized text
desc_label = QLabel(category_info["description"]) desc_label = QLabel(_(category_info["description_key"]))
desc_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-left: 28px; margin-bottom: 8px;") desc_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-left: 28px; margin-bottom: 8px;")
desc_label.setWordWrap(True) desc_label.setWordWrap(True)
@@ -577,15 +581,15 @@ class SponsorBlockCategoryDialog(QDialog):
# Quick selection buttons # Quick selection buttons
button_layout = QHBoxLayout() 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.clicked.connect(self.select_defaults)
select_defaults_btn.setStyleSheet(self._get_button_style()) 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.clicked.connect(self.select_all)
select_all_btn.setStyleSheet(self._get_button_style()) 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.clicked.connect(self.deselect_all)
deselect_all_btn.setStyleSheet(self._get_button_style()) deselect_all_btn.setStyleSheet(self._get_button_style())
@@ -597,7 +601,9 @@ class SponsorBlockCategoryDialog(QDialog):
layout.addLayout(button_layout) layout.addLayout(button_layout)
# Dialog buttons # 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.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
@@ -34,12 +34,13 @@ from src.core.ytsage_utils import (
update_auto_update_settings, update_auto_update_settings,
) )
from src.utils.ytsage_logger import logger from src.utils.ytsage_logger import logger
from src.utils.ytsage_localization import _
class DownloadSettingsDialog(QDialog): class DownloadSettingsDialog(QDialog):
def __init__(self, current_path, current_limit, current_unit_index, parent=None) -> None: def __init__(self, current_path, current_limit, current_unit_index, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Download Settings") self.setWindowTitle(_("settings.title"))
self.setMinimumWidth(450) self.setMinimumWidth(450)
self.setMinimumHeight(400) self.setMinimumHeight(400)
self.current_path = current_path self.current_path = current_path
@@ -163,7 +164,7 @@ class DownloadSettingsDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# --- Download Path Section --- # --- Download Path Section ---
path_group_box = QGroupBox("Download Path") path_group_box = QGroupBox(_("settings.download_path"))
path_layout = QVBoxLayout() path_layout = QVBoxLayout()
self.path_display = QLabel(str(self.current_path)) self.path_display = QLabel(str(self.current_path))
@@ -173,7 +174,8 @@ class DownloadSettingsDialog(QDialog):
) )
path_layout.addWidget(self.path_display) path_layout.addWidget(self.path_display)
browse_button = QPushButton("Browse...")
browse_button = QPushButton(_("settings.browse"))
browse_button.clicked.connect(self.browse_new_path) browse_button.clicked.connect(self.browse_new_path)
path_layout.addWidget(browse_button) path_layout.addWidget(browse_button)
@@ -181,11 +183,11 @@ class DownloadSettingsDialog(QDialog):
layout.addWidget(path_group_box) layout.addWidget(path_group_box)
# --- Speed Limit Section --- # --- Speed Limit Section ---
speed_group_box = QGroupBox("Speed Limit") speed_group_box = QGroupBox(_("settings.speed_limit"))
speed_layout = QHBoxLayout() speed_layout = QHBoxLayout()
self.speed_limit_input = QLineEdit(str(self.current_limit)) 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) speed_layout.addWidget(self.speed_limit_input)
self.speed_limit_unit = QComboBox() self.speed_limit_unit = QComboBox()
@@ -197,25 +199,25 @@ class DownloadSettingsDialog(QDialog):
layout.addWidget(speed_group_box) layout.addWidget(speed_group_box)
# --- Auto-Update yt-dlp Section --- # --- 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() auto_update_layout = QVBoxLayout()
# Load current auto-update settings # Load current auto-update settings
auto_settings = get_auto_update_settings() auto_settings = get_auto_update_settings()
# Enable/Disable auto-update checkbox # 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"]) self.auto_update_enabled.setChecked(auto_settings["enabled"])
auto_update_layout.addWidget(self.auto_update_enabled) auto_update_layout.addWidget(self.auto_update_enabled)
# Frequency options # Frequency options
frequency_label = QLabel("Update frequency:") frequency_label = QLabel(_("settings.update_frequency"))
frequency_label.setStyleSheet("color: #ffffff; margin-top: 10px;") frequency_label.setStyleSheet("color: #ffffff; margin-top: 10px;")
auto_update_layout.addWidget(frequency_label) auto_update_layout.addWidget(frequency_label)
self.startup_radio = QRadioButton("Check on every startup (minimum 1 hour between checks)") self.startup_radio = QRadioButton(_("settings.check_startup"))
self.daily_radio = QRadioButton("Check daily") self.daily_radio = QRadioButton(_("settings.check_daily"))
self.weekly_radio = QRadioButton("Check weekly") self.weekly_radio = QRadioButton(_("settings.check_weekly"))
# Set current selection based on saved settings # Set current selection based on saved settings
current_frequency = auto_settings["frequency"] current_frequency = auto_settings["frequency"]
@@ -232,7 +234,7 @@ class DownloadSettingsDialog(QDialog):
# Test update button # Test update button
test_update_layout = QHBoxLayout() 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_button.clicked.connect(self.test_update_check)
test_update_layout.addWidget(test_update_button) test_update_layout.addWidget(test_update_button)
test_update_layout.addStretch() test_update_layout.addStretch()
@@ -242,13 +244,15 @@ class DownloadSettingsDialog(QDialog):
layout.addWidget(auto_update_group_box) layout.addWidget(auto_update_group_box)
# Dialog buttons (OK/Cancel) # 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.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
layout.addWidget(button_box) layout.addWidget(button_box)
def browse_new_path(self) -> None: 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: if new_path:
self.current_path = new_path self.current_path = new_path
self.path_display.setText(self.current_path) self.path_display.setText(self.current_path)
@@ -316,8 +320,8 @@ class DownloadSettingsDialog(QDialog):
if "Error" in current_version: if "Error" in current_version:
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning, QMessageBox.Icon.Warning,
"Update Check", _("settings.update_check_title"),
"Could not determine current yt-dlp version.", _("settings.could_not_determine_version"),
) )
msg_box.exec() msg_box.exec()
return return
@@ -334,22 +338,22 @@ class DownloadSettingsDialog(QDialog):
if version_parser.parse(latest_version) > version_parser.parse(current_version): if version_parser.parse(latest_version) > version_parser.parse(current_version):
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information, QMessageBox.Icon.Information,
"Update Check", _("settings.update_check_title"),
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_available_dialog", current=current_version, latest=latest_version),
) )
msg_box.exec() msg_box.exec()
else: else:
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information, QMessageBox.Icon.Information,
"Update Check", _("settings.update_check_title"),
f"yt-dlp is up to date!\n\nCurrent version: {current_version}", _("settings.up_to_date_dialog", version=current_version),
) )
msg_box.exec() msg_box.exec()
except Exception as e: except Exception as e:
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning, QMessageBox.Icon.Warning,
"Update Check", _("settings.update_check_title"),
f"Error checking for updates: {e}", _("settings.error_checking_updates", error=str(e)),
) )
msg_box.exec() msg_box.exec()
@@ -375,13 +379,13 @@ class DownloadSettingsDialog(QDialog):
if update_auto_update_settings(enabled, frequency): if update_auto_update_settings(enabled, frequency):
QMessageBox.information( QMessageBox.information(
self, self,
"Settings Saved", _("settings.settings_saved_title"),
"Auto-update settings have been saved successfully!", _("settings.settings_saved_message"),
) )
else: 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: 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 # Call the parent accept method to close the dialog
super().accept() super().accept()
@@ -390,7 +394,7 @@ class DownloadSettingsDialog(QDialog):
class AutoUpdateSettingsDialog(QDialog): class AutoUpdateSettingsDialog(QDialog):
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Auto-Update Settings") self.setWindowTitle(_("settings.auto_update_title"))
self.setMinimumWidth(400) self.setMinimumWidth(400)
self.setMinimumHeight(300) self.setMinimumHeight(300)
@@ -406,32 +410,32 @@ class AutoUpdateSettingsDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Title # 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) title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label) layout.addWidget(title_label)
# Description # 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.setWordWrap(True)
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter) desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;") desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
layout.addWidget(desc_label) layout.addWidget(desc_label)
# Enable/Disable auto-update # 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.setChecked(True) # Default enabled
self.enable_checkbox.toggled.connect(self.on_enable_toggled) self.enable_checkbox.toggled.connect(self.on_enable_toggled)
layout.addWidget(self.enable_checkbox) layout.addWidget(self.enable_checkbox)
# Frequency options # Frequency options
frequency_group = QGroupBox("Update Frequency") frequency_group = QGroupBox(_("settings.update_frequency_group"))
frequency_layout = QVBoxLayout() frequency_layout = QVBoxLayout()
self.frequency_group = QButtonGroup(self) self.frequency_group = QButtonGroup(self)
self.startup_radio = QRadioButton("Check on every startup (minimum 1 hour between checks)") self.startup_radio = QRadioButton(_("settings.check_startup"))
self.daily_radio = QRadioButton("Check daily") self.daily_radio = QRadioButton(_("settings.check_daily"))
self.weekly_radio = QRadioButton("Check weekly") self.weekly_radio = QRadioButton(_("settings.check_weekly"))
self.daily_radio.setChecked(True) # Default to daily self.daily_radio.setChecked(True) # Default to daily
@@ -447,12 +451,12 @@ class AutoUpdateSettingsDialog(QDialog):
layout.addWidget(frequency_group) layout.addWidget(frequency_group)
# Current status # Current status
status_group = QGroupBox("Current Status") status_group = QGroupBox(_("settings.current_status"))
status_layout = QVBoxLayout() status_layout = QVBoxLayout()
self.current_version_label = QLabel("Current yt-dlp version: Checking...") self.current_version_label = QLabel(_("settings.current_version_label"))
self.last_check_label = QLabel("Last update check: Never") self.last_check_label = QLabel(_("settings.last_check_label"))
self.next_check_label = QLabel("Next check: Based on settings") self.next_check_label = QLabel(_("settings.next_check_label"))
status_layout.addWidget(self.current_version_label) status_layout.addWidget(self.current_version_label)
status_layout.addWidget(self.last_check_label) status_layout.addWidget(self.last_check_label)
@@ -462,17 +466,17 @@ class AutoUpdateSettingsDialog(QDialog):
layout.addWidget(status_group) layout.addWidget(status_group)
# Manual check button # 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) self.manual_check_btn.clicked.connect(self.manual_check)
layout.addWidget(self.manual_check_btn) layout.addWidget(self.manual_check_btn)
# Buttons # Buttons
button_layout = QHBoxLayout() 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.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) self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.save_btn) button_layout.addWidget(self.save_btn)
@@ -721,19 +725,19 @@ class AutoUpdateSettingsDialog(QDialog):
if update_auto_update_settings(enabled, frequency): if update_auto_update_settings(enabled, frequency):
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
QMessageBox.Icon.Information, QMessageBox.Icon.Information,
"Settings Saved", _("settings.settings_saved_title"),
"✅ Auto-update settings have been saved successfully!", _("settings.settings_saved_successfully"),
) )
msg_box.exec() msg_box.exec()
self.accept() self.accept()
else: else:
msg_box = self._create_styled_message_box( msg_box = self._create_styled_message_box(
QMessageBox.Icon.Warning, QMessageBox.Icon.Warning,
"Error", _("settings.error_title"),
"❌ Failed to save auto-update settings.\nPlease try again.", _("settings.failed_save_settings"),
) )
msg_box.exec() msg_box.exec()
except Exception as e: except Exception as e:
logger.exception(f"Error saving auto-update settings: {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() 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_utils import get_ytdlp_version, load_config, save_config
from src.core.ytsage_yt_dlp import get_yt_dlp_path 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_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 from src.utils.ytsage_logger import logger
try: try:
@@ -118,15 +119,15 @@ class UpdateThread(QThread):
error_message = "" error_message = ""
success = False success = False
try: try:
self.update_status.emit("🔍 Checking current installation...") self.update_status.emit(_('update.checking_current'))
self.update_progress.emit(10) self.update_progress.emit(10)
# Get the yt-dlp path # Get the yt-dlp path
try: try:
yt_dlp_path = get_yt_dlp_path() 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: 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}") self.update_finished.emit(False, f"❌ Error getting yt-dlp path: {e}")
return return
@@ -152,17 +153,17 @@ class UpdateThread(QThread):
is_app_managed = False is_app_managed = False
if is_app_managed: 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) success = self._update_binary(yt_dlp_path)
else: else:
self.update_status.emit("🐍 Updating system yt-dlp via pip...") self.update_status.emit(_('update.updating_pip'))
success = self._update_via_pip() success = self._update_via_pip()
if success: if success:
self.update_progress.emit(100) self.update_progress.emit(100)
error_message = "✅ yt-dlp has been successfully updated!" error_message = "✅ yt-dlp has been successfully updated!"
else: 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: except requests.RequestException as e:
error_message = f"❌ Network error during update: {e}" error_message = f"❌ Network error during update: {e}"
@@ -196,61 +197,61 @@ class UpdateThread(QThread):
logger.info("UpdateThread: yt-dlp update completed successfully.") logger.info("UpdateThread: yt-dlp update completed successfully.")
if result.stdout: if result.stdout:
logger.debug(f"yt-dlp output: {result.stdout.strip()}") 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) self.update_progress.emit(95)
return True return True
else: else:
logger.error(f"UpdateThread: yt-dlp update failed. {result.stderr.strip()}") 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 return False
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
logger.error("UpdateThread: yt-dlp update timed out.") 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 return False
except Exception as e: except Exception as e:
logger.exception(f"UpdateThread: Unexpected error during update: {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 return False
def _update_via_pip(self) -> bool: def _update_via_pip(self) -> bool:
"""Update yt-dlp via pip.""" """Update yt-dlp via pip."""
try: try:
self.update_status.emit("🔍 Checking current pip installation...") self.update_status.emit(_('update.checking_pip'))
self.update_progress.emit(30) self.update_progress.emit(30)
# Get current version # Get current version
try: try:
current_version = get_version("yt-dlp") 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: 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" current_version = "0.0.0"
self.update_progress.emit(40) self.update_progress.emit(40)
# Get the latest version from PyPI # 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) response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
if response.status_code != 200: if response.status_code != 200:
self.update_status.emit("❌ Failed to check for updates") self.update_status.emit(_('update.failed_check_updates'))
return False return False
data = response.json() data = response.json()
latest_version = data["info"]["version"] 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) self.update_progress.emit(50)
# Compare versions # Compare versions
if version.parse(latest_version) > version.parse(current_version): 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) self.update_progress.emit(60)
try: try:
# Run pip update with timeout # 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( update_result = subprocess.run(
[sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"], [sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"],
capture_output=True, capture_output=True,
@@ -263,11 +264,11 @@ class UpdateThread(QThread):
self.update_progress.emit(85) self.update_progress.emit(85)
if update_result.returncode == 0: 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) self.update_progress.emit(95)
return True return True
else: 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 return False
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
@@ -289,7 +290,7 @@ class UpdateThread(QThread):
class YTDLPUpdateDialog(QDialog): class YTDLPUpdateDialog(QDialog):
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("Update yt-dlp") self.setWindowTitle(_('update.title'))
self.setMinimumWidth(450) self.setMinimumWidth(450)
self.setMinimumHeight(200) self.setMinimumHeight(200)
self._closing = False # Flag to track if dialog is closing self._closing = False # Flag to track if dialog is closing
@@ -297,7 +298,7 @@ class YTDLPUpdateDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
# Status label # 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.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setWordWrap(True) self.status_label.setWordWrap(True)
self.status_label.setMinimumHeight(60) self.status_label.setMinimumHeight(60)
@@ -310,11 +311,11 @@ class YTDLPUpdateDialog(QDialog):
# Buttons # Buttons
button_layout = QHBoxLayout() 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.clicked.connect(self.perform_update)
self.update_btn.setEnabled(False) self.update_btn.setEnabled(False)
self.close_btn = QPushButton("Close") self.close_btn = QPushButton(_('buttons.close'))
self.close_btn.clicked.connect(self.close) self.close_btn.clicked.connect(self.close)
button_layout.addWidget(self.update_btn) button_layout.addWidget(self.update_btn)
@@ -370,7 +371,7 @@ class YTDLPUpdateDialog(QDialog):
self.check_version() self.check_version()
def check_version(self) -> None: def check_version(self) -> None:
self.status_label.setText("Checking for updates...") self.status_label.setText(_('update.checking'))
self.update_btn.setEnabled(False) self.update_btn.setEnabled(False)
self.version_check_thread = VersionCheckThread() self.version_check_thread = VersionCheckThread()
self.version_check_thread.finished.connect(self.on_version_check_finished) self.version_check_thread.finished.connect(self.on_version_check_finished)
@@ -387,7 +388,7 @@ class YTDLPUpdateDialog(QDialog):
return return
if not current_version or not latest_version: 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) self.update_btn.setEnabled(False)
return return
@@ -398,7 +399,7 @@ class YTDLPUpdateDialog(QDialog):
if current_ver < latest_ver: if current_ver < latest_ver:
self.status_label.setText( 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) self.update_btn.setEnabled(True)
else: else:
@@ -408,22 +409,22 @@ class YTDLPUpdateDialog(QDialog):
# If version parsing fails, do a simple string comparison # If version parsing fails, do a simple string comparison
if current_version != latest_version: if current_version != latest_version:
self.status_label.setText( 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) self.update_btn.setEnabled(True)
else: 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) self.update_btn.setEnabled(False)
except Exception as e: 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) self.update_btn.setEnabled(False)
def perform_update(self) -> None: def perform_update(self) -> None:
# Immediate visual feedback # Immediate visual feedback
self.update_btn.setEnabled(False) self.update_btn.setEnabled(False)
self.close_btn.setEnabled(False) self.close_btn.setEnabled(False)
self.update_btn.setText("Updating...") self.update_btn.setText(_('update.updating'))
self.status_label.setText("🚀 Initializing update process...") self.status_label.setText(_('update.initializing'))
# Show progress bar immediately # Show progress bar immediately
self.progress_bar.setRange(0, 100) self.progress_bar.setRange(0, 100)
@@ -461,7 +462,7 @@ class YTDLPUpdateDialog(QDialog):
self.progress_bar.setValue(100) self.progress_bar.setValue(100)
self.status_label.setText(message) self.status_label.setText(message)
self.close_btn.setEnabled(True) self.close_btn.setEnabled(True)
self.update_btn.setText("Update") # Reset button text self.update_btn.setText(_('buttons.update')) # Reset button text
if success: if success:
# Show success briefly then auto-check version # Show success briefly then auto-check version
+43 -41
View File
@@ -4,6 +4,8 @@ from PySide6.QtCore import QObject, Qt, Signal
from PySide6.QtGui import QColor from PySide6.QtGui import QColor
from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QHeaderView, QSizePolicy, QTableWidget, QTableWidgetItem, QWidget from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QHeaderView, QSizePolicy, QTableWidget, QTableWidgetItem, QWidget
from src.utils.ytsage_localization import _
if TYPE_CHECKING: if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp from src.gui.ytsage_gui_main import YTSageApp
@@ -22,14 +24,14 @@ class FormatTableMixin:
self.format_table.setColumnCount(8) self.format_table.setColumnCount(8)
self.format_table.setHorizontalHeaderLabels( self.format_table.setHorizontalHeaderLabels(
[ [
"Select", _("formats.select"),
"Quality", _("formats.quality"),
"Extension", _("formats.extension"),
"Resolution", _("formats.resolution"),
"File Size", _("formats.file_size"),
"Codec", _("formats.codec"),
"Audio", _("formats.audio"),
"Notes", _("formats.notes"),
] ]
) )
@@ -183,7 +185,7 @@ class FormatTableMixin:
# Configure columns based on mode # Configure columns based on mode
if is_playlist_mode: if is_playlist_mode:
self.format_table.setColumnCount(5) self.format_table.setColumnCount(5)
self.format_table.setHorizontalHeaderLabels(["Select", "Quality", "Resolution", "Notes", "Audio"]) self.format_table.setHorizontalHeaderLabels([_("formats.select"), _("formats.quality"), _("formats.resolution"), _("formats.notes"), _("formats.audio")])
# Configure column visibility and resizing for playlist mode # Configure column visibility and resizing for playlist mode
self.format_table.setColumnHidden(5, True) self.format_table.setColumnHidden(5, True)
@@ -202,14 +204,14 @@ class FormatTableMixin:
self.format_table.setColumnCount(8) self.format_table.setColumnCount(8)
self.format_table.setHorizontalHeaderLabels( self.format_table.setHorizontalHeaderLabels(
[ [
"Select", _("formats.select"),
"Quality", _("formats.quality"),
"Extension", _("formats.extension"),
"Resolution", _("formats.resolution"),
"File Size", _("formats.file_size"),
"Codec", _("formats.codec"),
"Audio", _("formats.audio"),
"Notes", _("formats.notes"),
] ]
) )
# Ensure all columns are visible # Ensure all columns are visible
@@ -262,14 +264,14 @@ class FormatTableMixin:
# Column 1: Quality (Always shown) # Column 1: Quality (Always shown)
quality_text = self.get_quality_label(f) quality_text = self.get_quality_label(f)
quality_item = QTableWidgetItem(quality_text) quality_item = QTableWidgetItem(quality_text)
# Set color based on quality # Set color based on quality (check both English and Spanish terms)
if "Best" in quality_text: if any(term in quality_text for term in ["Best", "Óptima", "Mejor"]):
quality_item.setForeground(QColor("#00ff00")) # Green for best quality quality_item.setForeground(QColor("#00ff00")) # Green for best quality
elif "High" in quality_text: elif any(term in quality_text for term in ["High", "Alta", "Alto"]):
quality_item.setForeground(QColor("#00cc00")) # Light green for high quality quality_item.setForeground(QColor("#00cc00")) # Light green for high quality
elif "Medium" in quality_text: elif any(term in quality_text for term in ["Medium", "Media", "Medio"]):
quality_item.setForeground(QColor("#ffaa00")) # Orange for medium quality quality_item.setForeground(QColor("#ffaa00")) # Orange for medium quality
elif "Low" in quality_text: elif any(term in quality_text for term in ["Low", "Baja", "Bajo"]):
quality_item.setForeground(QColor("#ff5555")) # Red for low quality quality_item.setForeground(QColor("#ff5555")) # Red for low quality
self.format_table.setItem(row, 1, quality_item) self.format_table.setItem(row, 1, quality_item)
@@ -278,7 +280,7 @@ class FormatTableMixin:
# Column 2: Resolution (Always shown) # Column 2: Resolution (Always shown)
resolution = f.get("resolution", "N/A") resolution = f.get("resolution", "N/A")
if f.get("vcodec") == "none": if f.get("vcodec") == "none":
resolution = "Audio only" resolution = _("formats.audio_only_resolution")
self.format_table.setItem(row, 2, QTableWidgetItem(resolution)) self.format_table.setItem(row, 2, QTableWidgetItem(resolution))
# Column 3: Notes for playlist mode, Extension for normal mode # Column 3: Notes for playlist mode, Extension for normal mode
@@ -299,11 +301,11 @@ class FormatTableMixin:
# Column 4 in playlist mode, Column 6 in normal mode: Audio Status # Column 4 in playlist mode, Column 6 in normal mode: Audio Status
needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none" # Only mark video-only as needing merge needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none" # Only mark video-only as needing merge
audio_status = "Will merge audio" if needs_audio else ("✓ Has Audio" if f.get("vcodec") != "none" else "Audio Only") audio_status = _("formats.will_merge_audio") if needs_audio else (_("formats.has_audio") if f.get("vcodec") != "none" else _("formats.audio_only"))
audio_item = QTableWidgetItem(audio_status) audio_item = QTableWidgetItem(audio_status)
if needs_audio: if needs_audio:
audio_item.setForeground(QColor("#ffa500")) audio_item.setForeground(QColor("#ffa500"))
elif audio_status == "Audio Only": elif audio_status == _("formats.audio_only"):
audio_item.setForeground(QColor("#cccccc")) # Neutral color for audio only audio_item.setForeground(QColor("#cccccc")) # Neutral color for audio only
else: # Has Audio (Video+Audio) else: # Has Audio (Video+Audio)
audio_item.setForeground(QColor("#00cc00")) # Green for included audio audio_item.setForeground(QColor("#00cc00")) # Green for included audio
@@ -369,13 +371,13 @@ class FormatTableMixin:
# Audio quality # Audio quality
abr = format_info.get("abr", 0) abr = format_info.get("abr", 0)
if abr >= 256: if abr >= 256:
return "Best Audio" return _("formats.best_audio")
elif abr >= 192: elif abr >= 192:
return "High Audio" return _("formats.high_audio")
elif abr >= 128: elif abr >= 128:
return "Medium Audio" return _("formats.medium_audio")
else: else:
return "Low Audio" return _("formats.low_audio")
else: else:
# Video quality # Video quality
height = 0 height = 0
@@ -387,17 +389,17 @@ class FormatTableMixin:
pass pass
if height >= 2160: if height >= 2160:
return "Best (4K)" return _("formats.best_4k")
elif height >= 1440: elif height >= 1440:
return "Best (2K)" return _("formats.best_2k")
elif height >= 1080: elif height >= 1080:
return "High (1080p)" return _("formats.high_1080p")
elif height >= 720: elif height >= 720:
return "High (720p)" return _("formats.high_720p")
elif height >= 480: elif height >= 480:
return "Medium (480p)" return _("formats.medium_480p")
else: else:
return "Low Quality" return _("formats.low_quality")
def _get_format_notes(self, format_info) -> str: def _get_format_notes(self, format_info) -> str:
"""Generate helpful format notes based on format info.""" """Generate helpful format notes based on format info."""
@@ -410,11 +412,11 @@ class FormatTableMixin:
# Better file size categories # Better file size categories
if file_size > 50 * 1024 * 1024: # Over 50MB if file_size > 50 * 1024 * 1024: # Over 50MB
notes.append("Large size") notes.append(_("formats.large_size"))
elif file_size > 15 * 1024 * 1024: # 15-50MB elif file_size > 15 * 1024 * 1024: # 15-50MB
notes.append("Medium size") notes.append(_("formats.medium_size"))
elif file_size > 5 * 1024 * 1024: # 5-15MB elif file_size > 5 * 1024 * 1024: # 5-15MB
notes.append("Standard size") notes.append(_("formats.standard_size"))
else: # Under 5MB else: # Under 5MB
notes.append("Small size") notes.append("Small size")
@@ -422,11 +424,11 @@ class FormatTableMixin:
vcodec = format_info.get("vcodec", "") vcodec = format_info.get("vcodec", "")
if vcodec != "none": if vcodec != "none":
if "avc1" in vcodec: # H.264 if "avc1" in vcodec: # H.264
notes.append("Compatible") notes.append(_("formats.compatible"))
elif "av01" in vcodec: # AV1 elif "av01" in vcodec: # AV1
notes.append("Efficient") notes.append(_("formats.efficient"))
elif "vp9" in vcodec: # VP9 elif "vp9" in vcodec: # VP9
notes.append("High quality") notes.append(_("formats.high_quality"))
# Add quick mobile compatibility check # Add quick mobile compatibility check
if "avc1" in vcodec and file_size < 8 * 1024 * 1024: if "avc1" in vcodec and file_size < 8 * 1024 * 1024:
+55 -50
View File
@@ -46,6 +46,7 @@ from src.gui.ytsage_gui_video_info import VideoInfoMixin
from src.utils.ytsage_constants import ICON_PATH, SOUND_PATH, SUBPROCESS_CREATIONFLAGS from src.utils.ytsage_constants import ICON_PATH, SOUND_PATH, SUBPROCESS_CREATIONFLAGS
from src.utils.ytsage_logger import logger from src.utils.ytsage_logger import logger
from src.utils.ytsage_config_manager import ConfigManager from src.utils.ytsage_config_manager import ConfigManager
from src.utils.ytsage_localization import LocalizationManager, _
try: try:
import yt_dlp import yt_dlp
@@ -60,6 +61,10 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
# Initialize localization system
saved_language = ConfigManager.get("language") or "en"
LocalizationManager.initialize(saved_language)
# Log startup warnings for missing dependencies # Log startup warnings for missing dependencies
if not YT_DLP_AVAILABLE: if not YT_DLP_AVAILABLE:
logger.warning("yt-dlp not available at startup, will be downloaded at runtime") logger.warning("yt-dlp not available at startup, will be downloaded at runtime")
@@ -316,7 +321,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
logger.exception(f"Error playing notification sound: {e}") logger.exception(f"Error playing notification sound: {e}")
def init_ui(self) -> None: def init_ui(self) -> None:
self.setWindowTitle(f"YTSage v{self.version}") self.setWindowTitle(f"{_('app.title')} {_('app.version', version=self.version)}")
self.setMinimumSize(900, 750) self.setMinimumSize(900, 750)
# Main widget and layout # Main widget and layout
@@ -329,13 +334,13 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# URL input section # URL input section
url_layout = QHBoxLayout() url_layout = QHBoxLayout()
self.url_input = QLineEdit() self.url_input = QLineEdit()
self.url_input.setPlaceholderText("Enter YouTube video or playlist URL") self.url_input.setPlaceholderText(_("main_ui.url_placeholder"))
self.url_input.returnPressed.connect(self.analyze_url) # Analyze on Enter key self.url_input.returnPressed.connect(self.analyze_url) # Analyze on Enter key
self.analyze_button = QPushButton("Analyze") self.analyze_button = QPushButton(_("buttons.analyze"))
self.analyze_button.clicked.connect(self.analyze_url) self.analyze_button.clicked.connect(self.analyze_url)
self.paste_button = QPushButton("Paste URL") self.paste_button = QPushButton(_("buttons.paste_url"))
self.paste_button.clicked.connect(self.paste_url) self.paste_button.clicked.connect(self.paste_url)
url_layout.addWidget(self.url_input, 1) url_layout.addWidget(self.url_input, 1)
@@ -363,7 +368,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
layout.addWidget(self.playlist_info_label) layout.addWidget(self.playlist_info_label)
# Add playlist selection BUTTON (initially hidden) - REPLACED QLineEdit # Add playlist selection BUTTON (initially hidden) - REPLACED QLineEdit
self.playlist_select_btn = QPushButton("Select Videos...") self.playlist_select_btn = QPushButton(_("buttons.select_videos"))
self.playlist_select_btn.clicked.connect(self.open_playlist_selection_dialog) self.playlist_select_btn.clicked.connect(self.open_playlist_selection_dialog)
self.playlist_select_btn.setVisible(False) self.playlist_select_btn.setVisible(False)
self.playlist_select_btn.setStyleSheet( self.playlist_select_btn.setStyleSheet(
@@ -394,7 +399,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_layout = QHBoxLayout() self.format_layout = QHBoxLayout()
# Show formats label # Show formats label
self.show_formats_label = QLabel("Show formats:") self.show_formats_label = QLabel(_("formats.show_formats"))
self.show_formats_label.setStyleSheet("color: white;") self.show_formats_label.setStyleSheet("color: white;")
self.format_layout.addWidget(self.show_formats_label) self.format_layout.addWidget(self.show_formats_label)
@@ -403,7 +408,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_buttons.setExclusive(True) self.format_buttons.setExclusive(True)
# Video button # Video button
self.video_button = QPushButton("Video") self.video_button = QPushButton(_("buttons.video"))
self.video_button.setCheckable(True) self.video_button.setCheckable(True)
self.video_button.setChecked(True) # Set video as default self.video_button.setChecked(True) # Set video as default
self.video_button.setStyleSheet( self.video_button.setStyleSheet(
@@ -431,7 +436,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_layout.addWidget(self.video_button) self.format_layout.addWidget(self.video_button)
# Audio button # Audio button
self.audio_button = QPushButton("Audio Only") self.audio_button = QPushButton(_("buttons.audio_only"))
self.audio_button.setCheckable(True) self.audio_button.setCheckable(True)
self.audio_button.setStyleSheet( self.audio_button.setStyleSheet(
""" """
@@ -458,7 +463,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_layout.addWidget(self.audio_button) self.format_layout.addWidget(self.audio_button)
# Add Merge Subtitles checkbox (Moved here) # Add Merge Subtitles checkbox (Moved here)
self.merge_subs_checkbox = QCheckBox("Merge Subtitles") self.merge_subs_checkbox = QCheckBox(_("main_ui.merge_subtitles"))
self.merge_subs_checkbox.setStyleSheet( self.merge_subs_checkbox.setStyleSheet(
""" """
QCheckBox { QCheckBox {
@@ -491,7 +496,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_layout.addWidget(self.merge_subs_checkbox) self.format_layout.addWidget(self.merge_subs_checkbox)
# Add Save Thumbnail Checkbox (Moved here) # Add Save Thumbnail Checkbox (Moved here)
self.save_thumbnail_checkbox = QCheckBox("Save Thumbnail") self.save_thumbnail_checkbox = QCheckBox(_("main_ui.save_thumbnail"))
self.save_thumbnail_checkbox.setChecked(False) self.save_thumbnail_checkbox.setChecked(False)
self.save_thumbnail_checkbox.stateChanged.connect(self.toggle_save_thumbnail) self.save_thumbnail_checkbox.stateChanged.connect(self.toggle_save_thumbnail)
self.save_thumbnail_checkbox.setStyleSheet( self.save_thumbnail_checkbox.setStyleSheet(
@@ -511,7 +516,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_layout.addWidget(self.save_thumbnail_checkbox) self.format_layout.addWidget(self.save_thumbnail_checkbox)
# Add Save Description Checkbox (Moved here) # Add Save Description Checkbox (Moved here)
self.save_description_checkbox = QCheckBox("Save Description") self.save_description_checkbox = QCheckBox(_("main_ui.save_description"))
self.save_description_checkbox.setChecked(False) self.save_description_checkbox.setChecked(False)
self.save_description_checkbox.stateChanged.connect(self.toggle_save_description) self.save_description_checkbox.stateChanged.connect(self.toggle_save_description)
self.save_description_checkbox.setStyleSheet( self.save_description_checkbox.setStyleSheet(
@@ -531,7 +536,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.format_layout.addWidget(self.save_description_checkbox) self.format_layout.addWidget(self.save_description_checkbox)
# Add Embed Chapters Checkbox # Add Embed Chapters Checkbox
self.embed_chapters_checkbox = QCheckBox("Embed Chapters") self.embed_chapters_checkbox = QCheckBox(_("main_ui.embed_chapters"))
self.embed_chapters_checkbox.setChecked(False) self.embed_chapters_checkbox.setChecked(False)
self.embed_chapters_checkbox.stateChanged.connect(self.toggle_embed_chapters) self.embed_chapters_checkbox.stateChanged.connect(self.toggle_embed_chapters)
self.embed_chapters_checkbox.setStyleSheet( self.embed_chapters_checkbox.setStyleSheet(
@@ -562,34 +567,34 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
download_layout = QHBoxLayout() download_layout = QHBoxLayout()
# Replace the two separate buttons with a single Custom Options button # Replace the two separate buttons with a single Custom Options button
self.custom_options_btn = QPushButton("Custom Options") self.custom_options_btn = QPushButton(_("buttons.custom_options"))
self.custom_options_btn.clicked.connect(self.show_custom_options) self.custom_options_btn.clicked.connect(self.show_custom_options)
self.about_btn = QPushButton("About") self.about_btn = QPushButton(_("buttons.about"))
self.about_btn.clicked.connect(self.show_about_dialog) self.about_btn.clicked.connect(self.show_about_dialog)
# Add new Time Range button # Add new Time Range button
self.time_range_btn = QPushButton("Trim Video") self.time_range_btn = QPushButton(_("buttons.trim_video"))
self.time_range_btn.clicked.connect(self.show_time_range_dialog) self.time_range_btn.clicked.connect(self.show_time_range_dialog)
self.update_ytdlp_btn = QPushButton("Update yt-dlp") self.update_ytdlp_btn = QPushButton(_("buttons.update"))
self.update_ytdlp_btn.clicked.connect(self.update_ytdlp) self.update_ytdlp_btn.clicked.connect(self.update_ytdlp)
# --- Rename Path Button to Settings Button --- # --- Rename Path Button to Settings Button ---
self.settings_button = QPushButton("Download Settings") # Renamed button self.settings_button = QPushButton(_("buttons.download_settings")) # Renamed button
self.settings_button.clicked.connect(self.show_download_settings_dialog) # Renamed method self.settings_button.clicked.connect(self.show_download_settings_dialog) # Renamed method
self.settings_button.setToolTip(f"Current Path: {self.last_path}\nSpeed Limit: None") # Update initial tooltip self.settings_button.setToolTip(f"Current Path: {self.last_path}\nSpeed Limit: None") # Update initial tooltip
# --- End Settings Button --- # --- End Settings Button ---
self.download_btn = QPushButton("Download") self.download_btn = QPushButton(_("buttons.download"))
self.download_btn.clicked.connect(self.start_download) self.download_btn.clicked.connect(self.start_download)
# Add pause and cancel buttons # Add pause and cancel buttons
self.pause_btn = QPushButton("Pause") self.pause_btn = QPushButton(_("buttons.pause"))
self.pause_btn.clicked.connect(self.toggle_pause) self.pause_btn.clicked.connect(self.toggle_pause)
self.pause_btn.setVisible(False) # Hidden initially self.pause_btn.setVisible(False) # Hidden initially
self.cancel_btn = QPushButton("Cancel") self.cancel_btn = QPushButton(_("buttons.cancel"))
self.cancel_btn.clicked.connect(self.cancel_download) self.cancel_btn.clicked.connect(self.cancel_download)
self.cancel_btn.setVisible(False) # Hidden initially self.cancel_btn.setVisible(False) # Hidden initially
@@ -640,7 +645,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
) )
progress_layout.addWidget(self.download_details_label) progress_layout.addWidget(self.download_details_label)
self.status_label = QLabel("Ready") self.status_label = QLabel(_("app.ready"))
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setStyleSheet( self.status_label.setStyleSheet(
""" """
@@ -673,12 +678,12 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.signals.update_status.emit("Invalid URL or please enter a URL.") self.signals.update_status.emit("Invalid URL or please enter a URL.")
return return
self.signals.update_status.emit("Analyzing (0%)... Preparing request") self.signals.update_status.emit(_("main_ui.analyzing_preparing"))
threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start() threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start()
def _analyze_url_thread(self, url) -> None: def _analyze_url_thread(self, url) -> None:
try: try:
self.signals.update_status.emit("Analyzing (15%)... Extracting basic info") self.signals.update_status.emit(_("main_ui.analyzing_extracting_basic"))
# Clean up the URL to handle both playlist and video URLs # Clean up the URL to handle both playlist and video URLs
if "list=" in url and "watch?v=" in url: if "list=" in url and "watch?v=" in url:
@@ -719,7 +724,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
if not basic_info: if not basic_info:
logger.error("Could not extract basic video information") logger.error("Could not extract basic video information")
self.signals.update_status.emit( self.signals.update_status.emit(
"Error: Could not extract basic video information. Please check your link." _("main_ui.error_extract_info")
) )
# Hide playlist UI on error # Hide playlist UI on error
self.signals.playlist_info_label_visible.emit(False) self.signals.playlist_info_label_visible.emit(False)
@@ -733,7 +738,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.signals.update_status.emit(user_friendly_error) self.signals.update_status.emit(user_friendly_error)
return return
self.signals.update_status.emit("Analyzing (30%)... Extracting detailed info") self.signals.update_status.emit(_("main_ui.analyzing_extracting_detailed"))
# Configure options for detailed extraction (keep other options) # Configure options for detailed extraction (keep other options)
# Add no_warnings here as well, as this is where detailed info is fetched # Add no_warnings here as well, as this is where detailed info is fetched
ydl_opts_detail = { ydl_opts_detail = {
@@ -762,7 +767,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# Use a separate options dict for the detailed extraction # Use a separate options dict for the detailed extraction
with yt_dlp.YoutubeDL(ydl_opts_detail) as ydl_detail: with yt_dlp.YoutubeDL(ydl_opts_detail) as ydl_detail:
try: try:
self.signals.update_status.emit("Analyzing (45%)... Processing video data") self.signals.update_status.emit(_("main_ui.analyzing_processing_video"))
if basic_info.get("_type") == "playlist": if basic_info.get("_type") == "playlist":
self.is_playlist = True self.is_playlist = True
self.playlist_info = basic_info self.playlist_info = basic_info
@@ -806,7 +811,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# Show playlist selection BUTTON # Show playlist selection BUTTON
# update signal method from QMetaObject.invokeMethod to signals # update signal method from QMetaObject.invokeMethod to signals
self.signals.playlist_select_btn_text.emit("Select Videos... (All selected)") self.signals.playlist_select_btn_text.emit(_("main_ui.select_videos_all"))
self.signals.playlist_select_btn_visible.emit(True) self.signals.playlist_select_btn_visible.emit(True)
else: # Single video else: # Single video
@@ -824,19 +829,19 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# Verify we have format information # Verify we have format information
if not self.video_info or "formats" not in self.video_info: if not self.video_info or "formats" not in self.video_info:
logger.debug(f"video_info keys: {self.video_info.keys() if self.video_info else 'None'}") logger.debug(f"video_info keys: {self.video_info.keys() if self.video_info else 'None'}")
self.signals.update_status.emit("Error: No format information available.") self.signals.update_status.emit(_("main_ui.error_no_format_info"))
self.signals.playlist_info_label_visible.emit(False) self.signals.playlist_info_label_visible.emit(False)
self.signals.playlist_select_btn_visible.emit(False) self.signals.playlist_select_btn_visible.emit(False)
return return
self.signals.update_status.emit("Analyzing (60%)... Processing formats") self.signals.update_status.emit(_("main_ui.analyzing_processing_formats"))
self.all_formats = self.video_info["formats"] self.all_formats = self.video_info["formats"]
# Update UI # Update UI
self.update_video_info(self.video_info) self.update_video_info(self.video_info)
# Update thumbnail # Update thumbnail
self.signals.update_status.emit("Analyzing (75%)... Loading thumbnail") self.signals.update_status.emit(_("main_ui.analyzing_loading_thumbnail"))
# Try to get thumbnail from playlist info first # Try to get thumbnail from playlist info first
# Fallback to video thumbnail if playlist thumbnail not found or not a playlist # Fallback to video thumbnail if playlist thumbnail not found or not a playlist
thumbnail_url = (self.playlist_info or {}).get("thumbnail") or (self.video_info or {}).get("thumbnail") thumbnail_url = (self.playlist_info or {}).get("thumbnail") or (self.video_info or {}).get("thumbnail")
@@ -849,14 +854,14 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
) )
# --- Subtitle Handling --- # --- Subtitle Handling ---
self.signals.update_status.emit("Analyzing (85%)... Processing subtitles") self.signals.update_status.emit(_("main_ui.analyzing_processing_subtitles"))
# Clear previous selections when analyzing a new video # Clear previous selections when analyzing a new video
self.selected_subtitles = [] self.selected_subtitles = []
self.available_subtitles = self.video_info.get("subtitles", {}) self.available_subtitles = self.video_info.get("subtitles", {})
self.available_automatic_subtitles = self.video_info.get("automatic_captions", {}) self.available_automatic_subtitles = self.video_info.get("automatic_captions", {})
# Update the UI elements related to subtitle selection state # Update the UI elements related to subtitle selection state
# update signal method from QMetaObject.invokeMethod to signals # update signal method from QMetaObject.invokeMethod to signals
self.signals.selected_subs_label_text.emit("0 selected") self.signals.selected_subs_label_text.emit(_("main_ui.subtitles_selected", count=0))
# QMetaObject.invokeMethod( # QMetaObject.invokeMethod(
# self.subtitle_select_btn, # self.subtitle_select_btn,
@@ -875,11 +880,11 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# ) # )
# Update format table # Update format table
self.signals.update_status.emit("Analyzing (95%)... Updating format table") self.signals.update_status.emit(_("main_ui.analyzing_updating_table"))
self.video_button.setChecked(True) self.video_button.setChecked(True)
self.audio_button.setChecked(False) self.audio_button.setChecked(False)
self.filter_formats() self.filter_formats()
self.signals.update_status.emit("Analysis complete!") self.signals.update_status.emit(_("main_ui.analysis_complete"))
except Exception as e: except Exception as e:
logger.exception(f"Detailed extraction failed: {e}") logger.exception(f"Detailed extraction failed: {e}")
@@ -948,22 +953,22 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
if not url or not path: if not url or not path:
# More specific error message if path is missing # More specific error message if path is missing
if not path: if not path:
self.status_label.setText("Please set a download path using 'Change Path'") self.status_label.setText(_('download.please_set_path'))
elif not url: elif not url:
self.status_label.setText("Please enter a URL") self.status_label.setText(_('download.please_enter_url'))
else: else:
self.status_label.setText("Please enter URL and set download path") self.status_label.setText(_('download.please_enter_url_and_path'))
return return
# --- End Path Change --- # --- End Path Change ---
# Get selected format # Get selected format
format_id = self.get_selected_format() format_id = self.get_selected_format()
if not format_id: if not format_id:
self.status_label.setText("Please select a format") self.status_label.setText(_('download.please_select_format'))
return return
# Show preparation message # Show preparation message
self.status_label.setText("🚀 Preparing your download...") self.status_label.setText(_('download.preparing'))
self.progress_bar.setValue(0) self.progress_bar.setValue(0)
# Get resolution for filename # Get resolution for filename
@@ -1044,7 +1049,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.download_cancelled = False self.download_cancelled = False
# Show pause/cancel buttons # Show pause/cancel buttons
self.pause_btn.setText("Pause") self.pause_btn.setText(_("buttons.pause"))
self.pause_btn.setVisible(True) self.pause_btn.setVisible(True)
self.cancel_btn.setVisible(True) self.cancel_btn.setVisible(True)
@@ -1066,16 +1071,16 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# Video file extensions # Video file extensions
if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]: if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]:
self.status_label.setText(f"✅ Video download completed!") self.status_label.setText(_('download.video_completed'))
# Audio file extensions # Audio file extensions
elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]: elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]:
self.status_label.setText(f"✅ Audio download completed!") self.status_label.setText(_('download.audio_completed'))
# Subtitle file extensions # Subtitle file extensions
elif ext in [".vtt", ".srt", ".ass", ".ssa"]: elif ext in [".vtt", ".srt", ".ass", ".ssa"]:
self.status_label.setText(f"✅ Subtitle download completed!") self.status_label.setText(_('download.subtitle_completed'))
# Default case # Default case
else: else:
self.status_label.setText("✅ Download completed!") self.status_label.setText(_('download.completed'))
# Play notification sound when download completes # Play notification sound when download completes
self.play_notification_sound() self.play_notification_sound()
@@ -1102,7 +1107,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.pause_btn.setText("Resume") self.pause_btn.setText("Resume")
self.signals.update_status.emit("Download paused") self.signals.update_status.emit("Download paused")
else: else:
self.pause_btn.setText("Pause") self.pause_btn.setText(_("buttons.pause"))
self.signals.update_status.emit("Download resumed") self.signals.update_status.emit("Download resumed")
def check_for_updates(self) -> None: def check_for_updates(self) -> None:
@@ -1735,7 +1740,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.signals.playlist_select_btn_visible.emit(False) self.signals.playlist_select_btn_visible.emit(False)
return return
self.signals.update_status.emit("Analyzing (30%)... Extracting info with yt-dlp executable") self.signals.update_status.emit(_("main_ui.analyzing_extracting_ytdlp"))
# Clean up the URL to handle both playlist and video URLs # Clean up the URL to handle both playlist and video URLs
if "list=" in url and "watch?v=" in url: if "list=" in url and "watch?v=" in url:
@@ -1787,7 +1792,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.signals.playlist_select_btn_visible.emit(False) self.signals.playlist_select_btn_visible.emit(False)
return return
self.signals.update_status.emit("Analyzing (60%)... Processing data") self.signals.update_status.emit(_("main_ui.analyzing_processing_data"))
if first_info.get("_type") == "playlist" or len(json_lines) > 1: if first_info.get("_type") == "playlist" or len(json_lines) > 1:
# Handle playlist # Handle playlist
@@ -1852,14 +1857,14 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.signals.playlist_select_btn_visible.emit(False) self.signals.playlist_select_btn_visible.emit(False)
return return
self.signals.update_status.emit("Analyzing (75%)... Processing formats") self.signals.update_status.emit(_("main_ui.analyzing_processing_formats_ytdlp"))
self.all_formats = self.video_info["formats"] self.all_formats = self.video_info["formats"]
# Update UI # Update UI
self.update_video_info(self.video_info) self.update_video_info(self.video_info)
# Update thumbnail # Update thumbnail
self.signals.update_status.emit("Analyzing (85%)... Loading thumbnail") self.signals.update_status.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp"))
# Try to get thumbnail from playlist info first # Try to get thumbnail from playlist info first
# Fallback to video thumbnail if playlist thumbnail not found or not a playlist # Fallback to video thumbnail if playlist thumbnail not found or not a playlist
thumbnail_url = (self.playlist_info or {}).get("thumbnail") or (self.video_info or {}).get("thumbnail") thumbnail_url = (self.playlist_info or {}).get("thumbnail") or (self.video_info or {}).get("thumbnail")
@@ -1871,7 +1876,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
self.download_thumbnail_file(self.video_url, self.path_input.text()) # type: ignore[reportAttributeAccessIssue] self.download_thumbnail_file(self.video_url, self.path_input.text()) # type: ignore[reportAttributeAccessIssue]
# Handle subtitles # Handle subtitles
self.signals.update_status.emit("Analyzing (90%)... Processing subtitles") self.signals.update_status.emit(_("main_ui.analyzing_processing_subtitles_ytdlp"))
self.selected_subtitles = [] self.selected_subtitles = []
self.available_subtitles = self.video_info.get("subtitles", {}) self.available_subtitles = self.video_info.get("subtitles", {})
self.available_automatic_subtitles = self.video_info.get("automatic_captions", {}) self.available_automatic_subtitles = self.video_info.get("automatic_captions", {})
+16 -15
View File
@@ -14,6 +14,7 @@ from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__
SponsorBlockCategoryDialog, SponsorBlockCategoryDialog,
SubtitleSelectionDialog, SubtitleSelectionDialog,
) )
from src.utils.ytsage_localization import _
from src.utils.ytsage_logger import logger from src.utils.ytsage_logger import logger
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -95,7 +96,7 @@ class VideoInfoMixin:
subtitle_layout.setSpacing(10) subtitle_layout.setSpacing(10)
# Subtitle selection button # Subtitle selection button
self.subtitle_select_btn = QPushButton("Select Subtitles...") # Renamed & changed text self.subtitle_select_btn = QPushButton(_("main_ui.select_subtitles")) # Renamed & changed text
self.subtitle_select_btn.setFixedHeight(30) self.subtitle_select_btn.setFixedHeight(30)
# self.subtitle_select_btn.setFixedWidth(150) # Let it size naturally or adjust as needed # self.subtitle_select_btn.setFixedWidth(150) # Let it size naturally or adjust as needed
self.subtitle_select_btn.clicked.connect(self.open_subtitle_dialog) self.subtitle_select_btn.clicked.connect(self.open_subtitle_dialog)
@@ -124,7 +125,7 @@ class VideoInfoMixin:
subtitle_layout.addWidget(self.subtitle_select_btn) subtitle_layout.addWidget(self.subtitle_select_btn)
# Label to show number of selected subtitles # Label to show number of selected subtitles
self.selected_subs_label = QLabel("0 selected") self.selected_subs_label = QLabel(_("selection.none_selected"))
self.selected_subs_label.setStyleSheet("color: #cccccc; padding-left: 5px;") self.selected_subs_label.setStyleSheet("color: #cccccc; padding-left: 5px;")
subtitle_layout.addWidget(self.selected_subs_label) subtitle_layout.addWidget(self.selected_subs_label)
@@ -138,7 +139,7 @@ class VideoInfoMixin:
# --- SponsorBlock Section --- # --- SponsorBlock Section ---
sponsorblock_layout = QHBoxLayout() sponsorblock_layout = QHBoxLayout()
self.sponsorblock_select_btn = QPushButton("SponsorBlock Categories...") self.sponsorblock_select_btn = QPushButton(_("main_ui.sponsorblock_categories"))
self.sponsorblock_select_btn.setFixedHeight(30) self.sponsorblock_select_btn.setFixedHeight(30)
self.sponsorblock_select_btn.clicked.connect(self.open_sponsorblock_dialog) self.sponsorblock_select_btn.clicked.connect(self.open_sponsorblock_dialog)
self.sponsorblock_select_btn.setStyleSheet( self.sponsorblock_select_btn.setStyleSheet(
@@ -167,7 +168,7 @@ class VideoInfoMixin:
sponsorblock_layout.addWidget(self.sponsorblock_select_btn) sponsorblock_layout.addWidget(self.sponsorblock_select_btn)
# Label to show selection count # Label to show selection count
self.selected_sponsorblock_label = QLabel("0 selected") self.selected_sponsorblock_label = QLabel(_("selection.none_selected"))
self.selected_sponsorblock_label.setStyleSheet("color: #cccccc; padding-left: 5px;") self.selected_sponsorblock_label.setStyleSheet("color: #cccccc; padding-left: 5px;")
sponsorblock_layout.addWidget(self.selected_sponsorblock_label) sponsorblock_layout.addWidget(self.selected_sponsorblock_label)
@@ -253,7 +254,7 @@ class VideoInfoMixin:
date_obj = datetime.strptime(upload_date, "%Y%m%d") date_obj = datetime.strptime(upload_date, "%Y%m%d")
formatted_date = date_obj.strftime("%B %d, %Y") formatted_date = date_obj.strftime("%B %d, %Y")
else: else:
formatted_date = "Unknown date" formatted_date = _("video_info.unknown_date")
# Format duration # Format duration
duration = info.get("duration", 0) duration = info.get("duration", 0)
@@ -261,13 +262,13 @@ class VideoInfoMixin:
seconds = duration % 60 seconds = duration % 60
duration_str = f"{minutes}:{seconds:02d}" duration_str = f"{minutes}:{seconds:02d}"
# Update labels # Update labels with localized text
self.title_label.setText(info.get("title", "Unknown title")) self.title_label.setText(info.get("title", _("video_info.unknown_title")))
self.channel_label.setText(f"Channel: {info.get('uploader', 'Unknown channel')}") self.channel_label.setText(f"{_("video_info.channel")}: {info.get('uploader', _("video_info.unknown_channel"))}")
self.views_label.setText(f"Views: {formatted_views}") self.views_label.setText(f"{_("video_info.views")}: {formatted_views}")
self.like_count_label.setText(f"Likes: {formatted_likes}") self.like_count_label.setText(f"{_("video_info.likes")}: {formatted_likes}")
self.date_label.setText(f"Upload date: {formatted_date}") self.date_label.setText(f"{_("video_info.upload_date")}: {formatted_date}")
self.duration_label.setText(f"Duration: {duration_str}") self.duration_label.setText(f"{_("video_info.duration")}: {duration_str}")
def open_subtitle_dialog(self) -> None: def open_subtitle_dialog(self) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference. self = cast("YTSageApp", self) # for autocompletion and type inference.
@@ -341,11 +342,11 @@ class VideoInfoMixin:
# Update label text # Update label text
if count == 0: if count == 0:
self.selected_sponsorblock_label.setText("0 selected") self.selected_sponsorblock_label.setText(_("selection.none_selected"))
elif count == 1: elif count == 1:
self.selected_sponsorblock_label.setText("1 category selected") self.selected_sponsorblock_label.setText(_("selection.one_selected"))
else: else:
self.selected_sponsorblock_label.setText(f"{count} categories selected") self.selected_sponsorblock_label.setText(_("selection.count_selected", count=count))
# Update button property for styling # Update button property for styling
self.sponsorblock_select_btn.setProperty("sponsorBlockSelected", count > 0) self.sponsorblock_select_btn.setProperty("sponsorBlockSelected", count > 0)
+1
View File
@@ -79,6 +79,7 @@ class ConfigManager:
"auto_update_ytdlp": True, "auto_update_ytdlp": True,
"auto_update_frequency": "daily", "auto_update_frequency": "daily",
"last_update_check": 0, "last_update_check": 0,
"language": "en",
"cached_versions": { "cached_versions": {
"ytdlp": {"version": None, "path": None, "last_check": 0, "path_mtime": 0}, "ytdlp": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
"ffmpeg": {"version": None, "path": None, "last_check": 0, "path_mtime": 0}, "ffmpeg": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
+266
View File
@@ -0,0 +1,266 @@
"""
Localization Manager Module
==========================
This module provides centralized localization support for YTSage application.
It handles loading language files, switching languages, and retrieving localized strings.
Features
--------
- Thread-safe operations for getting localized text
- Fallback to English when translation is missing
- Support for multiple languages via JSON files
- Dynamic language switching without restart
- Nested key support with dot notation
Usage
-----
from src.utils.ytsage_localization import LocalizationManager
# Get localized text
text = LocalizationManager.get_text("download.ready")
button_text = LocalizationManager.get_text("buttons.download")
# Change language
LocalizationManager.set_language("es")
# Get available languages
languages = LocalizationManager.get_available_languages()
"""
import json
import threading
from pathlib import Path
from typing import Any, Dict
from src.utils.ytsage_logger import logger
class LocalizationManager:
"""
Thread-safe localization manager for YTSage.
Handles loading, caching, and retrieving localized strings from JSON language files.
"""
_lock = threading.RLock()
_current_language = "en"
_languages: Dict[str, Dict[str, Any]] = {}
_languages_dir = Path(__file__).parent.parent.parent / "languages"
# Fallback English strings embedded in code
_fallback_strings = {
"app": {
"title": "YTSage",
"version": "v{version}",
"ready": "Ready"
},
"buttons": {
"download": "Download",
"pause": "Pause",
"resume": "Resume",
"cancel": "Cancel",
"browse": "Browse",
"clear": "Clear",
"ok": "OK",
"apply": "Apply",
"close": "Close"
},
"dialogs": {
"custom_options": "Custom Options",
"settings": "Settings"
},
"tabs": {
"cookies": "Login with Cookies",
"custom_command": "Custom Command",
"proxy": "Proxy",
"language": "Language"
},
"language": {
"select_language": "Select Language:",
"current_language": "Current language: {language}",
"restart_required": "Language changes will take effect after restarting the application.",
"english": "English",
"spanish": "Español (Spanish)"
},
"download": {
"preparing": "🚀 Preparing your download...",
"completed": "✅ Download completed!",
"video_completed": "✅ Video download completed!",
"audio_completed": "✅ Audio download completed!",
"subtitle_completed": "✅ Subtitle download completed!",
"please_set_path": "Please set a download path using 'Change Path'",
"please_enter_url": "Please enter a URL",
"please_enter_url_and_path": "Please enter URL and set download path",
"please_select_format": "Please select a format"
},
"formats": {
"show_formats": "Show formats:"
}
}
@classmethod
def _ensure_languages_dir(cls) -> None:
"""Ensure the languages directory exists."""
cls._languages_dir.mkdir(exist_ok=True)
@classmethod
def _load_language(cls, language_code: str) -> Dict[str, Any]:
"""
Load a language file from disk.
Args:
language_code: The language code (e.g., 'en', 'es')
Returns:
Dictionary containing the language strings, or empty dict if not found
"""
language_file = cls._languages_dir / f"{language_code}.json"
if not language_file.exists():
logger.warning(f"Language file not found: {language_file}")
return {}
try:
with open(language_file, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError) as e:
logger.error(f"Failed to load language file {language_file}: {e}")
return {}
@classmethod
def _get_nested_value(cls, data: Dict[str, Any], key: str) -> Any:
"""
Get a nested value from dictionary using dot notation.
Args:
data: Dictionary to search in
key: Dot-separated key (e.g., "app.title")
Returns:
The value if found, None otherwise
"""
parts = key.split(".")
value = data
for part in parts:
if isinstance(value, dict) and part in value:
value = value[part]
else:
return None
return value
@classmethod
def get_text(cls, key: str, **kwargs) -> str:
"""
Get localized text for the given key.
Args:
key: Dot-separated key for the text (e.g., "app.title")
**kwargs: Format parameters for the text
Returns:
Localized text, with fallback to English if not found
"""
with cls._lock:
# Load current language if not cached
if cls._current_language not in cls._languages:
cls._languages[cls._current_language] = cls._load_language(cls._current_language)
# Try to get from current language
current_lang_data = cls._languages.get(cls._current_language, {})
text = cls._get_nested_value(current_lang_data, key)
# Fallback to embedded English strings
if text is None:
text = cls._get_nested_value(cls._fallback_strings, key)
# Final fallback to key itself
if text is None:
logger.warning(f"Localization key not found: {key}")
text = key
# Format the text with provided parameters
if kwargs and isinstance(text, str):
try:
text = text.format(**kwargs)
except (KeyError, ValueError) as e:
logger.warning(f"Failed to format localized text '{key}': {e}")
return str(text)
@classmethod
def set_language(cls, language_code: str) -> None:
"""
Set the current language.
Args:
language_code: The language code to set (e.g., 'en', 'es')
"""
with cls._lock:
if language_code != cls._current_language:
cls._current_language = language_code
# Clear cache to force reload
cls._languages.clear()
logger.info(f"Language set to: {language_code}")
@classmethod
def get_current_language(cls) -> str:
"""Get the current language code."""
return cls._current_language
@classmethod
def get_available_languages(cls) -> Dict[str, str]:
"""
Get available languages from the languages directory.
Returns:
Dictionary mapping language codes to display names
"""
cls._ensure_languages_dir()
available_languages = {"en": cls.get_text("language.english")}
# Scan for language files
for language_file in cls._languages_dir.glob("*.json"):
lang_code = language_file.stem
if lang_code != "en": # Skip English as it's already added
# Try to get language display name from the file
lang_data = cls._load_language(lang_code)
display_name = cls._get_nested_value(lang_data, "language.display_name")
if display_name:
available_languages[lang_code] = display_name
else:
# Fallback display name
available_languages[lang_code] = lang_code.upper()
return available_languages
@classmethod
def initialize(cls, language_code: str = "en") -> None:
"""
Initialize the localization system.
Args:
language_code: Initial language code to use
"""
with cls._lock:
cls._ensure_languages_dir()
cls.set_language(language_code)
logger.info(f"Localization system initialized with language: {language_code}")
# Convenience function for getting localized text
def _(key: str, **kwargs) -> str:
"""
Convenience function to get localized text.
Args:
key: Dot-separated key for the text
**kwargs: Format parameters
Returns:
Localized text
"""
return LocalizationManager.get_text(key, **kwargs)