diff --git a/languages/ar.json b/languages/ar.json index 0f21b5a..7304964 100644 --- a/languages/ar.json +++ b/languages/ar.json @@ -99,7 +99,8 @@ "cookies": "تسجيل الدخول بالكوكيز", "custom_command": "أمر مخصص", "proxy": "بروكسي", - "language": "اللغة" + "language": "اللغة", + "updater": "التحديث" }, "cookies": { "help_text": "اختر طريقة لتوفير ملفات تعريف الارتباط للمصادقة.\nيسمح هذا بتنزيل مقاطع الفيديو الخاصة وملفات الصوت عالية الجودة.", @@ -466,5 +467,24 @@ "redownload_confirm_title": "إعادة التنزيل؟", "redownload_confirm_message": "إعادة التنزيل؟\n\n{title}", "redownload_started": "بدأ التنزيل" + }, + "ffmpeg_updater": { + "title": "محدث FFmpeg", + "current_version": "الإصدار الحالي:", + "latest_version": "أحدث إصدار:", + "status_up_to_date": "✓ محدث", + "status_update_available": "⚠ تحديث متاح", + "status_not_installed": "✗ غير مثبت", + "status_checking": "🔄 جارٍ التحقق من التحديثات...", + "status_downloading": "⬇ جارٍ تنزيل FFmpeg...", + "status_installing": "⚙ جارٍ تثبيت FFmpeg...", + "status_verifying": "✓ جارٍ التحقق من السلامة...", + "check_updates": "التحقق من التحديثات", + "update_button": "تحديث FFmpeg", + "update_success": "تم تحديث FFmpeg بنجاح إلى الإصدار {version}!", + "update_failed": "فشل تحديث FFmpeg. يرجى المحاولة مرة أخرى.", + "check_failed": "فشل التحقق من التحديثات. يرجى التحقق من اتصال الإنترنت.", + "install_first": "FFmpeg غير مثبت. انقر فوق تحديث للتثبيت.", + "description": "حافظ على تحديث FFmpeg للحصول على أفضل أداء وتوافق." } } \ No newline at end of file diff --git a/languages/de.json b/languages/de.json index 631f928..b22ed1e 100644 --- a/languages/de.json +++ b/languages/de.json @@ -99,7 +99,8 @@ "cookies": "Mit Cookies anmelden", "custom_command": "Benutzerdefinierter Befehl", "proxy": "Proxy", - "language": "Sprache" + "language": "Sprache", + "updater": "Updater" }, "cookies": { "help_text": "Wählen Sie aus, wie Cookies für die Anmeldung bereitgestellt werden sollen.\nDies ermöglicht das Herunterladen privater Videos und hochwertiger Audio-Dateien.", @@ -466,5 +467,24 @@ "redownload_confirm_title": "Erneut Laden?", "redownload_confirm_message": "Erneut herunterladen?\n\n{title}", "redownload_started": "Download gestartet" + }, + "ffmpeg_updater": { + "title": "FFmpeg-Updater", + "current_version": "Aktuelle Version:", + "latest_version": "Neueste Version:", + "status_up_to_date": "✓ Auf dem neuesten Stand", + "status_update_available": "⚠ Update verfügbar", + "status_not_installed": "✗ Nicht installiert", + "status_checking": "🔄 Suche nach Updates...", + "status_downloading": "⬇ FFmpeg wird heruntergeladen...", + "status_installing": "⚙ FFmpeg wird installiert...", + "status_verifying": "✓ Integrität wird überprüft...", + "check_updates": "Nach Updates suchen", + "update_button": "FFmpeg aktualisieren", + "update_success": "FFmpeg erfolgreich auf Version {version} aktualisiert!", + "update_failed": "FFmpeg-Update fehlgeschlagen. Bitte versuchen Sie es erneut.", + "check_failed": "Update-Prüfung fehlgeschlagen. Bitte überprüfen Sie Ihre Internetverbindung.", + "install_first": "FFmpeg ist nicht installiert. Klicken Sie auf Aktualisieren, um zu installieren.", + "description": "Halten Sie FFmpeg auf dem neuesten Stand für beste Leistung und Kompatibilität." } } \ No newline at end of file diff --git a/languages/en.json b/languages/en.json index a88c79f..7abedb5 100644 --- a/languages/en.json +++ b/languages/en.json @@ -99,7 +99,8 @@ "cookies": "Login with Cookies", "custom_command": "Custom Command", "proxy": "Proxy", - "language": "Language" + "language": "Language", + "updater": "Updater" }, "cookies": { "help_text": "Choose how to provide cookies for logging in.\nThis allows downloading of private videos and premium quality audio.", @@ -466,5 +467,25 @@ "redownload_confirm_title": "Redownload Video?", "redownload_confirm_message": "Download this video again using the same settings?\n\n{title}", "redownload_started": "Redownload started" + }, + "ffmpeg_updater": { + "title": "FFmpeg Updater", + "current_version": "Current Version:", + "latest_version": "Latest Version:", + "status_up_to_date": "✓ Up to date", + "status_update_available": "⚠ Update available", + "status_not_installed": "✗ Not installed", + "status_idle": "Click 'Check for Updates' to get started", + "status_checking": "🔄 Checking for updates...", + "status_downloading": "⬇ Downloading FFmpeg...", + "status_installing": "⚙ Installing FFmpeg...", + "status_verifying": "✓ Verifying integrity...", + "check_updates": "Check for Updates", + "update_button": "Update FFmpeg", + "update_success": "FFmpeg successfully updated to version {version}!", + "update_failed": "Failed to update FFmpeg. Please try again.", + "check_failed": "Failed to check for updates. Please check your internet connection.", + "install_first": "FFmpeg is not installed. Click Update to install.", + "description": "Keep FFmpeg up to date for the best performance and compatibility." } } \ No newline at end of file diff --git a/languages/es.json b/languages/es.json index a3c4fa2..8aa02ac 100644 --- a/languages/es.json +++ b/languages/es.json @@ -66,7 +66,8 @@ "cookies": "Iniciar sesión con Cookies", "custom_command": "Comando Personalizado", "proxy": "Proxy", - "language": "Idioma" + "language": "Idioma", + "updater": "Actualizador" }, "cookies": { "help_text": "Elige cómo proporcionar cookies para iniciar sesión.\nEsto permite la descarga de videos privados y audio de calidad premium.", @@ -449,5 +450,24 @@ "redownload_confirm_title": "¿Descargar de Nuevo?", "redownload_confirm_message": "¿Descargar nuevamente?\n\n{title}", "redownload_started": "Descarga iniciada" + }, + "ffmpeg_updater": { + "title": "Actualizador de FFmpeg", + "current_version": "Versión Actual:", + "latest_version": "Última Versión:", + "status_up_to_date": "✓ Actualizado", + "status_update_available": "⚠ Actualización disponible", + "status_not_installed": "✗ No instalado", + "status_checking": "🔄 Verificando actualizaciones...", + "status_downloading": "⬇ Descargando FFmpeg...", + "status_installing": "⚙ Instalando FFmpeg...", + "status_verifying": "✓ Verificando integridad...", + "check_updates": "Buscar Actualizaciones", + "update_button": "Actualizar FFmpeg", + "update_success": "¡FFmpeg actualizado exitosamente a la versión {version}!", + "update_failed": "Error al actualizar FFmpeg. Por favor, inténtelo de nuevo.", + "check_failed": "Error al verificar actualizaciones. Por favor, verifique su conexión a internet.", + "install_first": "FFmpeg no está instalado. Haga clic en Actualizar para instalar.", + "description": "Mantenga FFmpeg actualizado para el mejor rendimiento y compatibilidad." } } \ No newline at end of file diff --git a/languages/fr.json b/languages/fr.json index a176167..caada1b 100644 --- a/languages/fr.json +++ b/languages/fr.json @@ -99,7 +99,8 @@ "cookies": "Se connecter avec des cookies", "custom_command": "Commande personnalisée", "proxy": "Proxy", - "language": "Langue" + "language": "Langue", + "updater": "Mise à jour" }, "cookies": { "help_text": "Choisissez comment fournir les cookies pour l'authentification.\nCeci permet de télécharger des vidéos privées et des fichiers audio de haute qualité.", @@ -466,5 +467,24 @@ "redownload_confirm_title": "Télécharger à Nouveau?", "redownload_confirm_message": "Télécharger à nouveau?\n\n{title}", "redownload_started": "Téléchargement démarré" + }, + "ffmpeg_updater": { + "title": "Mise à jour FFmpeg", + "current_version": "Version Actuelle:", + "latest_version": "Dernière Version:", + "status_up_to_date": "✓ À jour", + "status_update_available": "⚠ Mise à jour disponible", + "status_not_installed": "✗ Non installé", + "status_checking": "🔄 Vérification des mises à jour...", + "status_downloading": "⬇ Téléchargement de FFmpeg...", + "status_installing": "⚙ Installation de FFmpeg...", + "status_verifying": "✓ Vérification de l'intégrité...", + "check_updates": "Vérifier les mises à jour", + "update_button": "Mettre à jour FFmpeg", + "update_success": "FFmpeg mis à jour avec succès vers la version {version}!", + "update_failed": "Échec de la mise à jour de FFmpeg. Veuillez réessayer.", + "check_failed": "Échec de la vérification des mises à jour. Vérifiez votre connexion Internet.", + "install_first": "FFmpeg n'est pas installé. Cliquez sur Mettre à jour pour installer.", + "description": "Maintenez FFmpeg à jour pour les meilleures performances et compatibilité." } } \ No newline at end of file diff --git a/languages/hi.json b/languages/hi.json index d6e5dbd..d44230e 100644 --- a/languages/hi.json +++ b/languages/hi.json @@ -99,7 +99,8 @@ "cookies": "कुकीज़ के साथ लॉगिन", "custom_command": "कस्टम कमांड", "proxy": "प्रॉक्सी", - "language": "भाषा" + "language": "भाषा", + "updater": "अपडेटर" }, "cookies": { "help_text": "प्रमाणीकरण के लिए कुकीज़ प्रदान करने का तरीका चुनें।\nयह निजी वीडियो और उच्च गुणवत्ता वाली ऑडियो फ़ाइलें डाउनलोड करने की अनुमति देता है।", @@ -466,5 +467,24 @@ "redownload_confirm_title": "फिर से डाउनलोड करें?", "redownload_confirm_message": "फिर से डाउनलोड करें?\n\n{title}", "redownload_started": "डाउनलोड शुरू हुआ" + }, + "ffmpeg_updater": { + "title": "FFmpeg अपडेटर", + "current_version": "वर्तमान संस्करण:", + "latest_version": "नवीनतम संस्करण:", + "status_up_to_date": "✓ अप टू डेट", + "status_update_available": "⚠ अपडेट उपलब्ध", + "status_not_installed": "✗ इंस्टॉल नहीं है", + "status_checking": "🔄 अपडेट जांच रहे हैं...", + "status_downloading": "⬇ FFmpeg डाउनलोड हो रहा है...", + "status_installing": "⚙ FFmpeg इंस्टॉल हो रहा है...", + "status_verifying": "✓ अखंडता सत्यापित कर रहे हैं...", + "check_updates": "अपडेट जांचें", + "update_button": "FFmpeg अपडेट करें", + "update_success": "FFmpeg सफलतापूर्वक संस्करण {version} में अपडेट किया गया!", + "update_failed": "FFmpeg अपडेट विफल रहा। कृपया पुनः प्रयास करें।", + "check_failed": "अपडेट जांच विफल रही। कृपया अपना इंटरनेट कनेक्शन जांचें।", + "install_first": "FFmpeg इंस्टॉल नहीं है। इंस्टॉल करने के लिए अपडेट पर क्लिक करें।", + "description": "सर्वोत्तम प्रदर्शन और संगतता के लिए FFmpeg को अप टू डेट रखें।" } } \ No newline at end of file diff --git a/languages/id.json b/languages/id.json index a17c2ba..41d05a5 100644 --- a/languages/id.json +++ b/languages/id.json @@ -99,7 +99,8 @@ "cookies": "Masuk dengan cookies", "custom_command": "Perintah khusus", "proxy": "Proxy", - "language": "Bahasa" + "language": "Bahasa", + "updater": "Pembaruan" }, "cookies": { "help_text": "Pilih cara memberikan cookies untuk autentikasi.\nIni memungkinkan pengunduhan video pribadi dan file audio berkualitas tinggi.", @@ -466,5 +467,24 @@ "redownload_confirm_title": "Unduh Lagi?", "redownload_confirm_message": "Unduh lagi?\n\n{title}", "redownload_started": "Unduhan dimulai" + }, + "ffmpeg_updater": { + "title": "Pembaruan FFmpeg", + "current_version": "Versi Saat Ini:", + "latest_version": "Versi Terbaru:", + "status_up_to_date": "✓ Terbaru", + "status_update_available": "⚠ Pembaruan tersedia", + "status_not_installed": "✗ Tidak terinstal", + "status_checking": "🔄 Memeriksa pembaruan...", + "status_downloading": "⬇ Mengunduh FFmpeg...", + "status_installing": "⚙ Menginstal FFmpeg...", + "status_verifying": "✓ Memverifikasi integritas...", + "check_updates": "Periksa Pembaruan", + "update_button": "Perbarui FFmpeg", + "update_success": "FFmpeg berhasil diperbarui ke versi {version}!", + "update_failed": "Gagal memperbarui FFmpeg. Silakan coba lagi.", + "check_failed": "Gagal memeriksa pembaruan. Periksa koneksi internet Anda.", + "install_first": "FFmpeg tidak terinstal. Klik Perbarui untuk menginstal.", + "description": "Jaga FFmpeg tetap terbaru untuk performa dan kompatibilitas terbaik." } } \ No newline at end of file diff --git a/languages/it.json b/languages/it.json index 8d3fbfb..d5e2b93 100644 --- a/languages/it.json +++ b/languages/it.json @@ -99,7 +99,8 @@ "cookies": "Accedi con i cookie", "custom_command": "Comando personalizzato", "proxy": "Proxy", - "language": "Lingua" + "language": "Lingua", + "updater": "Aggiornamento" }, "cookies": { "help_text": "Seleziona un metodo per fornire i cookie per l'autenticazione.\nCiò consente di scaricare video privati e file audio di alta qualità.", @@ -466,5 +467,24 @@ "redownload_confirm_title": "Scaricare di Nuovo?", "redownload_confirm_message": "Scaricare di nuovo?\n\n{title}", "redownload_started": "Download avviato" + }, + "ffmpeg_updater": { + "title": "Aggiornamento FFmpeg", + "current_version": "Versione corrente:", + "latest_version": "Ultima versione:", + "status_up_to_date": "✓ Aggiornato", + "status_update_available": "⚠ Aggiornamento disponibile", + "status_not_installed": "✗ Non installato", + "status_checking": "🔄 Verifica aggiornamenti...", + "status_downloading": "⬇ Download di FFmpeg...", + "status_installing": "⚙ Installazione di FFmpeg...", + "status_verifying": "✓ Verifica integrità...", + "check_updates": "Verifica aggiornamenti", + "update_button": "Aggiorna FFmpeg", + "update_success": "FFmpeg aggiornato con successo alla versione {version}!", + "update_failed": "Aggiornamento di FFmpeg non riuscito. Riprova.", + "check_failed": "Verifica aggiornamenti non riuscita. Controlla la connessione Internet.", + "install_first": "FFmpeg non è installato. Clicca Aggiorna per installare.", + "description": "Mantieni FFmpeg aggiornato per le migliori prestazioni e compatibilità." } } \ No newline at end of file diff --git a/languages/ja.json b/languages/ja.json index 0c00bc4..c783851 100644 --- a/languages/ja.json +++ b/languages/ja.json @@ -99,7 +99,8 @@ "cookies": "Cookieでログイン", "custom_command": "カスタムコマンド", "proxy": "プロキシ", - "language": "言語" + "language": "言語", + "updater": "アップデーター" }, "cookies": { "help_text": "認証用のCookieを提供する方法を選択してください。\nこれにより、プライベート動画や高品質の音声ファイルをダウンロードできます。", @@ -466,5 +467,24 @@ "redownload_confirm_title": "再ダウンロード?", "redownload_confirm_message": "再ダウンロードしますか?\n\n{title}", "redownload_started": "ダウンロード開始" + }, + "ffmpeg_updater": { + "title": "FFmpegアップデーター", + "current_version": "現在のバージョン:", + "latest_version": "最新バージョン:", + "status_up_to_date": "✓ 最新", + "status_update_available": "⚠ 更新可能", + "status_not_installed": "✗ 未インストール", + "status_checking": "🔄 更新を確認中...", + "status_downloading": "⬇ FFmpegをダウンロード中...", + "status_installing": "⚙ FFmpegをインストール中...", + "status_verifying": "✓ 整合性を検証中...", + "check_updates": "更新を確認", + "update_button": "FFmpegを更新", + "update_success": "FFmpegがバージョン{version}に更新されました!", + "update_failed": "FFmpegの更新に失敗しました。再試行してください。", + "check_failed": "更新の確認に失敗しました。インターネット接続を確認してください。", + "install_first": "FFmpegがインストールされていません。インストールするには「更新」をクリックしてください。", + "description": "最高のパフォーマンスと互換性のためにFFmpegを最新の状態に保ってください。" } } \ No newline at end of file diff --git a/languages/pl.json b/languages/pl.json index 55617fb..f35e94c 100644 --- a/languages/pl.json +++ b/languages/pl.json @@ -99,7 +99,8 @@ "cookies": "Zaloguj za pomocą ciasteczek", "custom_command": "Polecenie niestandardowe", "proxy": "Proxy", - "language": "Język" + "language": "Język", + "updater": "Aktualizator" }, "cookies": { "help_text": "Wybierz sposób dostarczania ciasteczek do uwierzytelniania.\nUmożliwia to pobieranie prywatnych filmów i wysokiej jakości plików audio.", @@ -466,5 +467,24 @@ "redownload_confirm_title": "Pobrać Ponownie?", "redownload_confirm_message": "Pobrać ponownie?\n\n{title}", "redownload_started": "Rozpoczęto pobieranie" + }, + "ffmpeg_updater": { + "title": "Aktualizator FFmpeg", + "current_version": "Obecna wersja:", + "latest_version": "Najnowsza wersja:", + "status_up_to_date": "✓ Aktualne", + "status_update_available": "⚠ Dostępna aktualizacja", + "status_not_installed": "✗ Nie zainstalowano", + "status_checking": "🔄 Sprawdzanie aktualizacji...", + "status_downloading": "⬇ Pobieranie FFmpeg...", + "status_installing": "⚙ Instalowanie FFmpeg...", + "status_verifying": "✓ Weryfikacja integralności...", + "check_updates": "Sprawdź aktualizacje", + "update_button": "Aktualizuj FFmpeg", + "update_success": "FFmpeg zaktualizowano pomyślnie do wersji {version}!", + "update_failed": "Nie udało się zaktualizować FFmpeg. Spróbuj ponownie.", + "check_failed": "Nie udało się sprawdzić aktualizacji. Sprawdź połączenie internetowe.", + "install_first": "FFmpeg nie jest zainstalowany. Kliknij Aktualizuj, aby zainstalować.", + "description": "Utrzymuj FFmpeg w aktualnej wersji dla najlepszej wydajności i kompatybilności." } } \ No newline at end of file diff --git a/languages/pt.json b/languages/pt.json index db6f3f7..095020b 100644 --- a/languages/pt.json +++ b/languages/pt.json @@ -99,7 +99,8 @@ "cookies": "Entrar com Cookies", "custom_command": "Comando Personalizado", "proxy": "Proxy", - "language": "Idioma" + "language": "Idioma", + "updater": "Atualizador" }, "cookies": { "help_text": "Escolha como fornecer cookies para fazer login.\nIsso permite baixar vídeos privados e áudio de qualidade premium.", @@ -466,5 +467,24 @@ "redownload_confirm_title": "Baixar Novamente?", "redownload_confirm_message": "Baixar novamente?\n\n{title}", "redownload_started": "Download iniciado" + }, + "ffmpeg_updater": { + "title": "Atualizador FFmpeg", + "current_version": "Versão Atual:", + "latest_version": "Última Versão:", + "status_up_to_date": "✓ Atualizado", + "status_update_available": "⚠ Atualização disponível", + "status_not_installed": "✗ Não instalado", + "status_checking": "🔄 Verificando atualizações...", + "status_downloading": "⬇ Baixando FFmpeg...", + "status_installing": "⚙ Instalando FFmpeg...", + "status_verifying": "✓ Verificando integridade...", + "check_updates": "Verificar Atualizações", + "update_button": "Atualizar FFmpeg", + "update_success": "FFmpeg atualizado com sucesso para a versão {version}!", + "update_failed": "Falha ao atualizar FFmpeg. Tente novamente.", + "check_failed": "Falha ao verificar atualizações. Verifique sua conexão com a Internet.", + "install_first": "FFmpeg não está instalado. Clique em Atualizar para instalar.", + "description": "Mantenha o FFmpeg atualizado para melhor desempenho e compatibilidade." } } \ No newline at end of file diff --git a/languages/ru.json b/languages/ru.json index 9df999c..6750f94 100644 --- a/languages/ru.json +++ b/languages/ru.json @@ -99,7 +99,8 @@ "cookies": "Войти через Cookie", "custom_command": "Пользовательская команда", "proxy": "Прокси", - "language": "Язык" + "language": "Язык", + "updater": "Обновление" }, "cookies": { "help_text": "Выберите способ предоставления cookie для входа в систему.\nЭто позволяет загружать приватные видео и аудио премиум качества.", @@ -466,5 +467,24 @@ "redownload_confirm_title": "Загрузить Снова?", "redownload_confirm_message": "Загрузить снова?\n\n{title}", "redownload_started": "Загрузка начата" + }, + "ffmpeg_updater": { + "title": "Обновление FFmpeg", + "current_version": "Текущая версия:", + "latest_version": "Последняя версия:", + "status_up_to_date": "✓ Обновлено", + "status_update_available": "⚠ Доступно обновление", + "status_not_installed": "✗ Не установлено", + "status_checking": "🔄 Проверка обновлений...", + "status_downloading": "⬇ Загрузка FFmpeg...", + "status_installing": "⚙ Установка FFmpeg...", + "status_verifying": "✓ Проверка целостности...", + "check_updates": "Проверить обновления", + "update_button": "Обновить FFmpeg", + "update_success": "FFmpeg успешно обновлен до версии {version}!", + "update_failed": "Не удалось обновить FFmpeg. Попробуйте еще раз.", + "check_failed": "Не удалось проверить обновления. Проверьте подключение к Интернету.", + "install_first": "FFmpeg не установлен. Нажмите Обновить для установки.", + "description": "Поддерживайте FFmpeg в актуальном состоянии для лучшей производительности и совместимости." } } \ No newline at end of file diff --git a/languages/tr.json b/languages/tr.json index df57048..37d999a 100644 --- a/languages/tr.json +++ b/languages/tr.json @@ -99,7 +99,8 @@ "cookies": "Çerezlerle giriş yap", "custom_command": "Özel komut", "proxy": "Proxy", - "language": "Dil" + "language": "Dil", + "updater": "Güncelleyici" }, "cookies": { "help_text": "Kimlik doğrulama için çerezlerin nasıl sağlanacağını seçin.\nBu, özel videoları ve yüksek kaliteli ses dosyalarını indirmeyi sağlar.", @@ -466,5 +467,24 @@ "redownload_confirm_title": "Tekrar İndir?", "redownload_confirm_message": "Tekrar indir?\n\n{title}", "redownload_started": "İndirme başladı" + }, + "ffmpeg_updater": { + "title": "FFmpeg Güncelleyici", + "current_version": "Mevcut Sürüm:", + "latest_version": "En Son Sürüm:", + "status_up_to_date": "✓ Güncel", + "status_update_available": "⚠ Güncelleme mevcut", + "status_not_installed": "✗ Yüklü değil", + "status_checking": "🔄 Güncellemeler kontrol ediliyor...", + "status_downloading": "⬇ FFmpeg indiriliyor...", + "status_installing": "⚙ FFmpeg yükleniyor...", + "status_verifying": "✓ Bütünlük doğrulanıyor...", + "check_updates": "Güncellemeleri Kontrol Et", + "update_button": "FFmpeg'i Güncelle", + "update_success": "FFmpeg başarıyla {version} sürümüne güncellendi!", + "update_failed": "FFmpeg güncellemesi başarısız oldu. Lütfen tekrar deneyin.", + "check_failed": "Güncelleme kontrolü başarısız oldu. İnternet bağlantınızı kontrol edin.", + "install_first": "FFmpeg yüklü değil. Yüklemek için Güncelle'ye tıklayın.", + "description": "En iyi performans ve uyumluluk için FFmpeg'i güncel tutun." } } \ No newline at end of file diff --git a/languages/zh.json b/languages/zh.json index 2dff995..74c0937 100644 --- a/languages/zh.json +++ b/languages/zh.json @@ -99,7 +99,8 @@ "cookies": "使用 Cookie 登录", "custom_command": "自定义命令", "proxy": "代理", - "language": "语言" + "language": "语言", + "updater": "更新器" }, "cookies": { "help_text": "选择如何提供 cookie 来登录。\n这允许下载私人视频和高级质量音频。", @@ -466,5 +467,24 @@ "redownload_confirm_title": "重新下载?", "redownload_confirm_message": "重新下载?\n\n{title}", "redownload_started": "已开始下载" + }, + "ffmpeg_updater": { + "title": "FFmpeg 更新器", + "current_version": "当前版本:", + "latest_version": "最新版本:", + "status_up_to_date": "✓ 已是最新", + "status_update_available": "⚠ 有可用更新", + "status_not_installed": "✗ 未安装", + "status_checking": "🔄 正在检查更新...", + "status_downloading": "⬇ 正在下载 FFmpeg...", + "status_installing": "⚙ 正在安装 FFmpeg...", + "status_verifying": "✓ 正在验证完整性...", + "check_updates": "检查更新", + "update_button": "更新 FFmpeg", + "update_success": "FFmpeg 已成功更新到版本 {version}!", + "update_failed": "更新 FFmpeg 失败。请重试。", + "check_failed": "检查更新失败。请检查您的网络连接。", + "install_first": "FFmpeg 未安装。点击更新以安装。", + "description": "保持 FFmpeg 更新以获得最佳性能和兼容性。" } } \ No newline at end of file diff --git a/src/core/ytsage_ffmpeg.py b/src/core/ytsage_ffmpeg.py index 8befa7d..57e29c4 100644 --- a/src/core/ytsage_ffmpeg.py +++ b/src/core/ytsage_ffmpeg.py @@ -85,9 +85,26 @@ def verify_sha256(file_path, expected_hash_url) -> bool: def get_ffmpeg_install_path() -> Path: - """Get the FFmpeg installation path.""" + """ + Get the FFmpeg installation path. + For Windows, tries to find the latest essentials build dynamically. + """ if OS_NAME == "Windows": - return Path(os.getenv("LOCALAPPDATA")) / "ffmpeg" / "ffmpeg-7.1.1-full_build" / "bin" # type: ignore + ffmpeg_base = Path(os.getenv("LOCALAPPDATA")) / "ffmpeg" # type: ignore + + # If the directory exists, look for any ffmpeg-*-essentials_build folder + if ffmpeg_base.exists(): + # Find all directories matching the pattern + essentials_dirs = list(ffmpeg_base.glob("ffmpeg-*-essentials_build")) + if essentials_dirs: + # Sort by name (which includes version) and take the latest + latest_dir = sorted(essentials_dirs, reverse=True)[0] + bin_dir = latest_dir / "bin" + if bin_dir.exists(): + return bin_dir + + # Fallback: return default path (even if it doesn't exist yet) + return ffmpeg_base / "ffmpeg-essentials_build" / "bin" elif OS_NAME == "Darwin": paths = ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin"] @@ -99,6 +116,7 @@ def get_ffmpeg_install_path() -> Path: return Path("/usr/bin") # Standard Linux path + def get_ffmpeg_path() -> str | Path: """ Get the FFmpeg executable path, either from PATH or installation directory. @@ -175,106 +193,29 @@ def check_ffmpeg_installed() -> bool: return False -def install_ffmpeg_windows() -> bool: +def install_ffmpeg_windows(progress_callback=None) -> bool: """Install FFmpeg on Windows using 7z method primarily, with zip as fallback.""" - ffmpeg_path = get_ffmpeg_install_path() - # Check if already installed if check_ffmpeg_installed(): logger.info("FFmpeg is already installed!") + if progress_callback: + progress_callback("✅ FFmpeg is already installed!") return True try: - # Define variables - prioritize 7z version - # ffmpeg variables moved to src\utils\ytsage_constants.py - extract_dir = Path(os.getenv("LOCALAPPDATA")) / "ffmpeg" # type: ignore - full_build_dir = extract_dir / "ffmpeg-7.1.1-full_build" - bin_dir = full_build_dir / "bin" - - # Create extraction directory if it doesn't exist - extract_dir.mkdir(exist_ok=True) - - # Try 7z method first (smaller size) - use_7zip = check_7zip_installed() - if use_7zip: - logger.info("Using 7-Zip method (smaller download size)...") - temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".7z").name - - # Download 7z file - if not download_file( - FFMPEG_7Z_DOWNLOAD_URL, - temp_file, - progress_callback=lambda msg: logger.debug(msg), - ): - logger.error("Failed to download 7z file, trying zip fallback...") - use_7zip = False - else: - # Verify SHA-256 hash for 7z file - if verify_sha256(temp_file, FFMPEG_7Z_SHA256_URL): - logger.info("Extracting FFmpeg components from 7z archive...") - try: - subprocess.run( - ["7z", "x", temp_file, f"-o{extract_dir}", "-y"], - creationflags=SUBPROCESS_CREATIONFLAGS, - timeout=300, - ) # 5-minute timeout - except Exception as e: - logger.exception(f"7z extraction failed: {e}, trying zip fallback...") - use_7zip = False - else: - logger.error("SHA-256 verification failed for 7z file, trying zip fallback...") - use_7zip = False - - # Fallback to zip method if 7z failed or not available - if not use_7zip: - logger.info("Using ZIP method as fallback...") - temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip").name - - # Download zip file - if not download_file( - FFMPEG_ZIP_DOWNLOAD_URL, - temp_file, - progress_callback=lambda msg: logger.debug(msg), - ): - logger.exception("Failed to download FFmpeg (both 7z and zip methods failed)") - return False - - logger.info("Extracting FFmpeg components from zip archive...") - try: - import zipfile - - with zipfile.ZipFile(temp_file, "r") as zip_ref: - zip_ref.extractall(extract_dir) - except Exception as e: - logger.exception(f"Extraction failed: {e}") - return False - - logger.info("Configuring system paths...") - # Add to System Path - user_path = os.environ.get("PATH", "") - if str(bin_dir) not in user_path.split(os.pathsep): - subprocess.run( - ["setx", "PATH", f"{user_path};{bin_dir}"], - creationflags=SUBPROCESS_CREATIONFLAGS, - ) - os.environ["PATH"] = f"{user_path};{bin_dir}" - - # Clean up - try: - Path(temp_file).unlink(missing_ok=True) - except Exception: - pass # Ignore cleanup errors - - # Verify installation - if not check_ffmpeg_installed(): - logger.error("FFmpeg installation verification failed") - return False - - logger.info("FFmpeg installation completed successfully!") - return True + # Import the updater for installation + from src.core.ytsage_ffmpeg_updater import update_ffmpeg_windows + + # Use the updater to install the latest version + logger.info("Installing FFmpeg using the updater module...") + if progress_callback: + progress_callback("📦 Installing FFmpeg...") + return update_ffmpeg_windows(progress_callback=progress_callback) except Exception as e: logger.exception(f"Error installing FFmpeg: {e}") + if progress_callback: + progress_callback(f"❌ Error installing FFmpeg: {e}") return False @@ -348,14 +289,16 @@ def install_ffmpeg_linux() -> bool: return False -def auto_install_ffmpeg() -> bool: +def auto_install_ffmpeg(progress_callback=None) -> bool: """Automatically install FFmpeg based on the operating system.""" if OS_NAME == "Windows": - return install_ffmpeg_windows() + return install_ffmpeg_windows(progress_callback=progress_callback) elif OS_NAME == "Darwin": return install_ffmpeg_macos() elif OS_NAME == "Linux": return install_ffmpeg_linux() else: logger.info(f"Unsupported operating system: {OS_NAME}") + if progress_callback: + progress_callback(f"❌ Unsupported operating system: {OS_NAME}") return False diff --git a/src/core/ytsage_ffmpeg_updater.py b/src/core/ytsage_ffmpeg_updater.py new file mode 100644 index 0000000..814e5bc --- /dev/null +++ b/src/core/ytsage_ffmpeg_updater.py @@ -0,0 +1,413 @@ +""" +FFmpeg updater module for YTSage application. +Handles checking for updates and updating FFmpeg to the latest Essentials build. +""" + +import hashlib +import os +import re +import shutil +import subprocess +import tempfile +from pathlib import Path +from typing import Optional, Tuple, Callable + +import requests + +from src.utils.ytsage_logger import logger +from src.utils.ytsage_constants import ( + FFMPEG_7Z_DOWNLOAD_URL, + FFMPEG_7Z_SHA256_URL, + FFMPEG_7Z_VERSION_URL, + FFMPEG_ZIP_DOWNLOAD_URL, + FFMPEG_ZIP_SHA256_URL, + FFMPEG_ZIP_VERSION_URL, + OS_NAME, + SUBPROCESS_CREATIONFLAGS, +) +from src.core.ytsage_ffmpeg import ( + check_7zip_installed, + download_file, + get_file_sha256, + verify_sha256, + get_ffmpeg_install_path, +) +from src.core.ytsage_utils import get_ffmpeg_version_direct + + +def get_latest_ffmpeg_version() -> Optional[str]: + """ + Fetch the latest FFmpeg version from the version URL. + + Returns: + str: Version string (e.g., "8.0") or None if fetch failed + """ + try: + # Try 7z version URL first + response = requests.get(FFMPEG_7Z_VERSION_URL, timeout=10) + response.raise_for_status() + version = response.text.strip() + + # Validate version format (should be something like "8.0" or "7.1.1") + if re.match(r'^\d+\.\d+(\.\d+)?$', version): + logger.info(f"Latest FFmpeg version: {version}") + return version + else: + logger.warning(f"Unexpected version format: {version}") + return None + + except requests.RequestException as e: + logger.error(f"Failed to fetch latest FFmpeg version: {e}") + return None + except Exception as e: + logger.exception(f"Unexpected error fetching FFmpeg version: {e}") + return None + + +def parse_version(version_str: str) -> Tuple[int, ...]: + """ + Parse version string into tuple of integers for comparison. + + Args: + version_str: Version string like "8.0" or "7.1.1" + + Returns: + Tuple of integers (e.g., (8, 0) or (7, 1, 1)) + """ + try: + # Extract version numbers from string + # Handles formats like "8.0", "7.1.1", "ffmpeg version 8.0", etc. + match = re.search(r'(\d+\.\d+(?:\.\d+)?)', version_str) + if match: + version_str = match.group(1) + + parts = version_str.split('.') + return tuple(int(p) for p in parts) + except (ValueError, AttributeError): + logger.warning(f"Could not parse version: {version_str}") + return (0,) + + +def compare_versions(current: str, latest: str) -> bool: + """ + Compare two version strings. + + Args: + current: Current version string + latest: Latest version string + + Returns: + True if update is needed (latest > current), False otherwise + """ + try: + current_tuple = parse_version(current) + latest_tuple = parse_version(latest) + + logger.info(f"Comparing versions - Current: {current_tuple}, Latest: {latest_tuple}") + + # Pad shorter version with zeros for comparison + max_len = max(len(current_tuple), len(latest_tuple)) + current_padded = current_tuple + (0,) * (max_len - len(current_tuple)) + latest_padded = latest_tuple + (0,) * (max_len - len(latest_tuple)) + + return latest_padded > current_padded + except Exception as e: + logger.exception(f"Error comparing versions: {e}") + return False + + +def check_ffmpeg_update_available() -> Tuple[bool, str, str]: + """ + Check if FFmpeg update is available. + + Returns: + Tuple of (update_available, current_version, latest_version) + - update_available: True if update is needed + - current_version: Currently installed version or "Not installed" + - latest_version: Latest available version or "Unknown" + """ + try: + # Get current version + current_version = get_ffmpeg_version_direct() + if current_version in ["Not found", "Error getting version", "Unknown version"]: + current_version = "Not installed" + + # Get latest version + latest_version = get_latest_ffmpeg_version() + if latest_version is None: + latest_version = "Unknown" + return False, current_version, latest_version + + # If not installed, update is needed + if current_version == "Not installed": + return True, current_version, latest_version + + # Compare versions + update_needed = compare_versions(current_version, latest_version) + + return update_needed, current_version, latest_version + + except Exception as e: + logger.exception(f"Error checking for FFmpeg updates: {e}") + return False, "Error", "Error" + + +def get_ffmpeg_extract_dir() -> Path: + """Get the directory where FFmpeg should be extracted.""" + if OS_NAME == "Windows": + return Path(os.getenv("LOCALAPPDATA")) / "ffmpeg" # type: ignore + elif OS_NAME == "Darwin": + return Path.home() / "Library" / "Application Support" / "ffmpeg" + else: + return Path.home() / ".local" / "share" / "ffmpeg" + + +def find_ffmpeg_bin_dir(extract_dir: Path, version: str) -> Optional[Path]: + """ + Find the FFmpeg bin directory after extraction. + + Args: + extract_dir: Directory where FFmpeg was extracted + version: Version string (e.g., "8.0") + + Returns: + Path to bin directory or None if not found + """ + # Expected pattern: ffmpeg-{version}-essentials_build + pattern = f"ffmpeg-{version}-essentials_build" + + # Look for the directory + for item in extract_dir.iterdir(): + if item.is_dir() and item.name.startswith(f"ffmpeg-{version}"): + bin_dir = item / "bin" + if bin_dir.exists(): + logger.info(f"Found FFmpeg bin directory: {bin_dir}") + return bin_dir + + logger.warning(f"Could not find FFmpeg bin directory with pattern: {pattern}") + return None + + +def update_ffmpeg_windows(progress_callback: Optional[Callable[[str], None]] = None) -> bool: + """ + Update FFmpeg on Windows to the latest Essentials build. + + Args: + progress_callback: Optional callback function for progress updates + + Returns: + True if update was successful, False otherwise + """ + if OS_NAME != "Windows": + logger.error("This update method is only for Windows") + return False + + try: + # Track last progress percentage to avoid spam + last_percent = -1 + + def report_progress(msg: str, force: bool = False): + """Helper to report progress (always logged, callback only if force=True or status message).""" + logger.info(msg) + + # Only send callback for non-percentage messages or when forced + if progress_callback and (force or not msg.strip().endswith('%')): + progress_callback(msg) + + def download_progress_wrapper(msg: str): + """Wrapper for download progress that throttles percentage updates.""" + nonlocal last_percent + + # Extract percentage if present + percent_match = re.search(r'(\d+)%', msg) + if percent_match: + current_percent = int(percent_match.group(1)) + # Only report every 10% and to logger only + if current_percent != last_percent and current_percent % 10 == 0: + logger.info(msg) + if progress_callback: + progress_callback(msg) + last_percent = current_percent + else: + # Non-percentage messages get reported normally + report_progress(msg, force=True) + + # Get latest version + report_progress("🔍 Checking latest FFmpeg version...", force=True) + latest_version = get_latest_ffmpeg_version() + if not latest_version: + report_progress("❌ Failed to determine latest version", force=True) + return False + + report_progress(f"📦 Latest version: {latest_version}", force=True) + + extract_dir = get_ffmpeg_extract_dir() + extract_dir.mkdir(exist_ok=True) + + # Try 7z method first (smaller size) + use_7zip = check_7zip_installed() + success = False + + if use_7zip: + report_progress("⚡ Using 7-Zip method (smaller download)...", force=True) + temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".7z").name + + # Download 7z file + report_progress("⬇ Downloading FFmpeg (7z)...", force=True) + last_percent = -1 # Reset for this download + if download_file( + FFMPEG_7Z_DOWNLOAD_URL, + temp_file, + progress_callback=download_progress_wrapper, + ): + # Verify SHA-256 hash + report_progress("🔐 Verifying download integrity...", force=True) + if verify_sha256(temp_file, FFMPEG_7Z_SHA256_URL): + report_progress("⚙ Extracting FFmpeg...", force=True) + try: + subprocess.run( + ["7z", "x", temp_file, f"-o{extract_dir}", "-y"], + creationflags=SUBPROCESS_CREATIONFLAGS, + timeout=300, + check=True, + ) + success = True + except Exception as e: + logger.exception(f"7z extraction failed: {e}") + report_progress(f"❌ 7z extraction failed, trying zip fallback...", force=True) + else: + report_progress("❌ SHA-256 verification failed, trying zip fallback...", force=True) + + # Clean up temp file + try: + Path(temp_file).unlink(missing_ok=True) + except Exception: + pass + + # Fallback to zip method + if not success: + report_progress("📦 Using ZIP method...", force=True) + temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip").name + + # Download zip file + report_progress("⬇ Downloading FFmpeg (zip)...", force=True) + last_percent = -1 # Reset for this download + if not download_file( + FFMPEG_ZIP_DOWNLOAD_URL, + temp_file, + progress_callback=download_progress_wrapper, + ): + report_progress("❌ Failed to download FFmpeg", force=True) + return False + + # Verify SHA-256 hash for zip + report_progress("🔐 Verifying download integrity...", force=True) + if not verify_sha256(temp_file, FFMPEG_ZIP_SHA256_URL): + report_progress("⚠️ SHA-256 verification failed, proceeding anyway...", force=True) + + report_progress("⚙ Extracting FFmpeg...", force=True) + try: + import zipfile + with zipfile.ZipFile(temp_file, "r") as zip_ref: + zip_ref.extractall(extract_dir) + success = True + except Exception as e: + logger.exception(f"Extraction failed: {e}") + report_progress(f"❌ Extraction failed: {e}", force=True) + return False + finally: + # Clean up temp file + try: + Path(temp_file).unlink(missing_ok=True) + except Exception: + pass + + if not success: + report_progress("❌ Both 7z and zip methods failed", force=True) + return False + + # Find the bin directory + report_progress("🔍 Locating FFmpeg binaries...", force=True) + bin_dir = find_ffmpeg_bin_dir(extract_dir, latest_version) + if not bin_dir: + report_progress("❌ Could not locate FFmpeg bin directory", force=True) + return False + + # Add to System Path + report_progress("🔧 Configuring system paths...", force=True) + user_path = os.environ.get("PATH", "") + + # Remove old FFmpeg paths from PATH + path_parts = user_path.split(os.pathsep) + cleaned_paths = [p for p in path_parts if "ffmpeg" not in p.lower() or str(bin_dir) in p] + + # Add new path if not already present + if str(bin_dir) not in cleaned_paths: + cleaned_paths.insert(0, str(bin_dir)) + + new_path = os.pathsep.join(cleaned_paths) + + try: + subprocess.run( + ["setx", "PATH", new_path], + creationflags=SUBPROCESS_CREATIONFLAGS, + timeout=30, + check=True, + ) + os.environ["PATH"] = new_path + except Exception as e: + logger.warning(f"Failed to update PATH permanently: {e}") + # Still update for current session + os.environ["PATH"] = new_path + + # Verify installation + report_progress("✅ Verifying installation...", force=True) + new_version = get_ffmpeg_version_direct() + if new_version not in ["Not found", "Error getting version", "Unknown version"]: + report_progress(f"✅ FFmpeg successfully updated to version {new_version}!", force=True) + return True + else: + report_progress("⚠️ Update completed but verification failed", force=True) + return True # Still return True as files were extracted + + except Exception as e: + logger.exception(f"Error updating FFmpeg: {e}") + if progress_callback: + progress_callback(f"❌ Error: {e}") + return False + + +def update_ffmpeg(progress_callback: Optional[Callable[[str], None]] = None) -> bool: + """ + Update FFmpeg based on the operating system. + + Args: + progress_callback: Optional callback function for progress updates + + Returns: + True if update was successful, False otherwise + """ + if OS_NAME == "Windows": + return update_ffmpeg_windows(progress_callback) + elif OS_NAME == "Darwin": + # macOS update via Homebrew + if progress_callback: + progress_callback("🍺 Updating FFmpeg via Homebrew...") + try: + subprocess.run(["brew", "upgrade", "ffmpeg"], check=True, timeout=300) + if progress_callback: + progress_callback("✅ FFmpeg updated successfully!") + return True + except Exception as e: + logger.exception(f"Error updating FFmpeg on macOS: {e}") + if progress_callback: + progress_callback(f"❌ Error: {e}") + return False + elif OS_NAME == "Linux": + # Linux update via package manager + if progress_callback: + progress_callback("📦 Please use your package manager to update FFmpeg") + return False + else: + logger.error(f"Unsupported operating system: {OS_NAME}") + return False diff --git a/src/gui/ytsage_gui_dialogs/__init__.py b/src/gui/ytsage_gui_dialogs/__init__.py index f33c15b..c0e8b9f 100644 --- a/src/gui/ytsage_gui_dialogs/__init__.py +++ b/src/gui/ytsage_gui_dialogs/__init__.py @@ -23,6 +23,7 @@ from src.gui.ytsage_gui_dialogs.ytsage_dialogs_selection import ( ) from src.gui.ytsage_gui_dialogs.ytsage_dialogs_settings import AutoUpdateSettingsDialog, DownloadSettingsDialog from src.gui.ytsage_gui_dialogs.ytsage_dialogs_update import AutoUpdateThread, UpdateThread, VersionCheckThread, YTDLPUpdateDialog +from src.gui.ytsage_gui_dialogs.ytsage_dialogs_updater import UpdaterTabWidget __all__ = [ # Base dialogs @@ -52,6 +53,9 @@ __all__ = [ "CustomOptionsDialog", "TimeRangeDialog", + # Updater widget + "UpdaterTabWidget", + # History dialog "HistoryDialog", ] diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py index 3c7f3f4..75efe7a 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py @@ -34,6 +34,7 @@ from src.utils.ytsage_constants import YTDLP_DOCS_URL from src.utils.ytsage_config_manager import ConfigManager from src.utils.ytsage_localization import LocalizationManager, _ from src.utils.ytsage_logger import logger +from src.gui.ytsage_gui_dialogs.ytsage_dialogs_updater import UpdaterTabWidget if TYPE_CHECKING: from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import) @@ -516,11 +517,15 @@ class CustomOptionsDialog(QDialog): language_layout.addStretch() + # === Updater Tab === + updater_tab = UpdaterTabWidget(self) + # Add tabs to the tab widget self.tab_widget.addTab(cookies_tab, _("tabs.cookies")) self.tab_widget.addTab(command_tab, _("tabs.custom_command")) self.tab_widget.addTab(proxy_tab, _("tabs.proxy")) self.tab_widget.addTab(language_tab, _("tabs.language")) + self.tab_widget.addTab(updater_tab, _("tabs.updater")) # Dialog buttons button_box = QDialogButtonBox() diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py index a395103..af521b1 100644 --- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_ffmpeg.py @@ -3,9 +3,7 @@ FFmpeg installation dialogs for YTSage application. Contains dialogs and threads for checking and installing FFmpeg. """ -import contextlib import webbrowser -from io import StringIO from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QIcon @@ -20,15 +18,12 @@ class FFmpegInstallThread(QThread): progress = Signal(str) def run(self) -> None: - # Redirect stdout to capture progress messages - output = StringIO() - with contextlib.redirect_stdout(output): - success = auto_install_ffmpeg() - - # Process captured output and emit progress signals - for line in output.getvalue().splitlines(): - self.progress.emit(line) - + # Use a callback to capture progress instead of stdout redirection + def progress_callback(msg: str): + self.progress.emit(msg) + + # Install FFmpeg with progress callback + success = auto_install_ffmpeg(progress_callback=progress_callback) self.finished.emit(success) @@ -36,9 +31,9 @@ class FFmpegCheckDialog(QDialog): def __init__(self, parent=None) -> None: super().__init__(parent) self.setWindowTitle("FFmpeg Installation") - self.setMinimumWidth(450) - self.setMinimumHeight(200) - self.resize(450, 220) + self.setMinimumWidth(500) + self.setMinimumHeight(280) + self.resize(500, 300) # Set the window icon to match the main app if parent and parent.windowIcon(): @@ -67,11 +62,12 @@ class FFmpegCheckDialog(QDialog): self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(self.message_label) - # Progress label with improved styling and compact height + # Progress label with improved styling self.progress_label = QLabel("") self.progress_label.setWordWrap(True) - self.progress_label.setMinimumHeight(60) # Smaller but visible area - self.progress_label.setMaximumHeight(80) # Limit maximum height + self.progress_label.setMinimumHeight(80) + self.progress_label.setMaximumHeight(120) + self.progress_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) self.progress_label.setStyleSheet( """ QLabel { @@ -79,22 +75,22 @@ class FFmpegCheckDialog(QDialog): color: #cccccc; border: 1px solid #3d3d3d; border-radius: 6px; - padding: 8px; + padding: 12px; font-family: 'Consolas', 'Courier New', monospace; font-size: 11px; - line-height: 1.2; + line-height: 1.4; } """ ) self.progress_label.hide() layout.addWidget(self.progress_label) - # Add minimal stretch - just enough to push buttons down slightly - layout.addSpacing(10) + # Add stretch to push buttons to bottom + layout.addStretch() # Buttons container - simple approach that should work button_layout = QHBoxLayout() - button_layout.setSpacing(15) # Simple spacing + button_layout.setSpacing(12) # Install button self.install_btn = QPushButton("Install FFmpeg") @@ -148,6 +144,7 @@ class FFmpegCheckDialog(QDialog): # Initialize installation thread self.install_thread = None + self.progress_messages = [] # Store progress messages def start_installation(self) -> None: self.install_btn.setEnabled(False) @@ -165,6 +162,7 @@ class FFmpegCheckDialog(QDialog): return self.message_label.setText("Installing FFmpeg... Please wait") + self.progress_messages = [] # Clear previous messages self.progress_label.show() self.install_thread = FFmpegInstallThread() @@ -173,7 +171,13 @@ class FFmpegCheckDialog(QDialog): self.install_thread.start() def update_progress(self, message) -> None: - self.progress_label.setText(message) + # Keep only the last 5 messages to avoid overflow + self.progress_messages.append(message) + if len(self.progress_messages) > 5: + self.progress_messages.pop(0) + + # Display the messages + self.progress_label.setText("\n".join(self.progress_messages)) def installation_finished(self, success) -> None: if success: diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py new file mode 100644 index 0000000..e3e7d2f --- /dev/null +++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py @@ -0,0 +1,362 @@ +""" +Updater tab for Custom Options dialog. +Handles checking for and installing FFmpeg updates. +""" + +import threading +from typing import TYPE_CHECKING, cast + +from PySide6.QtCore import QObject, Signal +from PySide6.QtWidgets import ( + QGroupBox, + QHBoxLayout, + QLabel, + QProgressBar, + QPushButton, + QTextEdit, + QVBoxLayout, + QWidget, +) + +from src.utils.ytsage_localization import _ +from src.utils.ytsage_logger import logger +from src.core.ytsage_ffmpeg_updater import check_ffmpeg_update_available, update_ffmpeg +from src.utils.ytsage_constants import OS_NAME + +if TYPE_CHECKING: + from src.gui.ytsage_gui_dialogs.ytsage_dialogs_custom import CustomOptionsDialog + + +class UpdateWorker(QObject): + """Worker class for running FFmpeg updates in a separate thread.""" + + progress = Signal(str) # Progress messages + finished = Signal(bool) # Completion status (success/failure) + + def run_update(self): + """Run the FFmpeg update process.""" + try: + def progress_callback(msg: str): + """Callback for progress updates.""" + self.progress.emit(msg) + + # Run the update + success = update_ffmpeg(progress_callback=progress_callback) + self.finished.emit(success) + + except Exception as e: + logger.exception(f"Error during FFmpeg update: {e}") + self.progress.emit(f"❌ Error: {e}") + self.finished.emit(False) + + +class UpdaterTabWidget(QWidget): + """Widget for the Updater tab in Custom Options dialog.""" + + def __init__(self, parent=None) -> None: + super().__init__(parent) + self._parent: "CustomOptionsDialog" = cast("CustomOptionsDialog", self.parent()) + + self.update_thread = None + self.update_worker = None + + # State variables + self.current_version = "Unknown" + self.latest_version = "Unknown" + self.update_available = False + + self._init_ui() + + def _init_ui(self) -> None: + """Initialize the UI components.""" + layout = QVBoxLayout(self) + + # Help text + help_text = QLabel(_('ffmpeg_updater.description')) + help_text.setWordWrap(True) + help_text.setStyleSheet("color: #999999; padding: 10px;") + layout.addWidget(help_text) + + # FFmpeg Update Section + ffmpeg_group = QGroupBox(_('ffmpeg_updater.title')) + ffmpeg_layout = QVBoxLayout(ffmpeg_group) + + # Version information layout + version_layout = QVBoxLayout() + + # Current version + current_layout = QHBoxLayout() + current_label = QLabel(_('ffmpeg_updater.current_version')) + current_label.setStyleSheet("font-weight: bold; color: #ffffff;") + current_layout.addWidget(current_label) + + self.current_version_label = QLabel("...") + self.current_version_label.setStyleSheet("color: #cccccc;") + current_layout.addWidget(self.current_version_label) + current_layout.addStretch() + version_layout.addLayout(current_layout) + + # Latest version + latest_layout = QHBoxLayout() + latest_label = QLabel(_('ffmpeg_updater.latest_version')) + latest_label.setStyleSheet("font-weight: bold; color: #ffffff;") + latest_layout.addWidget(latest_label) + + self.latest_version_label = QLabel("...") + self.latest_version_label.setStyleSheet("color: #cccccc;") + latest_layout.addWidget(self.latest_version_label) + latest_layout.addStretch() + version_layout.addLayout(latest_layout) + + ffmpeg_layout.addLayout(version_layout) + + # Status label + self.status_label = QLabel(_('ffmpeg_updater.status_idle')) + self.status_label.setWordWrap(True) + self.status_label.setStyleSheet( + "color: #888888; font-size: 12px; padding: 8px; " + "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" + ) + ffmpeg_layout.addWidget(self.status_label) + + # Button layout + button_layout = QHBoxLayout() + + # Check for updates button + self.check_button = QPushButton(_('ffmpeg_updater.check_updates')) + self.check_button.clicked.connect(self.check_for_updates) + self.check_button.setStyleSheet( + """ + QPushButton { + padding: 8px 15px; + background-color: #444444; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + min-width: 120px; + } + QPushButton:hover { + background-color: #555555; + } + QPushButton:disabled { + background-color: #2a2a2a; + color: #666666; + } + """ + ) + button_layout.addWidget(self.check_button) + + button_layout.addStretch() + + # Update button + self.update_button = QPushButton(_('ffmpeg_updater.update_button')) + self.update_button.clicked.connect(self.start_update) + self.update_button.setEnabled(False) + self.update_button.setStyleSheet( + """ + QPushButton { + padding: 8px 15px; + background-color: #c90000; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + min-width: 120px; + } + QPushButton:hover { + background-color: #a50000; + } + QPushButton:disabled { + background-color: #4a4a4a; + color: #888888; + } + """ + ) + button_layout.addWidget(self.update_button) + + ffmpeg_layout.addLayout(button_layout) + + # Progress bar + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 0) # Indeterminate progress + self.progress_bar.setVisible(False) + self.progress_bar.setStyleSheet( + """ + QProgressBar { + border: 2px solid #2a2d36; + border-radius: 4px; + background-color: #1d1e22; + text-align: center; + color: #ffffff; + height: 20px; + } + QProgressBar::chunk { + background-color: #c90000; + border-radius: 2px; + } + """ + ) + ffmpeg_layout.addWidget(self.progress_bar) + + # Log output + log_label = QLabel("Update Log:") + log_label.setStyleSheet("font-size: 12px; font-weight: bold; color: #ffffff; margin-top: 10px;") + ffmpeg_layout.addWidget(log_label) + + self.log_output = QTextEdit() + self.log_output.setReadOnly(True) + self.log_output.setPlaceholderText("Update logs will appear here...") + self.log_output.setMinimumHeight(150) + self.log_output.setStyleSheet( + """ + QTextEdit { + background-color: #1d1e22; + color: #ffffff; + border: 2px solid #2a2d36; + border-radius: 6px; + padding: 8px; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 11px; + } + """ + ) + ffmpeg_layout.addWidget(self.log_output) + + layout.addWidget(ffmpeg_group) + layout.addStretch() + + def check_for_updates(self) -> None: + """Check if FFmpeg updates are available.""" + self.check_button.setEnabled(False) + self.update_button.setEnabled(False) + self.status_label.setText(_('ffmpeg_updater.status_checking')) + self.status_label.setStyleSheet( + "color: #ffaa00; font-size: 12px; padding: 8px; " + "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" + ) + self.log_output.append("🔍 Checking for FFmpeg updates...") + + # Run check in background thread + def check_thread(): + try: + update_available, current_version, latest_version = check_ffmpeg_update_available() + + # Update UI in main thread using signals + self.check_button.setEnabled(True) + self._update_check_results(update_available, current_version, latest_version) + + except Exception as e: + logger.exception(f"Error checking for updates: {e}") + self.check_button.setEnabled(True) + self._show_check_error(str(e)) + + thread = threading.Thread(target=check_thread, daemon=True) + thread.start() + + def _update_check_results(self, update_available: bool, current_version: str, latest_version: str) -> None: + """Handle completion of update check.""" + self.update_available = update_available + self.current_version = current_version + self.latest_version = latest_version + + # Update version labels + self.current_version_label.setText(current_version) + self.latest_version_label.setText(latest_version) + + # Update status + if current_version == "Not installed": + self.status_label.setText(_('ffmpeg_updater.install_first')) + self.status_label.setStyleSheet( + "color: #ff6666; font-size: 12px; padding: 8px; " + "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" + ) + self.update_button.setEnabled(True if OS_NAME == "Windows" else False) + self.log_output.append(f"❌ FFmpeg is not installed. Latest version available: {latest_version}") + elif update_available: + self.status_label.setText(_('ffmpeg_updater.status_update_available')) + self.status_label.setStyleSheet( + "color: #ffaa00; font-size: 12px; padding: 8px; " + "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" + ) + self.update_button.setEnabled(True if OS_NAME == "Windows" else False) + self.log_output.append(f"⚠ Update available: {current_version} → {latest_version}") + else: + self.status_label.setText(_('ffmpeg_updater.status_up_to_date')) + self.status_label.setStyleSheet( + "color: #00cc00; font-size: 12px; padding: 8px; " + "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" + ) + self.update_button.setEnabled(False) + self.log_output.append(f"✅ FFmpeg is up to date (version {current_version})") + + # Show message for non-Windows systems + if OS_NAME != "Windows" and update_available: + self.log_output.append("ℹ️ Automatic updates are only available on Windows.") + self.log_output.append(" Please use your system's package manager to update FFmpeg.") + + def _show_check_error(self, error: str) -> None: + """Handle error during update check.""" + self.status_label.setText(_('ffmpeg_updater.check_failed')) + self.status_label.setStyleSheet( + "color: #ff6666; font-size: 12px; padding: 8px; " + "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" + ) + self.log_output.append(f"❌ Error checking for updates: {error}") + + def start_update(self) -> None: + """Start the FFmpeg update process.""" + if OS_NAME != "Windows": + self.log_output.append("❌ Automatic updates are only supported on Windows.") + return + + # Disable buttons during update + self.check_button.setEnabled(False) + self.update_button.setEnabled(False) + self.progress_bar.setVisible(True) + + self.log_output.append("=" * 60) + self.log_output.append("🚀 Starting FFmpeg update process...") + self.log_output.append("=" * 60) + + # Create worker and thread + self.update_worker = UpdateWorker() + self.update_thread = threading.Thread(target=self.update_worker.run_update, daemon=True) + + # Connect signals + self.update_worker.progress.connect(self._on_update_progress) + self.update_worker.finished.connect(self._on_update_finished) + + # Start update + self.update_thread.start() + + def _on_update_progress(self, message: str) -> None: + """Handle progress updates from the update worker.""" + self.log_output.append(message) + # Auto-scroll to bottom + cursor = self.log_output.textCursor() + cursor.movePosition(cursor.MoveOperation.End) + self.log_output.setTextCursor(cursor) + + def _on_update_finished(self, success: bool) -> None: + """Handle completion of the update process.""" + self.progress_bar.setVisible(False) + self.check_button.setEnabled(True) + + self.log_output.append("=" * 60) + + if success: + self.log_output.append(_('ffmpeg_updater.update_success', version=self.latest_version)) + self.status_label.setText(_('ffmpeg_updater.status_up_to_date')) + self.status_label.setStyleSheet( + "color: #00cc00; font-size: 12px; padding: 8px; " + "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" + ) + # Re-check to update version info + self.check_for_updates() + else: + self.log_output.append(_('ffmpeg_updater.update_failed')) + self.status_label.setText(_('ffmpeg_updater.status_update_available')) + self.update_button.setEnabled(True) + + self.log_output.append("=" * 60) diff --git a/src/utils/ytsage_constants.py b/src/utils/ytsage_constants.py index 14f9c88..6334a28 100644 --- a/src/utils/ytsage_constants.py +++ b/src/utils/ytsage_constants.py @@ -145,10 +145,13 @@ else: # Linux and other UNIX-like # Documentation URLs YTDLP_DOCS_URL: str = "https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#usage-and-options" -# ffmpeg download links -FFMPEG_7Z_DOWNLOAD_URL = "https://github.com/GyanD/codexffmpeg/releases/download/7.1.1/ffmpeg-7.1.1-full_build.7z" -FFMPEG_7Z_SHA256_URL = "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-7.1.1-full_build.7z.sha256" -FFMPEG_ZIP_DOWNLOAD_URL = "https://github.com/GyanD/codexffmpeg/releases/download/7.1.1/ffmpeg-7.1.1-full_build.zip" +# FFmpeg download links (Essentials build - always latest version) +FFMPEG_7Z_DOWNLOAD_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z" +FFMPEG_ZIP_DOWNLOAD_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip" +FFMPEG_7Z_SHA256_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z.sha256" +FFMPEG_ZIP_SHA256_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip.sha256" +FFMPEG_7Z_VERSION_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z.ver" +FFMPEG_ZIP_VERSION_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip.ver" if __name__ == "__main__": # If this file is run directly, print directory information; if imported, create the necessary directories for the application.