diff --git a/CHANGES b/CHANGES index 74a21dc4..ed947bcf 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,18 @@ # Changelog +## Version 4.7.0 + +* Adding #164 audio matching in profiles (thanks to bmcassagne) +* Adding #261 Advanced settings are currently not saved on Profiles (thanks to georgesaumen) +* Adding #294 NVEncC 10-bit encoding mode for 8-bit source (thanks to Don Gafford) +* Adding HDR10 support, svtav1-params, scene detection, and crf option for SVT AV1 +* Adding max colors and stats mode to GIF +* Adding OpenCL support for Remove HDR to speed it up +* Changing FFmpeg download to look for latest master GPL builds +* Fixing #296 low quality auto-crop due to high rounding, increasing accuracy from 16 to 2 pixels (thanks to Rayman24365) +* Fixing concat builder behavior to work smoother +* Fixing thumbnail generation for concat images + ## Version 4.6.0 * Adding #195 640kbps audio (thanks to ObviousInRetrospect and Harybo) diff --git a/README.md b/README.md index e6bd60a9..bb3ace9b 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,13 @@ Check out [the FastFlix github wiki](https://github.com/cdgriffith/FastFlix/wiki FastFlix supports the following encoders if available: | Encoder | x265 | NVENC HEVC | [NVEncC HEVC](https://github.com/rigaya/NVEnc/releases) | [VCEEncC HEVC](https://github.com/rigaya/VCEEnc/releases) | x264 | rav1e | AOM AV1 | SVT AV1 | VP9 | WEBP | GIF | -| --------- | ---- | ---------- | ----------- | ----------- | ---- | ----- | ------- | ------- | --- | ---- | --- | -| HDR10 | ✓ | | ✓ | ✓ | | | | | ✓* | | | -| HDR10+ | ✓ | | ✓ | | | | | | | | | -| Audio | ✓ | ✓ | ✓* | ✓* | ✓ | ✓ | ✓ | ✓ | ✓ | | | -| Subtitles | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | -| Covers | ✓ | ✓ | | | ✓ | ✓ | ✓ | ✓ | | | | -| bt.2020 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | +| --------- | ---- | ---------- | ----------- | ----------- | ---- | ----- | ------- |-----| --- | ---- | --- | +| HDR10 | ✓ | | ✓ | ✓ | | | | ✓ | ✓* | | | +| HDR10+ | ✓ | | ✓ | | | | | | | | | +| Audio | ✓ | ✓ | ✓* | ✓* | ✓ | ✓ | ✓ | ✓ | ✓ | | | +| Subtitles | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | +| Covers | ✓ | ✓ | | | ✓ | ✓ | ✓ | ✓ | | | | +| bt.2020 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | `✓ - Full support | ✓* - Limited support` @@ -33,7 +33,7 @@ Check out [the FastFlix github wiki](https://github.com/cdgriffith/FastFlix/wiki View the [releases](https://github.com/cdgriffith/FastFlix/releases) for binaries for Windows, MacOS or Linux -You will need to have `ffmpeg` and `ffprobe` executables on your PATH and they must be executable. Version 4.3 or greater is required. The one in your in your package manager system may not support all encoders or options. +You will need to have `ffmpeg` and `ffprobe` executables on your PATH and they must be executable. Version 4.3 or greater is required for most usage, latest master build is recommended and required for some features. The one in your package manager system may not support all encoders or options. Check out the [FFmpeg download page for static builds](https://ffmpeg.org/download.html) for Linux and Mac. ## Running from source code @@ -59,10 +59,10 @@ FastFlix was created to easily extract / copy HDR10 data, which it can do with t VP9 has limited support to copy some existing HDR10 metadata, usually from other VP9 files. Will have the line "Mastering Display Metadata, has_primaries:1 has_luminance:1 ..." when it works. -AV1 is still in development, and as such none of them currently support setting mastering-display data through FFmpeg (aka not supported with FastFlix), but some do if you use them directly. +AV1 is still in development, and hopefully all encoder will support it in the future, but only SVT AV1 works through ffmpeg as of now. * rav1e - can set mastering data and CLL via their CLI but [not through ffmpeg](https://github.com/xiph/rav1e/issues/2554). -* SVT AV1 - accepts a "--enable-hdr" flag that is [not well documented](https://github.com/AOMediaCodec/SVT-AV1/blob/master/Docs/svt-av1_encoder_user_guide.md), not supported through FFmpeg. +* SVT AV1 - Now supports HDR10 with latest master ffmpeg build, make sure to update before trying! * aomenc (libaom-av1) - does not look to support HDR10 ## HDR10+ diff --git a/fastflix/application.py b/fastflix/application.py index cc6fb9aa..41b9e24f 100644 --- a/fastflix/application.py +++ b/fastflix/application.py @@ -7,7 +7,7 @@ import reusables from PySide6 import QtGui, QtWidgets, QtCore -from fastflix.flix import ffmpeg_audio_encoders, ffmpeg_configuration, ffprobe_configuration +from fastflix.flix import ffmpeg_audio_encoders, ffmpeg_configuration, ffprobe_configuration, ffmpeg_opencl_support from fastflix.language import t from fastflix.models.config import Config, MissingFF from fastflix.models.fastflix import FastFlix @@ -171,6 +171,7 @@ def start_app(worker_queue, status_queue, log_queue, queue_list, queue_lock): Task(t("Gather FFmpeg version"), ffmpeg_configuration), Task(t("Gather FFprobe version"), ffprobe_configuration), Task(t("Gather FFmpeg audio encoders"), ffmpeg_audio_encoders), + Task(t("Determine OpenCL Support"), ffmpeg_opencl_support), Task(t("Initialize Encoders"), init_encoders), ] diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index 5c67e99b..889bca93 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -106,18 +106,6 @@ Advanced: por: Avançado swe: Avancerad pol: Zaawansowane -Advanced settings are currently not saved in Profiles: - deu: Erweiterte Einstellungen werden derzeit nicht in Profilen gespeichert - eng: Advanced settings are currently not saved in Profiles - fra: Les paramètres avancés ne sont actuellement pas enregistrés dans les Profils - ita: Le impostazioni avanzate non sono attualmente salvate in Profili - spa: Los ajustes avanzados no se guardan actualmente en Perfiles - zho: 高级设置目前不会保存到方案中 - jpn: 現在、詳細設定はプロファイルに保存されていません。 - rus: Расширенные настройки в настоящее время не сохраняются в профилях - por: As definições avançadas não são actualmente guardadas em Perfis - swe: Avancerade inställningar sparas för närvarande inte i profiler - pol: Ustawienia zaawansowane nie są obecnie zapisywane w profilach After Conversion: deu: Nach der Konvertierung eng: After Conversion @@ -5794,3 +5782,327 @@ Pause Warning: por: Aviso de Pausa swe: Varning för paus pol: Pauza Ostrzeżenie +First: + eng: First + pol: Pierwszy + deu: Erste + fra: Premier + ita: Prima + spa: Primero + zho: 首先 + jpn: ファースト + rus: Первый + por: Primeiro + swe: Första +Last: + eng: Last + pol: Ostatnio + deu: Zuletzt + fra: Dernier site + ita: Ultimo + spa: Última + zho: 最后一次 + jpn: 最後 + rus: Последний + por: Último + swe: Senast +Track Number: + eng: Track Number + pol: Numer ścieżki + deu: Titel Nummer + fra: Numéro de piste + ita: Numero di traccia + spa: Número de pista + zho: 轨道编号 + jpn: トラックナンバー + rus: Номер дорожки + por: Número da faixa + swe: Spårnummer +Channels: + eng: Channels + pol: Kanały + deu: Kanäle + fra: Chaînes + ita: Canali + spa: Canales + zho: 渠道 + jpn: チャンネル + rus: Каналы + por: Canais + swe: Kanaler +Audio Select: + eng: Audio Select + pol: Audio Select (Wybór dźwięku) + deu: Audio-Auswahl + fra: Sélection audio + ita: Selezione audio + spa: Selección de audio + zho: 音频选择 + jpn: オーディオセレクト + rus: Выбор аудио + por: Selecione o áudio + swe: Ljudval +Passthrough All: + eng: Passthrough All + pol: Passthrough Wszystkie + deu: Durchleitung Alle + fra: Passthrough Tous + ita: Passthrough Tutti + spa: Transferencia Todos + zho: 穿透式所有 + jpn: パススルー 全て + rus: Пропускная способность Все + por: Passagem de tudo + swe: Genomströmning Alla +Add Pattern Match: + eng: Add Pattern Match + pol: Dodaj dopasowanie wzoru + deu: Mustervergleich hinzufügen + fra: Ajouter une correspondance de motifs + ita: Aggiungere la corrispondenza del modello + spa: Añadir coincidencia de patrones + zho: 添加模式匹配 + jpn: パターンマッチの追加 + rus: Добавить сопоставление шаблонов + por: Adicionar Padrão de Combinação + swe: Lägg till mönstermatchning +contains: + eng: contains + pol: zawiera + deu: enthält + fra: contient + ita: contiene + spa: contiene + zho: 包含 + jpn: を含む + rus: содержит + por: contém + swe: innehåller +Match: + eng: Match + pol: Mecz + deu: Spiel + fra: Match + ita: Partita + spa: Partido + zho: 匹配 + jpn: マッチ + rus: Матч + por: Combinar + swe: Match +Select By: + eng: Select By + pol: Wybierz według + deu: Auswählen nach + fra: Sélectionner par + ita: Seleziona per + spa: Seleccionar por + zho: 选择方式 + jpn: セレクト・バイ + rus: Выберите по + por: Selecione por + swe: Välj efter +Advanced Options: + eng: Advanced Options + pol: Opcje zaawansowane + deu: Erweiterte Optionen + fra: Options avancées + ita: Opzioni avanzate + spa: Opciones avanzadas + zho: 高级选项 + jpn: 詳細オプション + rus: Дополнительные параметры + por: Opções avançadas + swe: Avancerade alternativ +License: + eng: License + deu: Lizenz + fra: Licence + ita: Licenza + spa: Licencia + zho: 许可证 + jpn: ライセンス + rus: Лицензия + por: Licença + swe: Licens + pol: Licencja +Quantization Mode: + eng: Quantization Mode + deu: Quantisierungsmodus + fra: Mode de quantification + ita: Modalità di quantizzazione + spa: Modo de cuantificación + zho: 量化模式 + jpn: 量子化モード + rus: Режим квантования + por: Modo de quantificação + swe: Kvantiseringsläge + pol: Tryb kwantyzacji +Use CRF or QP: + eng: Use CRF or QP + deu: CRF oder QP verwenden + fra: Utiliser le CRF ou le QP + ita: Utilizzare CRF o QP + spa: Utilizar CRF o QP + zho: 使用CRF或QP + jpn: CRFまたはQPを使用する + rus: Используйте CRF или QP + por: Use CRF ou QP + swe: Använd CRF eller QP + pol: Użyj CRF lub QP +Additional svt av1 params: + eng: Additional svt av1 params + deu: Zusätzliche svt av1-Parameter + fra: Paramètres supplémentaires de svt av1 + ita: Parametri svt av1 aggiuntivi + spa: Parámetros adicionales de svt av1 + zho: 额外的svt av1参数 + jpn: svt av1 の追加パラメータ + rus: Дополнительные параметры svt av1 + por: Parâmetros adicionais svt av1 + swe: Ytterligare parametrar för svt av1 + pol: Dodatkowe parametry svt av1 +Extra svt av1 params in opt=1:opt2=0 format: + eng: Extra svt av1 params in opt=1:opt2=0 format + deu: Zusätzliche svt av1-Parameter im Format opt=1:opt2=0 + fra: Paramètres supplémentaires de svt av1 au format opt=1:opt2=0 + ita: Parametri extra svt av1 nel formato opt=1:opt2=0 + spa: Parámetros extra svt av1 en formato opt=1:opt2=0 + zho: 额外的svt av1参数为opt=1:opt2=0格式 + jpn: opt=1:opt2=0のフォーマットでの余分なsvt av1パラメータ + rus: Дополнительные параметры svt av1 в формате opt=1:opt2=0 + por: Parâmetros av1 extra svt no formato opt=1:opt2=0 + swe: Extra svt av1-parametrar i formatet opt=1:opt2=0 + pol: Dodatkowe parametry svt av1 w formacie opt=1:opt2=0 +Max Colors: + eng: Max Colors + deu: Maximale Farben + fra: Couleurs maximales + ita: Colori massimi + spa: Colores máximos + zho: 最大颜色 + jpn: 最大色数 + rus: Максимальные цвета + por: Cores máximas + swe: Max färger + pol: Maks. kolory +Frame Rate: + eng: Frame Rate + deu: Bildfrequenz + fra: Fréquence d'images + ita: Frame Rate + spa: Velocidad de fotogramas + zho: 帧速率 + jpn: フレームレート + rus: Частота кадров + por: Taxa de quadros + swe: Bildfrekvens + pol: Liczba klatek na sekundę +Equalizer: + eng: Equalizer + deu: Equalizer + fra: Égaliseur + ita: Equalizzatore + spa: Ecualizador + zho: 平衡器 + jpn: イコライザー + rus: Эквалайзер + por: Equalizador + swe: Equalizer + pol: Equalizer +Method: + eng: Method + deu: Methode + fra: Méthode + ita: Metodo + spa: Método + zho: 方法 + jpn: 方法 + rus: Метод + por: Método + swe: Metod + pol: Metoda +Color Formats: + eng: Color Formats + deu: Farbige Formate + fra: Formats de couleur + ita: Formati di colore + spa: Formatos de color + zho: 彩色格式 + jpn: カラーフォーマット + rus: Форматы цветов + por: Formatos de cores + swe: Färgformat + pol: Formaty kolorów +Video Buffering: + eng: Video Buffering + deu: Video-Pufferung + fra: Mise en mémoire tampon des vidéos + ita: Buffering video + spa: Buffering de vídeo + zho: 视频缓冲 + jpn: ビデオバッファリング + rus: Буферизация видео + por: Buffer de vídeo + swe: Videobuffring + pol: Buforowanie wideo +Verifier: + eng: Verifier + deu: Überprüfer + fra: Vérificateur + ita: Verificatore + spa: Verificador + zho: 验证人 + jpn: ベリファイア + rus: Верификатор + por: Verificador + swe: Kontrollör + pol: Weryfikator +Statistics Mode: + eng: Statistics Mode + deu: Statistik-Modus + fra: Mode statistiques + ita: Modalità statistiche + spa: Modo de estadísticas + zho: 统计模式 + jpn: 統計モード + rus: Режим статистики + por: Modo Estatísticas + swe: Statistikläge + pol: Tryb statystyk +Scene Detection: + eng: Scene Detection + deu: Szene-Erkennung + fra: Détection de la scène + ita: Rilevamento della scena + spa: Detección de escenas + zho: 场景检测 + jpn: シーン検出 + rus: Обнаружение сцены + por: Detecção de cenas + swe: Detektering av scener + pol: Wykrywanie sceny +Determine OpenCL Support: + eng: Determine OpenCL Support + deu: Bestimmen Sie die OpenCL-Unterstützung + fra: Déterminer la prise en charge d'OpenCL + ita: Determinare il supporto OpenCL + spa: Determinar la compatibilidad con OpenCL + zho: 确定OpenCL支持 + jpn: OpenCLのサポートを決定 + rus: Определить поддержку OpenCL + por: Determinar o suporte do OpenCL + swe: Fastställa stöd för OpenCL + pol: Określanie obsługi OpenCL +Please load in a video to configure a new profile: + eng: Please load in a video to configure a new profile + deu: Bitte laden Sie ein Video zur Konfiguration eines neuen Profils + fra: Veuillez charger une vidéo pour configurer un nouveau profil. + ita: Si prega di caricare un video per configurare un nuovo profilo + spa: Por favor, cargue un vídeo para configurar un nuevo perfil + zho: 请加载一个视频来配置一个新的配置文件 + jpn: 新しいプロファイルを設定するためのビデオを読み込んでください。 + rus: Пожалуйста, загрузите видео по настройке нового профиля + por: Por favor, carregue em um vídeo para configurar um novo perfil + swe: Ladda in en video för att konfigurera en ny profil + pol: Wczytaj film, jak skonfigurować nowy profil diff --git a/fastflix/data/styles/breeze_styles/onyx/checkbox_checked.png b/fastflix/data/styles/breeze_styles/onyx/checkbox_checked.png new file mode 100644 index 00000000..7abc9f4f Binary files /dev/null and b/fastflix/data/styles/breeze_styles/onyx/checkbox_checked.png differ diff --git a/fastflix/data/styles/breeze_styles/onyx/checkbox_checked.svg b/fastflix/data/styles/breeze_styles/onyx/checkbox_checked.svg deleted file mode 100644 index f281ebab..00000000 --- a/fastflix/data/styles/breeze_styles/onyx/checkbox_checked.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/fastflix/data/styles/breeze_styles/onyx/checkbox_checked_disabled.png b/fastflix/data/styles/breeze_styles/onyx/checkbox_checked_disabled.png new file mode 100644 index 00000000..ac8fa0fd Binary files /dev/null and b/fastflix/data/styles/breeze_styles/onyx/checkbox_checked_disabled.png differ diff --git a/fastflix/data/styles/breeze_styles/onyx/checkbox_checked_disabled.svg b/fastflix/data/styles/breeze_styles/onyx/checkbox_checked_disabled.svg deleted file mode 100644 index 61bb6810..00000000 --- a/fastflix/data/styles/breeze_styles/onyx/checkbox_checked_disabled.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/fastflix/data/styles/breeze_styles/onyx/checkbox_unchecked.png b/fastflix/data/styles/breeze_styles/onyx/checkbox_unchecked.png new file mode 100644 index 00000000..5e43f737 Binary files /dev/null and b/fastflix/data/styles/breeze_styles/onyx/checkbox_unchecked.png differ diff --git a/fastflix/data/styles/breeze_styles/onyx/checkbox_unchecked.svg b/fastflix/data/styles/breeze_styles/onyx/checkbox_unchecked.svg deleted file mode 100644 index f96609c8..00000000 --- a/fastflix/data/styles/breeze_styles/onyx/checkbox_unchecked.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/fastflix/data/styles/breeze_styles/onyx/checkbox_unchecked_disabled.png b/fastflix/data/styles/breeze_styles/onyx/checkbox_unchecked_disabled.png new file mode 100644 index 00000000..1058e631 Binary files /dev/null and b/fastflix/data/styles/breeze_styles/onyx/checkbox_unchecked_disabled.png differ diff --git a/fastflix/data/styles/breeze_styles/onyx/checkbox_unchecked_disabled.svg b/fastflix/data/styles/breeze_styles/onyx/checkbox_unchecked_disabled.svg deleted file mode 100644 index a7322894..00000000 --- a/fastflix/data/styles/breeze_styles/onyx/checkbox_unchecked_disabled.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/fastflix/data/styles/breeze_styles/onyx/radio_checked.png b/fastflix/data/styles/breeze_styles/onyx/radio_checked.png new file mode 100644 index 00000000..a619a3ae Binary files /dev/null and b/fastflix/data/styles/breeze_styles/onyx/radio_checked.png differ diff --git a/fastflix/data/styles/breeze_styles/onyx/radio_checked.svg b/fastflix/data/styles/breeze_styles/onyx/radio_checked.svg deleted file mode 100644 index fc3d68d6..00000000 --- a/fastflix/data/styles/breeze_styles/onyx/radio_checked.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/fastflix/data/styles/breeze_styles/onyx/radio_checked_disabled.png b/fastflix/data/styles/breeze_styles/onyx/radio_checked_disabled.png new file mode 100644 index 00000000..5ee26d6a Binary files /dev/null and b/fastflix/data/styles/breeze_styles/onyx/radio_checked_disabled.png differ diff --git a/fastflix/data/styles/breeze_styles/onyx/radio_checked_disabled.svg b/fastflix/data/styles/breeze_styles/onyx/radio_checked_disabled.svg deleted file mode 100644 index 0611c45a..00000000 --- a/fastflix/data/styles/breeze_styles/onyx/radio_checked_disabled.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/fastflix/data/styles/breeze_styles/onyx/radio_unchecked.png b/fastflix/data/styles/breeze_styles/onyx/radio_unchecked.png new file mode 100644 index 00000000..f7bb25df Binary files /dev/null and b/fastflix/data/styles/breeze_styles/onyx/radio_unchecked.png differ diff --git a/fastflix/data/styles/breeze_styles/onyx/radio_unchecked.svg b/fastflix/data/styles/breeze_styles/onyx/radio_unchecked.svg deleted file mode 100644 index 04060ac7..00000000 --- a/fastflix/data/styles/breeze_styles/onyx/radio_unchecked.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/fastflix/data/styles/breeze_styles/onyx/radio_unchecked_disabled.png b/fastflix/data/styles/breeze_styles/onyx/radio_unchecked_disabled.png new file mode 100644 index 00000000..7f9e17ab Binary files /dev/null and b/fastflix/data/styles/breeze_styles/onyx/radio_unchecked_disabled.png differ diff --git a/fastflix/data/styles/breeze_styles/onyx/radio_unchecked_disabled.svg b/fastflix/data/styles/breeze_styles/onyx/radio_unchecked_disabled.svg deleted file mode 100644 index c6470740..00000000 --- a/fastflix/data/styles/breeze_styles/onyx/radio_unchecked_disabled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/fastflix/data/styles/breeze_styles/onyx/stylesheet.qss b/fastflix/data/styles/breeze_styles/onyx/stylesheet.qss index c9cada2f..e4cc8000 100644 --- a/fastflix/data/styles/breeze_styles/onyx/stylesheet.qss +++ b/fastflix/data/styles/breeze_styles/onyx/stylesheet.qss @@ -131,7 +131,7 @@ QCheckBox::indicator:unchecked:focus, QTreeView::indicator:unchecked, QTreeView::indicator:unchecked:focus { - border-image: url(onyx:checkbox_unchecked_disabled.svg); + border-image: url(onyx:checkbox_unchecked.png); } QCheckBox::indicator:unchecked:hover, @@ -144,14 +144,14 @@ QGroupBox::indicator:unchecked:focus, QGroupBox::indicator:unchecked:pressed { border: none; - border-image: url(onyx:checkbox_unchecked.svg); + border-image: url(onyx:checkbox_unchecked.png); } QCheckBox::indicator:checked, QTreeView::indicator:checked, QGroupBox::indicator:checked { - border-image: url(onyx:checkbox_checked.svg); + border-image: url(onyx:checkbox_checked.png); } QCheckBox::indicator:checked:hover, @@ -165,7 +165,7 @@ QGroupBox::indicator:checked:focus, QGroupBox::indicator:checked:pressed { border: none; - border-image: url(onyx:checkbox_checked.svg); + border-image: url(onyx:checkbox_checked.png); } QCheckBox::indicator:indeterminate, @@ -194,14 +194,14 @@ QCheckBox::indicator:checked:disabled, QTreeView::indicator:checked:disabled, QGroupBox::indicator:checked:disabled { - border-image: url(onyx:checkbox_checked_disabled.svg); + border-image: url(onyx:checkbox_checked_disabled.png); } QCheckBox::indicator:unchecked:disabled, QTreeView::indicator:unchecked:disabled, QGroupBox::indicator:unchecked:disabled { - border-image: url(onyx:checkbox_unchecked_disabled.svg); + border-image: url(onyx:checkbox_unchecked_disabled.png); } QRadioButton @@ -226,7 +226,7 @@ QRadioButton::indicator QRadioButton::indicator:unchecked, QRadioButton::indicator:unchecked:focus { - border-image: url(onyx:radio_unchecked_disabled.svg); + border-image: url(onyx:radio_unchecked_disabled.png); } QRadioButton::indicator:unchecked:hover, @@ -234,14 +234,14 @@ QRadioButton::indicator:unchecked:pressed { border: none; outline: none; - border-image: url(onyx:radio_unchecked.svg); + border-image: url(onyx:radio_unchecked.png); } QRadioButton::indicator:checked { border: none; outline: none; - border-image: url(onyx:radio_checked.svg); + border-image: url(onyx:radio_checked.png); } QRadioButton::indicator:checked:hover, @@ -250,18 +250,18 @@ QRadioButton::indicator:checked:pressed { border: none; outline: none; - border-image: url(onyx:radio_checked.svg); + border-image: url(onyx:radio_checked.png); } QRadioButton::indicator:checked:disabled { outline: none; - border-image: url(onyx:radio_checked_disabled.svg); + border-image: url(onyx:radio_checked_disabled.png); } QRadioButton::indicator:unchecked:disabled { - border-image: url(onyx:radio_unchecked_disabled.svg); + border-image: url(onyx:radio_unchecked_disabled.png); } QMenuBar @@ -336,42 +336,42 @@ QMenu::indicator QMenu::indicator:non-exclusive:unchecked { - border-image: url(onyx:checkbox_unchecked_disabled.svg); + border-image: url(onyx:checkbox_unchecked_disabled.png); } QMenu::indicator:non-exclusive:unchecked:selected { - border-image: url(onyx:checkbox_unchecked_disabled.svg); + border-image: url(onyx:checkbox_unchecked_disabled.png); } QMenu::indicator:non-exclusive:checked { - border-image: url(onyx:checkbox_checked.svg); + border-image: url(onyx:checkbox_checked.png); } QMenu::indicator:non-exclusive:checked:selected { - border-image: url(onyx:checkbox_checked.svg); + border-image: url(onyx:checkbox_checked.png); } QMenu::indicator:exclusive:unchecked { - border-image: url(dark:radio_unchecked_disabled.svg); + border-image: url(dark:radio_unchecked_disabled.png); } QMenu::indicator:exclusive:unchecked:selected { - border-image: url(dark:radio_unchecked_disabled.svg); + border-image: url(dark:radio_unchecked_disabled.png); } QMenu::indicator:exclusive:checked { - border-image: url(dark:radio_checked.svg); + border-image: url(dark:radio_checked.png); } QMenu::indicator:exclusive:checked:selected { - border-image: url(dark:radio_checked.svg); + border-image: url(dark:radio_checked.png); } QMenu::right-arrow @@ -1962,3 +1962,8 @@ QMessageBox QPushButton min-height: 1.1em; min-width: 5em; } + +Audio +{ + background-color: #ffffff; +} diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py index 8dd47b00..9fec3a7d 100644 --- a/fastflix/encoders/common/helpers.py +++ b/fastflix/encoders/common/helpers.py @@ -43,6 +43,8 @@ def generate_ffmpeg_start( source_fps: Union[str, None] = None, vsync: Union[str, None] = None, concat: bool = False, + enable_opencl: bool = False, + remove_hdr: bool = True, **_, ) -> str: time_settings = f'{f"-ss {start_time}" if start_time else ""} {f"-to {end_time}" if end_time else ""} ' @@ -72,6 +74,7 @@ def generate_ffmpeg_start( f"-pix_fmt {pix_fmt}", f"{f'-maxrate:v {maxrate}k' if maxrate else ''}", f"{f'-bufsize:v {bufsize}k' if bufsize else ''}", + ("-init_hw_device opencl=ocl -filter_hw_device ocl " if enable_opencl and remove_hdr else ""), " ", # Leave space after commands ] ) @@ -120,6 +123,7 @@ def generate_filters( contrast=None, brightness=None, saturation=None, + enable_opencl: bool = False, tone_map: str = "hable", video_speed: Union[float, int] = 1, deblock: Union[str, None] = None, @@ -155,9 +159,14 @@ def generate_filters( if denoise: filter_list.append(denoise) if remove_hdr: - filter_list.append( - f"zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap={tone_map}:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" - ) + if enable_opencl: + filter_list.append( + f"format=p010,hwupload,tonemap_opencl=tonemap={tone_map}:desat=0:r=tv:p=bt709:t=bt709:m=bt709:format=nv12,hwdownload,format=nv12" + ) + else: + filter_list.append( + f"zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap={tone_map}:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" + ) eq_filters = [] if brightness: @@ -218,6 +227,7 @@ def generate_all( source=fastflix.current_video.source, burn_in_subtitle_track=burn_in_track, burn_in_subtitle_type=burn_in_type, + enable_opencl=fastflix.opencl_support, **fastflix.current_video.video_settings.dict(), ) @@ -235,6 +245,7 @@ def generate_all( encoder=encoder, filters=filters, concat=fastflix.current_video.concat, + enable_opencl=fastflix.opencl_support, **fastflix.current_video.video_settings.dict(), **settings.dict(), ) diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py index e435947d..7a6fcce7 100644 --- a/fastflix/encoders/common/setting_panel.py +++ b/fastflix/encoders/common/setting_panel.py @@ -392,7 +392,6 @@ def new_source(self): def update_profile(self): global ffmpeg_extra_command - logger.debug("Update profile called") for widget_name, opt in self.opts.items(): if isinstance(self.widgets[widget_name], QtWidgets.QComboBox): default = self.determine_default( @@ -408,7 +407,7 @@ def update_profile(self): self.widgets[widget_name].setChecked(checked) elif isinstance(self.widgets[widget_name], QtWidgets.QLineEdit): data = self.app.fastflix.config.encoder_opt(self.profile_name, opt) - if widget_name == "x265_params": + if widget_name in ("x265_params", "svtav1_params"): data = ":".join(data) self.widgets[widget_name].setText(str(data) or "") try: @@ -472,7 +471,7 @@ def reload(self): elif isinstance(self.widgets[widget_name], QtWidgets.QCheckBox): self.widgets[widget_name].setChecked(data) elif isinstance(self.widgets[widget_name], QtWidgets.QLineEdit): - if widget_name == "x265_params": + if widget_name in ("x265_params", "svtav1_params"): data = ":".join(data) self.widgets[widget_name].setText(str(data) or "") if getattr(self, "qp_radio", None): diff --git a/fastflix/encoders/gif/command_builder.py b/fastflix/encoders/gif/command_builder.py index 57001775..96f5616b 100644 --- a/fastflix/encoders/gif/command_builder.py +++ b/fastflix/encoders/gif/command_builder.py @@ -11,7 +11,13 @@ def build(fastflix: FastFlix): settings: GIFSettings = fastflix.current_video.video_settings.video_encoder_settings - palletgen_filters = generate_filters(custom_filters="palettegen", **fastflix.current_video.video_settings.dict()) + args = f"=stats_mode={settings.stats_mode}" + if settings.max_colors != "256": + args += f":max_colors={settings.max_colors}" + + palletgen_filters = generate_filters( + custom_filters=f"palettegen{args}", **fastflix.current_video.video_settings.dict() + ) filters = generate_filters( custom_filters=f"fps={settings.fps:.2f}", raw_filters=True, **fastflix.current_video.video_settings.dict() diff --git a/fastflix/encoders/gif/settings_panel.py b/fastflix/encoders/gif/settings_panel.py index d62c771a..e4450239 100644 --- a/fastflix/encoders/gif/settings_panel.py +++ b/fastflix/encoders/gif/settings_panel.py @@ -21,6 +21,8 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addLayout(self.init_dither(), 0, 0, 1, 2) grid.addLayout(self.init_fps(), 1, 0, 1, 2) + grid.addLayout(self.init_max_colors(), 2, 0, 1, 2) + grid.addLayout(self.init_statistics_mode(), 3, 0, 1, 2) grid.addLayout(self._add_custom(), 11, 0, 1, 6) grid.addWidget(QtWidgets.QWidget(), 5, 0, 5, 6) @@ -55,12 +57,32 @@ def init_dither(self): ], ) + def init_max_colors(self): + return self._add_combo_box( + label="Max Colors", + widget_name="max_colors", + options=["2", "3", "4", "8", "16", "32", "64", "128", "256"], + default=8, + opt="max_colors", + ) + + def init_statistics_mode(self): + return self._add_combo_box( + label="Statistics Mode", + widget_name="stats_mode", + options=["full", "diff", "single"], + default=0, + opt="stats_mode", + ) + def update_video_encoder_settings(self): self.app.fastflix.current_video.video_settings.video_encoder_settings = GIFSettings( fps=int(self.widgets.fps.currentText()), dither=self.widgets.dither.currentText(), extra=self.ffmpeg_extras, pix_fmt="yuv420p", # hack for thumbnails to show properly + max_colors=self.widgets.max_colors.currentText(), + stats_mode=self.widgets.stats_mode.currentText(), extra_both_passes=self.widgets.extra_both_passes.isChecked(), ) diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index a7fe2ec9..c435062d 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -102,6 +102,12 @@ def build(fastflix: FastFlix): elif settings.aq.lower() == "temporal": aq = f"--aq-temporal --aq-strength {settings.aq_strength}" + bit_depth = "8" + if video.current_video_stream.bit_depth > 8 and not video.video_settings.remove_hdr: + bit_depth = "10" + if settings.force_ten_bit: + bit_depth = "10" + command = [ f'"{clean_file_string(fastflix.config.nvencc)}"', "-i", @@ -142,7 +148,7 @@ def build(fastflix: FastFlix): (max_cll if max_cll else ""), (dhdr if dhdr else ""), "--output-depth", - ("10" if video.current_video_stream.bit_depth > 8 and not video.video_settings.remove_hdr else "8"), + bit_depth, "--multipass", settings.multipass, "--mv-precision", diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py index 06feaae0..74b99aa6 100644 --- a/fastflix/encoders/nvencc_hevc/settings_panel.py +++ b/fastflix/encoders/nvencc_hevc/settings_panel.py @@ -111,6 +111,8 @@ def __init__(self, parent, main, app: FastFlixApp): grid.addLayout(qp_line, 5, 2, 1, 4) advanced = QtWidgets.QHBoxLayout() + advanced.addLayout(self.init_10_bit()) + advanced.addStretch(1) advanced.addLayout(self.init_ref()) advanced.addStretch(1) advanced.addLayout(self.init_b_frames()) @@ -344,6 +346,9 @@ def init_ref(self): min_width=60, ) + def init_10_bit(self): + return self._add_check_box(label="10-bit", widget_name="ten_bit", opt="force_ten_bit") + def init_metrics(self): return self._add_check_box( widget_name="metrics", @@ -389,6 +394,13 @@ def setting_change(self, update=True): return self.updating_settings = True + if self.app.fastflix.current_video.current_video_stream.bit_depth > 8 and not self.main.remove_hdr: + self.widgets.ten_bit.setChecked(True) + self.widgets.ten_bit.setDisabled(True) + else: + self.widgets.ten_bit.setChecked(False) + self.widgets.ten_bit.setDisabled(False) + if update: self.main.page_update() self.updating_settings = False @@ -397,6 +409,7 @@ def update_video_encoder_settings(self): settings = NVEncCSettings( preset=self.widgets.preset.currentText().split("-")[0].strip(), # profile=self.widgets.profile.currentText(), + force_ten_bit=self.widgets.ten_bit.isChecked(), tier=self.widgets.tier.currentText(), lookahead=self.widgets.lookahead.currentIndex() if self.widgets.lookahead.currentIndex() > 0 else None, aq=self.widgets.aq.currentText(), @@ -443,3 +456,9 @@ def new_source(self): self.extract_button.show() else: self.extract_button.hide() + if self.app.fastflix.current_video.current_video_stream.bit_depth > 8 and not self.main.remove_hdr: + self.widgets.ten_bit.setChecked(True) + self.widgets.ten_bit.setDisabled(True) + else: + self.widgets.ten_bit.setChecked(False) + self.widgets.ten_bit.setDisabled(False) diff --git a/fastflix/encoders/svt_av1/command_builder.py b/fastflix/encoders/svt_av1/command_builder.py index f8d97272..f72fd233 100644 --- a/fastflix/encoders/svt_av1/command_builder.py +++ b/fastflix/encoders/svt_av1/command_builder.py @@ -25,9 +25,60 @@ def build(fastflix: FastFlix): f"-tile_columns {settings.tile_columns} " f"-tile_rows {settings.tile_rows} " f"-tier {settings.tier} " + f"-sc_detection {'true' if settings.scene_detection else 'false'} " f"{generate_color_details(fastflix)} " ) + svtav1_params = settings.svtav1_params.copy() + + if not fastflix.current_video.video_settings.remove_hdr: + + if ( + fastflix.current_video.video_settings.color_primaries == "bt2020" + or fastflix.current_video.color_primaries == "bt2020" + ): + svtav1_params.append(f"color-primaries=9") + + if ( + fastflix.current_video.video_settings.color_transfer == "smpte2084" + or fastflix.current_video.color_transfer == "smpte2084" + ): + svtav1_params.append(f"transfer-characteristics=16") + + if ( + fastflix.current_video.video_settings.color_space + and "bt2020" in fastflix.current_video.video_settings.color_space + ) or (fastflix.current_video.color_space and "bt2020" in fastflix.current_video.color_space): + svtav1_params.append(f"matrix-coefficients=9") + + enable_hdr = False + if settings.pix_fmt in ("yuv420p10le", "yuv420p12le"): + + def convert_me(two_numbers, conversion_rate=50_000) -> str: + num_one, num_two = map(int, two_numbers.strip("()").split(",")) + return f"{num_one / conversion_rate:0.4f},{num_two / conversion_rate:0.4f}" + + if fastflix.current_video.master_display: + svtav1_params.append( + "mastering-display=" + f"G({convert_me(fastflix.current_video.master_display.green)})" + f"B({convert_me(fastflix.current_video.master_display.blue)})" + f"R({convert_me(fastflix.current_video.master_display.red)})" + f"WP({convert_me(fastflix.current_video.master_display.white)})" + f"L({convert_me(fastflix.current_video.master_display.luminance, 10_000)})" + ) + enable_hdr = True + + if fastflix.current_video.cll: + svtav1_params.append(f"content-light={fastflix.current_video.cll}") + enable_hdr = True + + if enable_hdr: + svtav1_params.append("enable-hdr=1") + + if svtav1_params: + beginning += f" -svtav1-params \"{':'.join(svtav1_params)}\" " + if not settings.single_pass: pass_log_file = fastflix.current_video.work_path / f"pass_log_file_{secrets.token_hex(10)}" beginning += f'-passlogfile "{pass_log_file}" ' @@ -36,21 +87,21 @@ def build(fastflix: FastFlix): if settings.single_pass: if settings.bitrate: - command_1 = f"{beginning} -b:v {settings.bitrate} -rc 1 {settings.extra} {ending}" + command_1 = f"{beginning} -b:v {settings.bitrate} {settings.extra} {ending}" elif settings.qp is not None: - command_1 = f"{beginning} -qp {settings.qp} -rc 0 {settings.extra} {ending}" + command_1 = f"{beginning} -{settings.qp_mode} {settings.qp} {settings.extra} {ending}" else: return [] return [Command(command=command_1, name=f"{pass_type}", exe="ffmpeg")] else: if settings.bitrate: - command_1 = f"{beginning} -b:v {settings.bitrate} -rc 1 -pass 1 {settings.extra if settings.extra_both_passes else ''} -an -f matroska {null}" - command_2 = f"{beginning} -b:v {settings.bitrate} -rc 1 -pass 2 {settings.extra} {ending}" + command_1 = f"{beginning} -b:v {settings.bitrate} -pass 1 {settings.extra if settings.extra_both_passes else ''} -an -f matroska {null}" + command_2 = f"{beginning} -b:v {settings.bitrate} -pass 2 {settings.extra} {ending}" elif settings.qp is not None: - command_1 = f"{beginning} -qp {settings.qp} -rc 0 -pass 1 {settings.extra if settings.extra_both_passes else ''} -an -f matroska {null}" - command_2 = f"{beginning} -qp {settings.qp} -rc 0 -pass 2 {settings.extra} {ending}" + command_1 = f"{beginning} -qp {settings.qp} -pass 1 {settings.extra if settings.extra_both_passes else ''} -an -f matroska {null}" + command_2 = f"{beginning} -qp {settings.qp} -pass 2 {settings.extra} {ending}" else: return [] return [ diff --git a/fastflix/encoders/svt_av1/settings_panel.py b/fastflix/encoders/svt_av1/settings_panel.py index 15a5b646..81a6dcf0 100644 --- a/fastflix/encoders/svt_av1/settings_panel.py +++ b/fastflix/encoders/svt_av1/settings_panel.py @@ -29,6 +29,12 @@ ] recommended_qp = [ + "14", + "15", + "16", + "17", + "18", + "19", "20", "21", "22", @@ -66,15 +72,17 @@ def __init__(self, parent, main, app: FastFlixApp): self.mode = "QP" - grid.addLayout(self.init_speed(), 0, 0, 1, 2) + grid.addLayout(self.init_preset(), 0, 0, 1, 2) grid.addLayout(self.init_pix_fmt(), 1, 0, 1, 2) grid.addLayout(self.init_tile_rows(), 2, 0, 1, 2) grid.addLayout(self.init_tile_columns(), 3, 0, 1, 2) grid.addLayout(self.init_tier(), 4, 0, 1, 2) - # grid.addLayout(self.init_sc_detection(), 6, 0, 1, 2) - grid.addLayout(self.init_max_mux(), 5, 0, 1, 2) + grid.addLayout(self.init_qp_or_crf(), 5, 0, 1, 2) + grid.addLayout(self.init_sc_detection(), 6, 0, 1, 2) + grid.addLayout(self.init_max_mux(), 7, 0, 1, 2) grid.addLayout(self.init_modes(), 0, 2, 5, 4) - grid.addLayout(self.init_single_pass(), 5, 2, 1, 1) + grid.addLayout(self.init_single_pass(), 6, 2, 1, 1) + grid.addLayout(self.init_svtav1_params(), 5, 2, 1, 4) grid.setRowStretch(8, 1) guide_label = QtWidgets.QLabel( @@ -135,15 +143,41 @@ def init_single_pass(self): layout.addWidget(self.widgets.single_pass) return layout - def init_speed(self): + def init_preset(self): return self._add_combo_box( - label="Speed", + label="Preset", widget_name="speed", - options=[str(x) for x in range(9)], + options=[str(x) for x in range(14)], tooltip="Quality/Speed ratio modifier", opt="speed", ) + def init_qp_or_crf(self): + return self._add_combo_box( + label="Quantization Mode", + widget_name="qp_mode", + options=["qp", "crf"], + tooltip="Use CRF or QP", + opt="qp_mode", + ) + + def init_svtav1_params(self): + layout = QtWidgets.QHBoxLayout() + self.labels.svtav1_params = QtWidgets.QLabel(t("Additional svt av1 params")) + self.labels.svtav1_params.setFixedWidth(200) + tool_tip = f"{t('Extra svt av1 params in opt=1:opt2=0 format')},\n" f"{t('cannot modify generated settings')}" + self.labels.svtav1_params.setToolTip(tool_tip) + layout.addWidget(self.labels.svtav1_params) + self.widgets.svtav1_params = QtWidgets.QLineEdit() + self.widgets.svtav1_params.setToolTip(tool_tip) + self.widgets.svtav1_params.setText( + ":".join(self.app.fastflix.config.encoder_opt(self.profile_name, "svtav1_params")) + ) + self.opts["svtav1_params"] = "svtav1_params" + self.widgets.svtav1_params.textChanged.connect(lambda: self.main.page_update()) + layout.addWidget(self.widgets.svtav1_params) + return layout + def init_modes(self): return self._add_modes(recommended_bitrates, recommended_qp, qp_name="qp") @@ -153,17 +187,21 @@ def mode_update(self): self.main.build_commands() def update_video_encoder_settings(self): + svtav1_params_text = self.widgets.svtav1_params.text().strip() + settings = SVTAV1Settings( speed=self.widgets.speed.currentText(), tile_columns=self.widgets.tile_columns.currentText(), tile_rows=self.widgets.tile_rows.currentText(), single_pass=self.widgets.single_pass.isChecked(), tier=self.widgets.tier.currentText(), - # scene_detection=int(self.widgets.sc_detection.currentIndex()), + scene_detection=bool(self.widgets.sc_detection.currentIndex()), + qp_mode=self.widgets.qp_mode.currentText(), pix_fmt=self.widgets.pix_fmt.currentText().split(":")[1].strip(), max_muxing_queue_size=self.widgets.max_mux.currentText(), extra=self.ffmpeg_extras, extra_both_passes=self.widgets.extra_both_passes.isChecked(), + svtav1_params=svtav1_params_text.split(":") if svtav1_params_text else [], ) encode_type, q_value = self.get_mode_settings() settings.qp = q_value if encode_type == "qp" else None diff --git a/fastflix/entry.py b/fastflix/entry.py index 3d6aa03a..9cfd6ef2 100644 --- a/fastflix/entry.py +++ b/fastflix/entry.py @@ -92,7 +92,7 @@ def startup_options(): import fastflix.widgets.panels.queue_panel import fastflix.widgets.panels.status_panel import fastflix.widgets.panels.subtitle_panel - import fastflix.widgets.profile_window + import fastflix.widgets.windows.profile_window import fastflix.widgets.progress_bar import fastflix.widgets.settings import fastflix.widgets.video_options diff --git a/fastflix/flix.py b/fastflix/flix.py index df9668a8..9eb6cecb 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -23,6 +23,66 @@ logger = logging.getLogger("fastflix") +ffmpeg_valid_color_primaries = [ + "bt709", + "bt470m", + "bt470bg", + "smpte170m", + "smpte240m", + "film", + "bt2020", + "smpte428", + "smpte428_1", + "smpte431", + "smpte432", + "jedec-p22", +] + +ffmpeg_valid_color_transfers = [ + "bt709", + "gamma22", + "gamma28", + "smpte170m", + "smpte240m", + "linear", + "log", + "log100", + "log_sqrt", + "log316", + "iec61966_2_4", + "iec61966-2-4", + "bt1361", + "bt1361e", + "iec61966_2_1", + "iec61966-2-1", + "bt2020_10", + "bt2020_10bit", + "bt2020_12", + "bt2020_12bit", + "smpte2084", + "smpte428", + "smpte428_1", + "arib-std-b67", +] + +ffmpeg_valid_color_space = [ + "rgb", + "bt709", + "fcc", + "bt470bg", + "smpte170m", + "smpte240m", + "ycocg", + "bt2020nc", + "bt2020_ncl", + "bt2020c", + "bt2020_cl", + "smpte2085", + "chroma-derived-nc", + "chroma-derived-c", + "ictcp", +] + def clean_file_string(source): return str(source).strip() @@ -162,17 +222,23 @@ def get_all_concat_items(file): return items -def get_first_concat_item(file): +def get_concat_item(file, location=0): all_items = get_all_concat_items(file) if not all_items: raise FlixError("concat file must start with `file` on each line.") - return all_items[0] + if location == 0: + return all_items[0] + section = len(all_items) // 10 + item_num = int((location * section)) - 1 + if item_num >= len(all_items): + return all_items[-1] + return all_items[item_num] def parse(app: FastFlixApp, **_): source = app.fastflix.current_video.source if source.name.lower().endswith("txt"): - source = get_first_concat_item(source) + source = get_concat_item(source) app.fastflix.current_video.concat = True data = probe(app, source) if "streams" not in data: @@ -241,13 +307,29 @@ def extract_attachment(ffmpeg: Path, source: Path, stream: int, work_dir: Path, def generate_thumbnail_command( - config: Config, source: Path, output: Path, filters: str, start_time: float = 0, input_track: int = 0 + config: Config, + source: Path, + output: Path, + filters: str, + start_time: float = 0, + input_track: int = 0, + enable_opencl: bool = False, ) -> str: - return ( - f'"{config.ffmpeg}" -ss {start_time} -loglevel error -i "{clean_file_string(source)}" ' - f" {filters} -an -y -map_metadata -1 -map 0:{input_track} " - f'-vframes 1 "{clean_file_string(output)}" ' - ) + command_options = [ + f"{config.ffmpeg}", + f"-ss {start_time}" if start_time else "", + "-loglevel warning", + f'-i "{clean_file_string(source)}"', + ("-init_hw_device opencl=ocl -filter_hw_device ocl" if enable_opencl else ""), + filters, + f"-map 0:{input_track}" if "-map" not in filters else "", + "-an", + "-y", + "-map_metadata -1", + "-frames:v 1", + f'"{clean_file_string(output)}"', + ] + return " ".join(command_options) def get_auto_crop( @@ -272,7 +354,7 @@ def get_auto_crop( "-map", f"0:{input_track}", "-vf", - "cropdetect", + "cropdetect=round=2", "-vframes", "10", "-f", @@ -367,6 +449,12 @@ def ffmpeg_audio_encoders(app, config: Config) -> List: return encoders +def ffmpeg_opencl_support(app, config: Config) -> bool: + cmd = execute([f"{config.ffmpeg}", "-hide_banner", "-log_level", "error", "-init_hw_device", "opencl", "-h"]) + app.fastflix.opencl_support = cmd.returncode == 0 + return app.fastflix.opencl_support + + def convert_mastering_display(data: Box) -> Tuple[Box, str]: master_display = None cll = None @@ -469,7 +557,7 @@ def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): hdr10_parser_version_output = check_output([str(config.hdr10plus_parser), "--version"], encoding="utf-8") _, version_string = hdr10_parser_version_output.rsplit(sep=" ", maxsplit=1) hdr10_parser_version = LooseVersion(version_string) - logger.debug(f"Using HDR10 parser version {hdr10_parser_version}") + logger.debug(f"Using HDR10 parser version {str(hdr10_parser_version).strip()}") for stream in app.fastflix.current_video.streams.video: logger.debug(f"Checking for hdr10+ in stream {stream.index}") diff --git a/fastflix/models/config.py b/fastflix/models/config.py index 89a8634f..6578d0e0 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -6,6 +6,7 @@ from distutils.version import StrictVersion from pathlib import Path from typing import Dict, List, Optional +import json from appdirs import user_data_dir from box import Box, BoxError @@ -14,22 +15,11 @@ from fastflix.exceptions import ConfigError, MissingFF from fastflix.models.encode import ( - AOMAV1Settings, - CopySettings, - GIFSettings, - FFmpegNVENCSettings, - SVTAV1Settings, - VP9Settings, - WebPSettings, - rav1eSettings, x264Settings, x265Settings, - NVEncCSettings, - NVEncCAVCSettings, - VCEEncCAVCSettings, - VCEEncCSettings, setting_types, ) +from fastflix.models.profiles import Profile, AudioMatch, MatchItem, MatchType from fastflix.version import __version__ from fastflix.shared import get_config @@ -44,48 +34,6 @@ outdated_settings = ("copy",) -class Profile(BaseModel): - auto_crop: bool = False - keep_aspect_ratio: bool = True - fast_seek: bool = True - rotate: int = 0 - vertical_flip: bool = False - horizontal_flip: bool = False - copy_chapters: bool = True - remove_metadata: bool = True - remove_hdr: bool = False - encoder: str = "HEVC (x265)" - - audio_language: str = "en" - audio_select: bool = True - audio_select_preferred_language: bool = True - audio_select_first_matching: bool = False - - subtitle_language: str = "en" - subtitle_select: bool = True - subtitle_select_preferred_language: bool = True - subtitle_automatic_burn_in: bool = False - subtitle_select_first_matching: bool = False - - x265: Optional[x265Settings] = None - x264: Optional[x264Settings] = None - rav1e: Optional[rav1eSettings] = None - svt_av1: Optional[SVTAV1Settings] = None - vp9: Optional[VP9Settings] = None - aom_av1: Optional[AOMAV1Settings] = None - gif: Optional[GIFSettings] = None - webp: Optional[WebPSettings] = None - copy_settings: Optional[CopySettings] = None - ffmpeg_hevc_nvenc: Optional[FFmpegNVENCSettings] = None - nvencc_hevc: Optional[NVEncCSettings] = None - nvencc_avc: Optional[NVEncCAVCSettings] = None - vceencc_hevc: Optional[VCEEncCSettings] = None - vceencc_avc: Optional[VCEEncCAVCSettings] = None - - -empty_profile = Profile(x265=x265Settings()) - - def get_preset_defaults(): return { "Standard Profile": Profile(x265=x265Settings()), @@ -199,8 +147,56 @@ def opt(self, profile_option_name, default=NO_OPT): return getattr(self.profiles[self.selected_profile], profile_option_name, default) return getattr(self.profiles[self.selected_profile], profile_option_name) + def advanced_opt(self, profile_option_name, default=NO_OPT): + advanced_settings = getattr(self.profiles[self.selected_profile], "advanced_options") + if default != NO_OPT: + return getattr(advanced_settings, profile_option_name, default) + return getattr(advanced_settings, profile_option_name) + + def profile_v1_to_v2(self, name, raw_profile): + logger.info(f'Upgrading profile "{name}" to version 2') + try: + audio_language = raw_profile.pop("audio_language") + except KeyError: + audio_language = "en" + + try: + audio_select = raw_profile.pop("audio_select") + except KeyError: + audio_select = False + + try: + audio_select_preferred_language = raw_profile.pop("audio_select_preferred_language") + except KeyError: + audio_select_preferred_language = False + + try: + audio_select_first_matching = raw_profile.pop("audio_select_first_matching") + except KeyError: + audio_select_first_matching = False + + try: + del raw_profile["profile_version"] + except KeyError: + pass + + try: + del raw_profile["audio_filters"] + except KeyError: + pass + + if audio_select: + new_match = AudioMatch( + match_type=MatchType.FIRST if audio_select_first_matching else MatchType.ALL, + match_item=MatchItem.LANGUAGE if audio_select_preferred_language else MatchItem.ALL, + match_input=audio_language if audio_select_preferred_language else "*", + ) + + return Profile(profile_version=2, audio_filters=[new_match], **raw_profile) + return Profile(profile_version=2, **raw_profile) + def load(self): - if not self.config_path.exists(): + if not self.config_path.exists() or self.config_path.stat().st_size < 10: logger.debug(f"Creating new config file {self.config_path}") self.config_path.parent.mkdir(parents=True, exist_ok=True) self.save() @@ -215,6 +211,9 @@ def load(self): data = Box.from_yaml(filename=self.config_path) except BoxError as err: raise ConfigError(f"{self.config_path}: {err}") + if "version" not in data: + raise ConfigError(f"Corrupt config file. Please fix or remove {self.config_path}") + if StrictVersion(__version__) < StrictVersion(data.version): logger.warning( f"This FastFlix version ({__version__}) is older " @@ -227,24 +226,10 @@ def load(self): if key == "profiles": self.profiles = {} for k, v in value.items(): - if k in get_preset_defaults().keys(): - continue - profile = Profile() - for setting_name, setting in v.items(): - if setting_name in outdated_settings: - continue - if setting_name in setting_types.keys() and setting is not None: - try: - setattr(profile, setting_name, setting_types[setting_name](**setting)) - except (ValueError, TypeError): - logger.exception(f"Could not set profile setting {setting_name}") - else: - try: - setattr(profile, setting_name, setting) - except (ValueError, TypeError): - logger.exception(f"Could not set profile setting {setting_name}") - - self.profiles[k] = profile + if v.get("profile_version", 1) == 1: + self.profiles[k] = self.profile_v1_to_v2(k, v) + else: + self.profiles[k] = Profile(**v) continue if key in self and key not in ("config_path", "version"): setattr(self, key, Path(value) if key in paths and value else value) @@ -276,7 +261,10 @@ def save(self): for k, v in items.items(): if isinstance(v, Path): items[k] = str(v.absolute()) - items["profiles"] = {k: v.dict() for k, v in self.profiles.items() if k not in get_preset_defaults().keys()} + # Need to use pydantics converters, but those only run with `.json` and not `.dict` + items["profiles"] = { + k: json.loads(v.json()) for k, v in self.profiles.items() if k not in get_preset_defaults().keys() + } return Box(items).to_yaml(filename=self.config_path, default_flow_style=False) @property diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index aa45aab1..29a1552a 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -127,6 +127,7 @@ class NVEncCSettings(EncoderSettings): b_ref_mode: str = "disabled" ref: Optional[str] = None metrics: bool = False + force_ten_bit: bool = False class NVEncCAVCSettings(EncoderSettings): @@ -217,11 +218,13 @@ class SVTAV1Settings(EncoderSettings): tile_columns: str = "0" tile_rows: str = "0" tier: str = "main" - # scene_detection: str = "false" + scene_detection: bool = False single_pass: bool = False - speed: str = "7" + speed: str = "7" # Renamed preset in svtav1 encoder qp: Optional[Union[int, float]] = 24 + qp_mode: str = "qp" bitrate: Optional[str] = None + svtav1_params: List[str] = Field(default_factory=list) class VP9Settings(EncoderSettings): @@ -261,6 +264,8 @@ class GIFSettings(EncoderSettings): name = "GIF" fps: int = 15 dither: str = "sierra2_4a" + max_colors: str = "256" + stats_mode: str = "full" class CopySettings(EncoderSettings): diff --git a/fastflix/models/fastflix.py b/fastflix/models/fastflix.py index 13d25196..9302f6ed 100644 --- a/fastflix/models/fastflix.py +++ b/fastflix/models/fastflix.py @@ -22,6 +22,7 @@ class FastFlix(BaseModel): ffmpeg_version: str = "" ffmpeg_config: List[str] = "" ffprobe_version: str = "" + opencl_support: bool = False # Queues worker_queue: Any = None diff --git a/fastflix/models/profiles.py b/fastflix/models/profiles.py new file mode 100644 index 00000000..e507f77b --- /dev/null +++ b/fastflix/models/profiles.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from pathlib import Path +from typing import List, Optional, Union, Dict + +from pydantic import BaseModel, Field, validator +from box import Box +from enum import Enum + +from fastflix.models.encode import ( + AOMAV1Settings, + CopySettings, + GIFSettings, + FFmpegNVENCSettings, + SVTAV1Settings, + VP9Settings, + WebPSettings, + rav1eSettings, + x264Settings, + x265Settings, + NVEncCSettings, + NVEncCAVCSettings, + VCEEncCAVCSettings, + VCEEncCSettings, +) + +__all__ = ["MatchItem", "MatchType", "AudioMatch", "Profile", "SubtitleMatch", "AdvancedOptions"] + + +class MatchItem(Enum): + ALL = 1 + TITLE = 2 + TRACK = 3 + LANGUAGE = 4 + CHANNELS = 5 + + +class MatchType(Enum): + ALL = 1 + FIRST = 2 + LAST = 3 + + +class AudioMatch(BaseModel): + match_type: Union[MatchType, List[MatchType]] # TODO figure out why when saved becomes list in yaml + match_item: Union[MatchItem, List[MatchType]] + match_input: str = "*" + conversion: Optional[str] = None + bitrate: Optional[str] = None + downmix: Optional[int] = None + + @validator("match_type") + def match_type_must_be_enum(cls, v): + if isinstance(v, list): + return MatchType(v[0]) + return MatchType(v) + + @validator("match_item") + def match_item_must_be_enum(cls, v): + if isinstance(v, list): + return MatchType(v[0]) + return MatchItem(v) + + +class SubtitleMatch(BaseModel): + match_type: Union[MatchType, List[MatchType]] + match_item: Union[MatchItem, List[MatchType]] + match_input: str + + +# TODO upgrade path from old profile to new profile + + +class AdvancedOptions(BaseModel): + video_speed: float = 1 + deblock: Optional[str] = None + deblock_size: int = 16 + tone_map: str = "hable" + vsync: Optional[str] = None + brightness: Optional[str] = None + saturation: Optional[str] = None + contrast: Optional[str] = None + maxrate: Optional[int] = None + bufsize: Optional[int] = None + source_fps: Optional[str] = None + output_fps: Optional[str] = None + color_space: Optional[str] = None + color_transfer: Optional[str] = None + color_primaries: Optional[str] = None + denoise: Optional[str] = None + denoise_type_index: int = 0 + denoise_strength_index: int = 0 + + +class Profile(BaseModel): + profile_version: Optional[int] = 1 + auto_crop: bool = False + keep_aspect_ratio: bool = True + fast_seek: bool = True + rotate: int = 0 + vertical_flip: bool = False + horizontal_flip: bool = False + copy_chapters: bool = True + remove_metadata: bool = True + remove_hdr: bool = False + encoder: str = "HEVC (x265)" + + audio_filters: Optional[List[AudioMatch]] = None + # subtitle_filters: Optional[List[SubtitleMatch]] = None + + # Legacy Audio, here to properly import old profiles + audio_language: Optional[str] = None + audio_select: Optional[bool] = None + audio_select_preferred_language: Optional[bool] = None + audio_select_first_matching: Optional[bool] = None + + subtitle_language: Optional[str] = None + subtitle_select: Optional[bool] = None + subtitle_select_preferred_language: Optional[bool] = None + subtitle_automatic_burn_in: Optional[bool] = None + subtitle_select_first_matching: Optional[bool] = None + + advanced_options: AdvancedOptions = Field(default_factory=AdvancedOptions) + + x265: Optional[x265Settings] = None + x264: Optional[x264Settings] = None + rav1e: Optional[rav1eSettings] = None + svt_av1: Optional[SVTAV1Settings] = None + vp9: Optional[VP9Settings] = None + aom_av1: Optional[AOMAV1Settings] = None + gif: Optional[GIFSettings] = None + webp: Optional[WebPSettings] = None + copy_settings: Optional[CopySettings] = None + ffmpeg_hevc_nvenc: Optional[FFmpegNVENCSettings] = None + nvencc_hevc: Optional[NVEncCSettings] = None + nvencc_avc: Optional[NVEncCAVCSettings] = None + vceencc_hevc: Optional[VCEEncCSettings] = None + vceencc_avc: Optional[VCEEncCAVCSettings] = None diff --git a/fastflix/program_downloads.py b/fastflix/program_downloads.py index da7b06f3..22af2d60 100644 --- a/fastflix/program_downloads.py +++ b/fastflix/program_downloads.py @@ -75,11 +75,11 @@ def stop_me(): message(t("Download Cancelled")) return - gpl_ffmpeg = [asset for asset in data["assets"] if asset["name"].endswith("win64-gpl.zip")] + gpl_ffmpeg = [asset for asset in data["assets"] if "master-latest-win64-gpl.zip" in asset["name"]] if not gpl_ffmpeg: shutil.rmtree(extract_folder, ignore_errors=True) message( - t("Could not find any matching FFmpeg ending with 'win64-gpl.zip' with") + t("Could not find any matching FFmpeg containing 'win64-gpl-5' with") + f" {t('latest release from')} " "https://github.com/BtbN/FFmpeg-Builds/releases/ " ) diff --git a/fastflix/version.py b/fastflix/version.py index 45a0bd00..8ee65561 100644 --- a/fastflix/version.py +++ b/fastflix/version.py @@ -1,4 +1,4 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -__version__ = "4.6.0" +__version__ = "4.7.0" __author__ = "Chris Griffith" diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 890ad592..1ce35869 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -116,7 +116,7 @@ def run(self): ) _, version_string = hdr10_parser_version_output.rsplit(sep=" ", maxsplit=1) hdr10_parser_version = LooseVersion(version_string) - self.main.thread_logging_signal.emit(f"Using HDR10 parser version {hdr10_parser_version}") + self.main.thread_logging_signal.emit(f"Using HDR10 parser version {str(hdr10_parser_version).strip()}") ffmpeg_command = [ str(self.app.fastflix.config.ffmpeg), diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py index 76b99645..06186830 100644 --- a/fastflix/widgets/container.py +++ b/fastflix/widgets/container.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- import logging -import os import shutil import sys import time @@ -25,10 +24,10 @@ from fastflix.widgets.changes import Changes from fastflix.widgets.logs import Logs from fastflix.widgets.main import Main -from fastflix.widgets.profile_window import ProfileWindow +from fastflix.widgets.windows.profile_window import ProfileWindow from fastflix.widgets.progress_bar import ProgressBar, Task from fastflix.widgets.settings import Settings -from fastflix.widgets.concat import ConcatWindow +from fastflix.widgets.windows.concat import ConcatWindow logger = logging.getLogger("fastflix") @@ -67,6 +66,24 @@ def __init__(self, app: FastFlixApp, **kwargs): QScrollArea{ border: 1px solid #919191; } """ ) + # self.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint) + self.moveFlag = False + + # def mousePressEvent(self, event): + # if event.button() == QtCore.Qt.LeftButton: + # self.moveFlag = True + # self.movePosition = event.globalPos() - self.pos() + # self.setCursor(QtGui.QCursor(QtCore.Qt.OpenHandCursor)) + # event.accept() + # + # def mouseMoveEvent(self, event): + # if QtCore.Qt.LeftButton and self.moveFlag: + # self.move(event.globalPos() - self.movePosition) + # event.accept() + # + # def mouseReleaseEvent(self, event): + # self.moveFlag = False + # self.setCursor(QtCore.Qt.ArrowCursor) def closeEvent(self, a0: QtGui.QCloseEvent) -> None: if self.pb: @@ -109,6 +126,7 @@ def si(self, widget): def init_menu(self): menubar = self.menuBar() menubar.setNativeMenuBar(False) + menubar.setFixedWidth(260) file_menu = menubar.addMenu(t("File")) @@ -205,7 +223,10 @@ def show_setting(self): self.setting.show() def new_profile(self): - self.profile.show() + if not self.app.fastflix.current_video: + error_message(t("Please load in a video to configure a new profile")) + else: + self.profile.show() def show_profile(self): self.profile_details = ProfileDetails( @@ -307,6 +328,10 @@ def __init__(self, profile_name, profile): profile_title.setFont(QtGui.QFont("helvetica", 10, weight=70)) main_section.addWidget(profile_title) for k, v in profile.dict().items(): + if k == "advanced_options": + continue + if k.lower().startswith("audio") or k.lower() == "profile_version": + continue if k not in setting_types.keys(): item_1 = QtWidgets.QLabel(" ".join(str(k).split("_")).title()) item_2 = QtWidgets.QLabel(str(v)) @@ -326,5 +351,25 @@ def __init__(self, profile_name, profile): setting = getattr(profile, setting_name) if setting: self.layout.addWidget(self.profile_widget(setting)) + + splitter = QtWidgets.QWidget() + splitter.setMaximumWidth(1) + splitter.setStyleSheet("background-color: #999999") + self.layout.addWidget(splitter) + + advanced_section = QtWidgets.QVBoxLayout(self) + advanced_section.addWidget(QtWidgets.QLabel(t("Advanced Options"))) + for k, v in profile.advanced_options.dict().items(): + if k.endswith("_index"): + continue + item_1 = QtWidgets.QLabel(k) + item_2 = QtWidgets.QLabel(str(v)) + item_2.setMaximumWidth(150) + inner_layout = QtWidgets.QHBoxLayout() + inner_layout.addWidget(item_1) + inner_layout.addWidget(item_2) + advanced_section.addLayout(inner_layout) + self.layout.addLayout(advanced_section) + self.setMinimumWidth(780) self.setLayout(self.layout) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 25c13e5c..c5abe6fb 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- import copy import datetime -import importlib.machinery # Needed for pyinstaller import logging import math import os @@ -30,7 +29,7 @@ get_auto_crop, parse, parse_hdr_details, - get_first_concat_item, + get_concat_item, ) from fastflix.language import t from fastflix.models.fastflix_app import FastFlixApp @@ -50,7 +49,7 @@ from fastflix.widgets.background_tasks import ThumbnailCreator from fastflix.widgets.progress_bar import ProgressBar, Task from fastflix.widgets.video_options import VideoOptions -from fastflix.widgets.large_preview import LargePreview +from fastflix.widgets.windows.large_preview import LargePreview logger = logging.getLogger("fastflix") @@ -299,21 +298,23 @@ def init_top_bar(self): top_bar.addWidget(self.widgets.profile_box) top_bar.addWidget(QtWidgets.QSplitter(QtCore.Qt.Horizontal)) - add_profile = QtWidgets.QPushButton(QtGui.QIcon(self.get_icon("onyx-new-profile")), f' {t("New Profile")}') + self.add_profile = QtWidgets.QPushButton( + QtGui.QIcon(self.get_icon("onyx-new-profile")), f' {t("New Profile")}' + ) # add_profile.setFixedSize(QtCore.QSize(40, 40)) - add_profile.setFixedHeight(40) - add_profile.setIconSize(QtCore.QSize(20, 20)) - add_profile.setToolTip(t("Profile_newprofiletooltip")) + self.add_profile.setFixedHeight(40) + self.add_profile.setIconSize(QtCore.QSize(20, 20)) + self.add_profile.setToolTip(t("Profile_newprofiletooltip")) # add_profile.setLayoutDirection(QtCore.Qt.RightToLeft) - add_profile.clicked.connect(lambda: self.container.new_profile()) - + self.add_profile.clicked.connect(lambda: self.container.new_profile()) + self.add_profile.setDisabled(True) # options = QtWidgets.QPushButton(QtGui.QIcon(self.get_icon("settings")), "") # options.setFixedSize(QtCore.QSize(40, 40)) # options.setIconSize(QtCore.QSize(22, 22)) # options.setToolTip(t("Settings")) # options.clicked.connect(lambda: self.container.show_setting()) - top_bar.addWidget(add_profile) + top_bar.addWidget(self.add_profile) top_bar.addStretch(1) # top_bar.addWidget(options) @@ -1157,6 +1158,7 @@ def disable_all(self): button.setDisabled(True) self.output_path_button.setDisabled(True) self.output_video_path_widget.setDisabled(True) + self.add_profile.setDisabled(True) def enable_all(self): for name, widget in self.widgets.items(): @@ -1174,6 +1176,7 @@ def enable_all(self): self.widgets.scale.height.setDisabled(True) self.output_path_button.setEnabled(True) self.output_video_path_widget.setEnabled(True) + self.add_profile.setEnabled(True) @reusables.log_exception("fastflix", show_traceback=False) def scale_update(self): @@ -1540,6 +1543,7 @@ def generate_thumbnail(self): filters = helpers.generate_filters( start_filters="select=eq(pict_type\\,I)" if self.widgets.thumb_key.isChecked() else None, custom_filters=custom_filters, + enable_opencl=self.app.fastflix.opencl_support, **settings, ) @@ -1548,7 +1552,8 @@ def generate_thumbnail(self): source=self.source_material, output=self.thumb_file, filters=filters, - start_time=self.preview_place, + enable_opencl=self.app.fastflix.opencl_support, + start_time=self.preview_place if not self.app.fastflix.current_video.concat else None, input_track=self.app.fastflix.current_video.video_settings.selected_track, ) try: @@ -1560,7 +1565,9 @@ def generate_thumbnail(self): @property def source_material(self): - return get_first_concat_item(self.input_video) if self.app.fastflix.current_video.concat else self.input_video + if self.app.fastflix.current_video.concat: + return get_concat_item(self.input_video, self.widgets.thumb_time.value()) + return self.input_video @staticmethod def thread_logger(text): diff --git a/fastflix/widgets/panels/abstract_list.py b/fastflix/widgets/panels/abstract_list.py index 1df79575..f18c5e38 100644 --- a/fastflix/widgets/panels/abstract_list.py +++ b/fastflix/widgets/panels/abstract_list.py @@ -64,7 +64,7 @@ def _new_source(self, widgets): def new_source(self, *args, **kwargs): raise NotImplementedError() - def reorder(self, update=True): + def reorder(self, update=True, height=66): if not self.inner_layout: return for widget in self.tracks: @@ -92,7 +92,7 @@ def reorder(self, update=True): self.tracks[0].set_first(True) self.tracks[-1].set_last(True) self.inner_layout.addStretch() - new_height = len(self.tracks) * 66 + new_height = len(self.tracks) * height if len(self.tracks) <= 4: new_height += 30 self.inner_widget.setFixedHeight(new_height) diff --git a/fastflix/widgets/panels/advanced_panel.py b/fastflix/widgets/panels/advanced_panel.py index e78c372c..de8f69c0 100644 --- a/fastflix/widgets/panels/advanced_panel.py +++ b/fastflix/widgets/panels/advanced_panel.py @@ -10,6 +10,8 @@ from fastflix.models.fastflix_app import FastFlixApp from fastflix.models.video import VideoSettings from fastflix.resources import get_icon +from fastflix.models.profiles import AdvancedOptions +from fastflix.flix import ffmpeg_valid_color_primaries, ffmpeg_valid_color_transfers, ffmpeg_valid_color_space logger = logging.getLogger("fastflix") @@ -55,67 +57,8 @@ }, } -ffmpeg_valid_color_primaries = [ - "bt709", - "bt470m", - "bt470bg", - "smpte170m", - "smpte240m", - "film", - "bt2020", - "smpte428", - "smpte428_1", - "smpte431", - "smpte432", - "jedec-p22", -] - -ffmpeg_valid_color_transfers = [ - "bt709", - "gamma22", - "gamma28", - "smpte170m", - "smpte240m", - "linear", - "log", - "log100", - "log_sqrt", - "log316", - "iec61966_2_4", - "iec61966-2-4", - "bt1361", - "bt1361e", - "iec61966_2_1", - "iec61966-2-1", - "bt2020_10", - "bt2020_10bit", - "bt2020_12", - "bt2020_12bit", - "smpte2084", - "smpte428", - "smpte428_1", - "arib-std-b67", -] - -ffmpeg_valid_color_space = [ - "rgb", - "bt709", - "fcc", - "bt470bg", - "smpte170m", - "smpte240m", - "ycocg", - "bt2020nc", - "bt2020_ncl", - "bt2020c", - "bt2020_cl", - "smpte2085", - "chroma-derived-nc", - "chroma-derived-c", - "ictcp", -] - vsync = ["auto", "passthrough", "cfr", "vfr", "drop"] +tone_map_items = ["none", "clip", "linear", "gamma", "reinhard", "hable", "mobius"] def non(value): @@ -146,6 +89,7 @@ def __init__(self, parent, app: FastFlixApp): self.attachments = Box() self.updating = False self.only_int = QtGui.QIntValidator() + self.only_float = QtGui.QDoubleValidator() self.layout = QtWidgets.QGridLayout() @@ -164,6 +108,8 @@ def __init__(self, parent, app: FastFlixApp): self.init_color_info() self.add_spacer() self.init_vbv() + # self.add_spacer() + # self.init_custom_filters() self.last_row += 1 @@ -175,11 +121,7 @@ def __init__(self, parent, app: FastFlixApp): ico = QtGui.QIcon(get_icon("onyx-warning", app.fastflix.config.theme)) warning_label.setPixmap(ico.pixmap(22)) - self.layout.addWidget(warning_label, self.last_row, 0, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget( - QtWidgets.QLabel(t("Advanced settings are currently not saved in Profiles")), self.last_row, 1, 1, 4 - ) - for i in range(6): + for i in range(1, 7): self.layout.setColumnMinimumWidth(i, 155) self.setLayout(self.layout) @@ -188,7 +130,14 @@ def add_spacer(self): spacer_widget = QtWidgets.QWidget(self) spacer_widget.setFixedHeight(1) spacer_widget.setStyleSheet("background-color: #ddd") - self.layout.addWidget(spacer_widget, self.last_row, 0, 1, 6) + self.layout.addWidget(spacer_widget, self.last_row, 0, 1, 7) + + def add_row_label(self, label, row_number): + label = QtWidgets.QLabel(label) + label.setFixedWidth(100) + if self.app.fastflix.config.theme == "onyx": + label.setStyleSheet("color: #b5b5b5") + self.layout.addWidget(label, row_number, 0, alignment=QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) def init_fps(self): self.incoming_fps_widget = QtWidgets.QLineEdit() @@ -216,23 +165,25 @@ def init_fps(self): self.vsync_widget.addItems(vsync) self.vsync_widget.currentIndexChanged.connect(self.page_update) + self.add_row_label(t("Frame Rate"), self.last_row) + self.layout.addWidget( - QtWidgets.QLabel(t("Override Source FPS")), self.last_row, 0, alignment=QtCore.Qt.AlignRight + QtWidgets.QLabel(t("Override Source FPS")), self.last_row, 1, alignment=QtCore.Qt.AlignRight ) - self.layout.addWidget(self.incoming_fps_widget, self.last_row, 1) - self.layout.addWidget(self.incoming_same_as_source, self.last_row, 2) + self.layout.addWidget(self.incoming_fps_widget, self.last_row, 2) + self.layout.addWidget(self.incoming_same_as_source, self.last_row, 3) self.layout.addWidget( - QtWidgets.QLabel(t("Source Frame Rate")), self.last_row, 4, alignment=QtCore.Qt.AlignRight + QtWidgets.QLabel(t("Source Frame Rate")), self.last_row, 5, alignment=QtCore.Qt.AlignRight ) - self.layout.addWidget(self.source_frame_rate, self.last_row, 5) + self.layout.addWidget(self.source_frame_rate, self.last_row, 6) self.last_row += 1 - self.layout.addWidget(QtWidgets.QLabel(t("Output FPS")), self.last_row, 0, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.outgoing_fps_widget, self.last_row, 1) - self.layout.addWidget(self.outgoing_same_as_source, self.last_row, 2) + self.layout.addWidget(QtWidgets.QLabel(t("Output FPS")), self.last_row, 1, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.outgoing_fps_widget, self.last_row, 2) + self.layout.addWidget(self.outgoing_same_as_source, self.last_row, 3) - self.layout.addWidget(QtWidgets.QLabel(t("vsync")), self.last_row, 4, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.vsync_widget, self.last_row, 5) + self.layout.addWidget(QtWidgets.QLabel(t("vsync")), self.last_row, 5, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.vsync_widget, self.last_row, 6) self.last_row += 1 @@ -253,41 +204,46 @@ def init_video_speed(self): self.video_speed_widget = QtWidgets.QComboBox() self.video_speed_widget.addItems(video_speeds.keys()) self.video_speed_widget.currentIndexChanged.connect(self.page_update) - self.layout.addWidget(QtWidgets.QLabel(t("Video Speed")), self.last_row, 0, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.video_speed_widget, self.last_row, 1) - self.layout.addWidget(QtWidgets.QLabel(t("Warning: Audio will not be modified")), self.last_row, 2, 1, 3) + self.layout.addWidget(QtWidgets.QLabel(t("Video Speed")), self.last_row, 1, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.video_speed_widget, self.last_row, 2) + self.layout.addWidget(QtWidgets.QLabel(t("Warning: Audio will not be modified")), self.last_row, 3, 1, 3) # def init_tone_map(self): # self.last_row += 1 self.tone_map_widget = QtWidgets.QComboBox() - self.tone_map_widget.addItems(["none", "clip", "linear", "gamma", "reinhard", "hable", "mobius"]) + self.tone_map_widget.addItems(tone_map_items) self.tone_map_widget.setCurrentIndex(5) self.tone_map_widget.currentIndexChanged.connect(self.page_update) self.layout.addWidget( - QtWidgets.QLabel(t("HDR -> SDR Tone Map")), self.last_row, 4, alignment=QtCore.Qt.AlignRight + QtWidgets.QLabel(t("HDR -> SDR Tone Map")), self.last_row, 5, alignment=QtCore.Qt.AlignRight ) - self.layout.addWidget(self.tone_map_widget, self.last_row, 5) + self.layout.addWidget(self.tone_map_widget, self.last_row, 6) def init_eq(self): self.last_row += 1 self.brightness_widget = QtWidgets.QLineEdit() - self.brightness_widget.setPlaceholderText("0") + self.brightness_widget.setToolTip("Default is: 0") + self.brightness_widget.setValidator(self.only_float) self.brightness_widget.textChanged.connect(lambda: self.page_update(build_thumbnail=True)) self.contrast_widget = QtWidgets.QLineEdit() - self.contrast_widget.setPlaceholderText("1") + self.contrast_widget.setToolTip("Default is: 1") + self.contrast_widget.setValidator(self.only_float) self.contrast_widget.textChanged.connect(lambda: self.page_update(build_thumbnail=True)) self.saturation_widget = QtWidgets.QLineEdit() - self.saturation_widget.setPlaceholderText("1") + self.saturation_widget.setToolTip("Default is: 1") + self.saturation_widget.setValidator(self.only_float) self.saturation_widget.textChanged.connect(lambda: self.page_update(build_thumbnail=True)) - self.layout.addWidget(QtWidgets.QLabel(t("Brightness")), self.last_row, 0, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.brightness_widget, self.last_row, 1) - self.layout.addWidget(QtWidgets.QLabel(t("Contrast")), self.last_row, 2, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.contrast_widget, self.last_row, 3) - self.layout.addWidget(QtWidgets.QLabel(t("Saturation")), self.last_row, 4, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.saturation_widget, self.last_row, 5) + self.add_row_label(t("Equalizer"), self.last_row) + + self.layout.addWidget(QtWidgets.QLabel(t("Brightness")), self.last_row, 1, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.brightness_widget, self.last_row, 2) + self.layout.addWidget(QtWidgets.QLabel(t("Contrast")), self.last_row, 3, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.contrast_widget, self.last_row, 4) + self.layout.addWidget(QtWidgets.QLabel(t("Saturation")), self.last_row, 5, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.saturation_widget, self.last_row, 6) def init_denoise(self): self.last_row += 1 @@ -301,10 +257,11 @@ def init_denoise(self): self.denoise_strength_widget.setCurrentIndex(0) self.denoise_strength_widget.currentIndexChanged.connect(self.page_update) - self.layout.addWidget(QtWidgets.QLabel(t("Denoise")), self.last_row, 0, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.denoise_type_widget, self.last_row, 1) - self.layout.addWidget(QtWidgets.QLabel(t("Strength")), self.last_row, 2, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.denoise_strength_widget, self.last_row, 3) + self.add_row_label(t("Denoise"), self.last_row) + self.layout.addWidget(QtWidgets.QLabel(t("Method")), self.last_row, 1, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.denoise_type_widget, self.last_row, 2) + self.layout.addWidget(QtWidgets.QLabel(t("Strength")), self.last_row, 3, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.denoise_strength_widget, self.last_row, 4) def init_deblock(self): self.last_row += 1 @@ -320,10 +277,11 @@ def init_deblock(self): self.deblock_size_widget.currentIndexChanged.connect(self.page_update) self.deblock_size_widget.setCurrentIndex(2) - self.layout.addWidget(QtWidgets.QLabel(t("Deblock")), self.last_row, 0, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.deblock_widget, self.last_row, 1) - self.layout.addWidget(QtWidgets.QLabel(t("Block Size")), self.last_row, 2, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.deblock_size_widget, self.last_row, 3) + self.add_row_label(t("Deblock"), self.last_row) + self.layout.addWidget(QtWidgets.QLabel(t("Strength")), self.last_row, 1, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.deblock_widget, self.last_row, 2) + self.layout.addWidget(QtWidgets.QLabel(t("Block Size")), self.last_row, 3, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.deblock_size_widget, self.last_row, 4) def init_color_info(self): self.last_row += 1 @@ -342,44 +300,44 @@ def init_color_info(self): self.color_space_widget.addItems(ffmpeg_valid_color_space) self.color_space_widget.currentIndexChanged.connect(self.page_update) - self.layout.addWidget(QtWidgets.QLabel(t("Color Primaries")), self.last_row, 0, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.color_primaries_widget, self.last_row, 1) - self.layout.addWidget(QtWidgets.QLabel(t("Color Transfer")), self.last_row, 2, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.color_transfer_widget, self.last_row, 3) - self.layout.addWidget(QtWidgets.QLabel(t("Color Space")), self.last_row, 4, alignment=QtCore.Qt.AlignRight) - self.layout.addWidget(self.color_space_widget, self.last_row, 5) + self.add_row_label(t("Color Formats"), self.last_row) + self.layout.addWidget(QtWidgets.QLabel(t("Color Primaries")), self.last_row, 1, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.color_primaries_widget, self.last_row, 2) + self.layout.addWidget(QtWidgets.QLabel(t("Color Transfer")), self.last_row, 3, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.color_transfer_widget, self.last_row, 4) + self.layout.addWidget(QtWidgets.QLabel(t("Color Space")), self.last_row, 5, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.color_space_widget, self.last_row, 6) def init_vbv(self): self.last_row += 1 self.maxrate_widget = QtWidgets.QLineEdit() - self.maxrate_widget.setPlaceholderText("3000") - self.maxrate_widget.setDisabled(True) + # self.maxrate_widget.setPlaceholderText("3000") self.maxrate_widget.setValidator(self.only_int) self.maxrate_widget.textChanged.connect(self.page_update) self.bufsize_widget = QtWidgets.QLineEdit() - self.bufsize_widget.setPlaceholderText("3000") - self.bufsize_widget.setDisabled(True) + # self.bufsize_widget.setPlaceholderText("3000") self.bufsize_widget.setValidator(self.only_int) self.bufsize_widget.textChanged.connect(self.page_update) - self.vbv_checkbox = QtWidgets.QCheckBox(t("Enable VBV")) - self.vbv_checkbox.toggled.connect(self.vbv_check_changed) + # self.vbv_checkbox = QtWidgets.QCheckBox(t("Enable VBV")) + # self.vbv_checkbox.toggled.connect(self.vbv_check_changed) + self.add_row_label(f'{t("Video Buffering")}\n{t("Verifier")} (VBV)', self.last_row) self.layout.addWidget( - QtWidgets.QLabel(f'{t("Maxrate")} (kbps)'), self.last_row, 0, alignment=QtCore.Qt.AlignRight + QtWidgets.QLabel(f'{t("Maxrate")} (kbps)'), self.last_row, 1, alignment=QtCore.Qt.AlignRight ) - self.layout.addWidget(self.maxrate_widget, self.last_row, 1) + self.layout.addWidget(self.maxrate_widget, self.last_row, 2) self.layout.addWidget( - QtWidgets.QLabel(f'{t("Bufsize")} (kbps)'), self.last_row, 2, alignment=QtCore.Qt.AlignRight + QtWidgets.QLabel(f'{t("Bufsize")} (kbps)'), self.last_row, 3, alignment=QtCore.Qt.AlignRight ) - self.layout.addWidget(self.bufsize_widget, self.last_row, 3) - self.layout.addWidget(self.vbv_checkbox, self.last_row, 4) + self.layout.addWidget(self.bufsize_widget, self.last_row, 4) + self.layout.addWidget(QtWidgets.QLabel("Both must have values to be enabled"), self.last_row, 5, 1, 2) - def vbv_check_changed(self): - self.bufsize_widget.setEnabled(self.vbv_checkbox.isChecked()) - self.maxrate_widget.setEnabled(self.vbv_checkbox.isChecked()) - self.page_update() + # def vbv_check_changed(self): + # self.bufsize_widget.setEnabled(self.vbv_checkbox.isChecked()) + # self.maxrate_widget.setEnabled(self.vbv_checkbox.isChecked()) + # self.page_update() # def init_subtitle_overlay_fix(self): # self.last_row += 1 @@ -388,6 +346,21 @@ def vbv_check_changed(self): # crop=1904:800:6:140 # (800 + 140) - 1080 == -140 + def init_custom_filters(self): + self.last_row += 1 + + self.first_filters = QtWidgets.QLineEdit() + self.first_filters.textChanged.connect(self.page_update) + + self.second_filters = QtWidgets.QLineEdit() + self.second_filters.textChanged.connect(self.page_update) + + self.add_row_label(t("Custom Filters"), self.last_row) + self.layout.addWidget(QtWidgets.QLabel(t("First Pass")), self.last_row, 1, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.first_filters, self.last_row, 2, 1, 2) + self.layout.addWidget(QtWidgets.QLabel(t("Second Pass")), self.last_row, 4, alignment=QtCore.Qt.AlignRight) + self.layout.addWidget(self.second_filters, self.last_row, 5, 1, 2) + def update_settings(self): if self.updating or not self.app.fastflix.current_video: return False @@ -402,6 +375,9 @@ def update_settings(self): self.app.fastflix.current_video.video_settings.saturation = self.saturation_widget.text() or None self.app.fastflix.current_video.video_settings.contrast = self.contrast_widget.text() or None + # self.app.fastflix.current_video.video_settings.first_pass_filters = self.first_filters.text() or None + # self.app.fastflix.current_video.video_settings.second_filters = self.second_filters.text() or None + if not self.incoming_same_as_source.isChecked(): self.app.fastflix.current_video.video_settings.source_fps = self.incoming_fps_widget.text() if not self.outgoing_same_as_source.isChecked(): @@ -429,7 +405,7 @@ def update_settings(self): else: self.app.fastflix.current_video.video_settings.color_space = self.color_space_widget.currentText() - if self.vbv_checkbox.isChecked() and self.maxrate_widget.text() and self.bufsize_widget.text(): + if self.maxrate_widget.text() and self.bufsize_widget.text(): self.app.fastflix.current_video.video_settings.maxrate = int(self.maxrate_widget.text()) self.app.fastflix.current_video.video_settings.bufsize = int(self.bufsize_widget.text()) else: @@ -438,6 +414,48 @@ def update_settings(self): self.updating = False + def get_settings(self): + denoise = None + if self.denoise_type_widget.currentIndex() != 0: + denoise = denoise_presets[self.denoise_type_widget.currentText()][ + self.denoise_strength_widget.currentText() + ] + + maxrate = None + bufsize = None + if self.maxrate_widget.text() and self.bufsize_widget.text(): + maxrate = int(self.maxrate_widget.text()) + bufsize = int(self.bufsize_widget.text()) + + return AdvancedOptions( + video_speed=video_speeds[self.video_speed_widget.currentText()], + deblock=non(self.deblock_widget.currentText()), + deblock_size=int(self.deblock_size_widget.currentText()), + tone_map=self.tone_map_widget.currentText(), + vsync=non(self.vsync_widget.currentText()), + brightness=(self.brightness_widget.text() or None), + saturation=(self.saturation_widget.text() or None), + contrast=(self.contrast_widget.text() or None), + maxrate=maxrate, + bufsize=bufsize, + source_fps=(None if self.incoming_same_as_source.isChecked() else self.incoming_fps_widget.text()), + output_fps=(None if self.outgoing_same_as_source.isChecked() else self.outgoing_fps_widget.text()), + color_space=( + None if self.color_space_widget.currentIndex() == 0 else self.color_space_widget.currentText() + ), + color_transfer=( + None if self.color_transfer_widget.currentIndex() == 0 else self.color_transfer_widget.currentText() + ), + color_primaries=( + None if self.color_primaries_widget.currentIndex() == 0 else self.color_primaries_widget.currentText() + ), + denoise=denoise, + denoise_type_index=self.denoise_type_widget.currentIndex(), + denoise_strength_index=self.denoise_strength_widget.currentIndex(), + # first_pass_filters=self.first_filters.text() or None, + # second_pass_filters=self.second_filters.text() or None, + ) + def hdr_settings(self): if self.main.remove_hdr: self.color_primaries_widget.setCurrentText("bt709") @@ -448,24 +466,39 @@ def hdr_settings(self): self.color_space_widget.setCurrentIndex(0) else: if self.app.fastflix.current_video: - if self.app.fastflix.current_video.color_space: + if color_space := self.app.fastflix.config.advanced_opt("color_space"): + self.color_space_widget.setCurrentText(color_space) + elif self.app.fastflix.current_video.color_space: self.color_space_widget.setCurrentText(self.app.fastflix.current_video.color_space) else: self.color_space_widget.setCurrentIndex(0) - if self.app.fastflix.current_video.color_transfer: + if color_transfer := self.app.fastflix.config.advanced_opt("color_transfer"): + self.color_transfer_widget.setCurrentText(color_transfer) + elif self.app.fastflix.current_video.color_transfer: self.color_transfer_widget.setCurrentText(self.app.fastflix.current_video.color_transfer) else: self.color_transfer_widget.setCurrentIndex(0) - if self.app.fastflix.current_video.color_primaries: + if color_primaries := self.app.fastflix.config.advanced_opt("color_primaries"): + self.color_primaries_widget.setCurrentText(color_primaries) + elif self.app.fastflix.current_video.color_primaries: self.color_primaries_widget.setCurrentText(self.app.fastflix.current_video.color_primaries) else: self.color_primaries_widget.setCurrentIndex(0) else: - self.color_space_widget.setCurrentIndex(0) - self.color_transfer_widget.setCurrentIndex(0) - self.color_primaries_widget.setCurrentIndex(0) + if color_space := self.app.fastflix.config.advanced_opt("color_space"): + self.color_space_widget.setCurrentText(color_space) + else: + self.color_space_widget.setCurrentIndex(0) + if color_transfer := self.app.fastflix.config.advanced_opt("color_transfer"): + self.color_transfer_widget.setCurrentText(color_transfer) + else: + self.color_transfer_widget.setCurrentIndex(0) + if color_primaries := self.app.fastflix.config.advanced_opt("color_primaries"): + self.color_primaries_widget.setCurrentText(color_primaries) + else: + self.color_primaries_widget.setCurrentIndex(0) def page_update(self, build_thumbnail=False): self.main.page_update(build_thumbnail=build_thumbnail) @@ -508,42 +541,73 @@ def reset(self, settings: VideoSettings = None): self.vsync_widget.setCurrentIndex(0) if settings.maxrate: - self.vbv_checkbox.setChecked(True) self.maxrate_widget.setText(str(settings.maxrate)) self.bufsize_widget.setText(str(settings.bufsize)) - self.maxrate_widget.setEnabled(True) - self.bufsize_widget.setEnabled(True) else: - self.vbv_checkbox.setChecked(False) self.maxrate_widget.setText("") self.bufsize_widget.setText("") - self.maxrate_widget.setDisabled(True) - self.bufsize_widget.setDisabled(True) + + if settings.color_space: + self.color_space_widget.setCurrentText(settings.color_space) + else: + self.color_space_widget.setCurrentIndex(0) + + if settings.color_transfer: + self.color_transfer_widget.setCurrentText(settings.color_transfer) + else: + self.color_transfer_widget.setCurrentIndex(0) + + if settings.color_primaries: + self.color_primaries_widget.setCurrentText(settings.color_primaries) + else: + self.color_primaries_widget.setCurrentIndex(0) else: - self.video_speed_widget.setCurrentIndex(0) - self.deblock_widget.setCurrentIndex(0) - self.deblock_size_widget.setCurrentIndex(0) - self.tone_map_widget.setCurrentIndex(5) - self.incoming_same_as_source.setChecked(True) - self.outgoing_same_as_source.setChecked(True) - self.incoming_fps_widget.setDisabled(True) - self.outgoing_fps_widget.setDisabled(True) - self.incoming_fps_widget.setText("") - self.outgoing_fps_widget.setText("") - self.denoise_type_widget.setCurrentIndex(0) - self.denoise_strength_widget.setCurrentIndex(0) - self.vsync_widget.setCurrentIndex(0) - self.vbv_checkbox.setChecked(False) - self.maxrate_widget.setText("") - self.bufsize_widget.setText("") - self.maxrate_widget.setDisabled(True) - self.bufsize_widget.setDisabled(True) - self.brightness_widget.setText("") - self.saturation_widget.setText("") - self.contrast_widget.setText("") - - self.hdr_settings() + self.video_speed_widget.setCurrentIndex( + list(video_speeds.values()).index(self.app.fastflix.config.advanced_opt("video_speed")) + ) + + deblock = self.app.fastflix.config.advanced_opt("deblock") + if not deblock: + self.deblock_widget.setCurrentIndex(0) + else: + self.deblock_widget.setCurrentText(deblock) + self.deblock_size_widget.setCurrentText(str(self.app.fastflix.config.advanced_opt("deblock_size"))) + tone_map_select = self.app.fastflix.config.advanced_opt("tone_map") + + self.tone_map_widget.setCurrentIndex(tone_map_items.index(tone_map_select) if tone_map_select else 0) + + # FPS + source_fps = self.app.fastflix.config.advanced_opt("source_fps") + output_fps = self.app.fastflix.config.advanced_opt("output_fps") + self.incoming_same_as_source.setChecked(True if not source_fps else False) + self.outgoing_same_as_source.setChecked(True if not output_fps else False) + self.incoming_fps_widget.setDisabled(True if not source_fps else False) + self.outgoing_fps_widget.setDisabled(True if not output_fps else False) + self.incoming_fps_widget.setText("" if not source_fps else source_fps) + self.outgoing_fps_widget.setText("" if not output_fps else output_fps) + + self.denoise_type_widget.setCurrentIndex(self.app.fastflix.config.advanced_opt("denoise_type_index")) + self.denoise_strength_widget.setCurrentIndex( + self.app.fastflix.config.advanced_opt("denoise_strength_index") + ) + + vsync_value = self.app.fastflix.config.advanced_opt("vsync") + self.vsync_widget.setCurrentIndex(0 if not vsync_value else (vsync.index(vsync_value) + 1)) + + # VBV + maxrate = self.app.fastflix.config.advanced_opt("maxrate") + bufsize = self.app.fastflix.config.advanced_opt("bufsize") + vbv = bool(maxrate and bufsize) + self.maxrate_widget.setText(str(maxrate) if maxrate and vbv else "") + self.bufsize_widget.setText(str(bufsize) if maxrate and vbv else "") + + # Equalizer + self.brightness_widget.setText(self.app.fastflix.config.advanced_opt("brightness") or "") + self.saturation_widget.setText(self.app.fastflix.config.advanced_opt("saturation") or "") + self.contrast_widget.setText(self.app.fastflix.config.advanced_opt("contrast") or "") + + self.hdr_settings() # Set the frame rate if self.app.fastflix.current_video: diff --git a/fastflix/widgets/panels/audio_panel.py b/fastflix/widgets/panels/audio_panel.py index 8c4a9e12..4ab9cad9 100644 --- a/fastflix/widgets/panels/audio_panel.py +++ b/fastflix/widgets/panels/audio_panel.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from typing import List, Optional +import logging from box import Box from iso639 import Lang @@ -10,12 +11,14 @@ from fastflix.encoders.common.audio import lossless, channel_list from fastflix.language import t from fastflix.models.encode import AudioTrack +from fastflix.models.profiles import Profile, MatchType, MatchItem from fastflix.models.fastflix_app import FastFlixApp from fastflix.resources import get_icon from fastflix.shared import no_border, error_message from fastflix.widgets.panels.abstract_list import FlixList language_list = sorted((k for k, v in Lang._data["name"].items() if v["pt2B"] and v["pt1"]), key=lambda x: x.lower()) +logger = logging.getLogger("fastflix") class Audio(QtWidgets.QTabWidget): @@ -41,6 +44,7 @@ def __init__( ): self.loading = True super(Audio, self).__init__(parent) + self.setObjectName("Audio") self.parent = parent self.audio = audio self.setFixedHeight(60) @@ -89,7 +93,7 @@ def __init__( self.widgets.audio_info.setToolTip(all_info.to_yaml()) self.widgets.language.addItems(["No Language Set"] + language_list) - self.widgets.language.setMaximumWidth(110) + self.widgets.language.setMaximumWidth(150) if language: try: lang = Lang(language).name @@ -102,12 +106,13 @@ def __init__( self.widgets.language.currentIndexChanged.connect(self.page_update) self.widgets.title.setFixedWidth(150) self.widgets.title.textChanged.connect(self.page_update) - self.widgets.audio_info.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - + # self.widgets.audio_info.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.widgets.audio_info.setFixedWidth(350) self.widgets.downmix.addItems([t("No Downmix")] + [k for k, v in channel_list.items() if v <= channels]) self.widgets.downmix.currentIndexChanged.connect(self.update_downmix) self.widgets.downmix.setCurrentIndex(0) self.widgets.downmix.setDisabled(True) + self.widgets.downmix.hide() self.widgets.enable_check.setChecked(enabled) self.widgets.enable_check.toggled.connect(self.update_enable) @@ -126,8 +131,10 @@ def __init__( label = QtWidgets.QLabel(f"{t('Title')}: ") self.widgets.title.setFixedWidth(150) title_layout = QtWidgets.QHBoxLayout() - title_layout.addWidget(label) - title_layout.addWidget(self.widgets.title) + title_layout.addStretch(False) + title_layout.addWidget(label, stretch=False) + title_layout.addWidget(self.widgets.title, stretch=False) + title_layout.addStretch(True) grid = QtWidgets.QGridLayout() grid.addLayout(self.init_move_buttons(), 0, 0) @@ -179,13 +186,16 @@ def init_conversion(self): self.widgets.convert_bitrate.addItems(self.get_conversion_bitrates()) self.widgets.convert_bitrate.setCurrentIndex(3) self.widgets.convert_bitrate.setDisabled(True) + self.widgets.bitrate_label = QtWidgets.QLabel(f"{t('Bitrate')}: ") + self.widgets.convert_bitrate.hide() + self.widgets.bitrate_label.hide() self.widgets.convert_bitrate.currentIndexChanged.connect(lambda: self.page_update()) self.widgets.convert_to.currentIndexChanged.connect(self.update_conversion) layout.addWidget(QtWidgets.QLabel(f"{t('Conversion')}: ")) layout.addWidget(self.widgets.convert_to) - layout.addWidget(QtWidgets.QLabel(f"{t('Bitrate')}: ")) + layout.addWidget(self.widgets.bitrate_label) layout.addWidget(self.widgets.convert_bitrate) return layout @@ -218,8 +228,14 @@ def update_conversion(self): if self.widgets.convert_to.currentIndex() == 0: self.widgets.downmix.setDisabled(True) self.widgets.convert_bitrate.setDisabled(True) + self.widgets.convert_bitrate.hide() + self.widgets.bitrate_label.hide() + self.widgets.downmix.hide() else: self.widgets.downmix.setDisabled(False) + self.widgets.convert_bitrate.show() + self.widgets.bitrate_label.show() + self.widgets.downmix.show() if self.widgets.convert_to.currentText() in lossless: self.widgets.convert_bitrate.setDisabled(True) else: @@ -317,42 +333,33 @@ def __init__(self, parent, app: FastFlixApp): self.app = app self._first_selected = False - def lang_match(self, track): - if not self.app.fastflix.config.opt("audio_select"): - return False - if not self.app.fastflix.config.opt("audio_select_preferred_language"): - if self.app.fastflix.config.opt("audio_select_first_matching") and self._first_selected: - return False - self._first_selected = True - return True - try: - track_lang = Lang(track.get("tags", {}).get("language", "")) - except InvalidLanguageValue: - return True - else: - if Lang(self.app.fastflix.config.opt("audio_language")) == track_lang: - if self.app.fastflix.config.opt("audio_select_first_matching") and self._first_selected: - return False - self._first_selected = True - return True - return False + def _get_track_info(self, track): + track_info = "" + tags = track.get("tags", {}) + if tags: + track_info += tags.get("title", "") + # if "language" in tags: + # track_info += f" {tags.language}" + track_info += f" - {track.codec_name}" + if "profile" in track: + track_info += f" ({track.profile})" + track_info += f" - {track.channels} {t('channels')}" + return track_info, tags + + def enable_all(self): + for track in self.tracks: + track.widgets.enable_check.setChecked(True) + + def disable_all(self): + for track in self.tracks: + track.widgets.enable_check.setChecked(False) def new_source(self, codecs): self.tracks: List[Audio] = [] self._first_selected = False disable_dup = "nvencc" in self.main.convert_to.lower() for i, x in enumerate(self.app.fastflix.current_video.streams.audio, start=1): - track_info = "" - tags = x.get("tags", {}) - if tags: - track_info += tags.get("title", "") - # if "language" in tags: - # track_info += f" {tags.language}" - track_info += f" - {x.codec_name}" - if "profile" in x: - track_info += f" ({x.profile})" - track_info += f" - {x.channels} {t('channels')}" - + track_info, tags = self._get_track_info(x) new_item = Audio( self, track_info, @@ -367,7 +374,7 @@ def new_source(self, codecs): codecs=codecs, channels=x.channels, available_audio_encoders=self.available_audio_encoders, - enabled=self.lang_match(x), + enabled=True, all_info=x, disable_dup=disable_dup, ) @@ -376,7 +383,6 @@ def new_source(self, codecs): if self.tracks: self.tracks[0].set_first() self.tracks[-1].set_last() - super()._new_source(self.tracks) self.update_audio_settings() @@ -400,6 +406,122 @@ def allowed_formats(self, allowed_formats=None): for track in self.tracks: track.update_codecs(allowed_formats or set()) + def apply_profile_settings(self, profile: Profile, original_tracks: List[Box], audio_formats): + if profile.audio_filters: + self.disable_all() + else: + self.enable_all() + return + + disable_dups = "nvencc" in self.main.convert_to.lower() or "vcenc" in self.main.convert_to.lower() + self.tracks = [] + + def gen_track( + parent, audio_track, outdex, og=False, enabled=True, downmix=None, conversion=None, bitrate=None + ) -> Audio: + track_info, tags = self._get_track_info(audio_track) + new_track = Audio( + parent, + track_info, + title=tags.get("title"), + language=tags.get("language"), + profile=audio_track.get("profile"), + original=og, + index=audio_track.index, + outdex=outdex, + codec=audio_track.codec_name, + codecs=audio_formats, + channels=audio_track.channels, + available_audio_encoders=self.available_audio_encoders, + enabled=enabled, + all_info=audio_track, + disable_dup=disable_dups, + ) + if conversion: + new_track.widgets.convert_to.setCurrentText(conversion) + new_track.widgets.convert_bitrate.setCurrentText(bitrate) + if downmix and downmix < audio_track.channels: + new_track.widgets.downmix.setCurrentIndex(downmix) + return new_track + + # First populate all original tracks and disable them + for i, track in enumerate(original_tracks, start=1): + self.tracks.append(gen_track(self, track, outdex=i, og=True, enabled=False)) + + tracks = [] + for audio_match in profile.audio_filters: + if audio_match.match_item == MatchItem.ALL: + track_select = original_tracks.copy() + if track_select: + if audio_match.match_type == MatchType.FIRST: + track_select = [track_select[0]] + elif audio_match.match_type == MatchType.LAST: + track_select = [track_select[-1]] + for track in track_select: + tracks.append((track, audio_match)) + + elif audio_match.match_item == MatchItem.TITLE: + subset_tracks = [] + for track in original_tracks: + if audio_match.match_input.lower() in track.tags.get("title", "").casefold(): + subset_tracks.append((track, audio_match)) + if subset_tracks: + if audio_match.match_type == MatchType.FIRST: + tracks.append(subset_tracks[0]) + elif audio_match.match_type == MatchType.LAST: + tracks.append(subset_tracks[-1]) + else: + tracks.extend(subset_tracks) + + elif audio_match.match_item == MatchItem.TRACK: + for track in original_tracks: + if track.index == int(audio_match.match_input): + tracks.append((track, audio_match)) + + elif audio_match.match_item == MatchItem.LANGUAGE: + subset_tracks = [] + for track in original_tracks: + try: + if Lang(audio_match.match_input) == Lang(track.tags["language"]): + subset_tracks.append((track, audio_match)) + except (InvalidLanguageValue, KeyError): + pass + if subset_tracks: + if audio_match.match_type == MatchType.FIRST: + tracks.append(subset_tracks[0]) + elif audio_match.match_type == MatchType.LAST: + tracks.append(subset_tracks[-1]) + else: + tracks.extend(subset_tracks) + + elif audio_match.match_item == MatchItem.CHANNELS: + subset_tracks = [] + for track in original_tracks: + if int(audio_match.match_input) == track.channels: + subset_tracks.append((track, audio_match)) + if subset_tracks: + if audio_match.match_type == MatchType.FIRST: + tracks.append(subset_tracks[0]) + elif audio_match.match_type == MatchType.LAST: + tracks.append(subset_tracks[-1]) + else: + tracks.extend(subset_tracks) + + self.tracks.extend( + gen_track( + self, + track[0], + i, + enabled=True, + og=False, + conversion=track[1].conversion, + bitrate=track[1].bitrate, + downmix=track[1].downmix, + ) + for i, track in enumerate(tracks, start=len(self.tracks) + 1) + ) + super()._new_source(self.tracks) + def update_audio_settings(self): tracks = [] for track in self.tracks: @@ -464,17 +586,7 @@ def reload(self, original_tracks: List[AudioTrack], audio_formats): for i, x in enumerate(self.app.fastflix.current_video.streams.audio, start=1): if x.index in repopulated_tracks: continue - track_info = "" - tags = x.get("tags", {}) - if tags: - track_info += tags.get("title", "") - # if "language" in tags: - # track_info += f" {tags.language}" - track_info += f" - {x.codec_name}" - if "profile" in x: - track_info += f" ({x.profile})" - track_info += f" - {x.channels} {t('channels')}" - + track_info, tags = self._get_track_info(x) new_item = Audio( self, track_info, diff --git a/fastflix/widgets/profile_window.py b/fastflix/widgets/profile_window.py deleted file mode 100644 index d164cdff..00000000 --- a/fastflix/widgets/profile_window.py +++ /dev/null @@ -1,211 +0,0 @@ -# -*- coding: utf-8 -*- - -import shutil -from pathlib import Path -import logging - -from box import Box -from iso639 import Lang -from PySide6 import QtCore, QtGui, QtWidgets - -from fastflix.exceptions import FastFlixInternalException -from fastflix.language import t -from fastflix.models.config import Profile, get_preset_defaults -from fastflix.models.fastflix_app import FastFlixApp -from fastflix.models.video import ( - AOMAV1Settings, - CopySettings, - GIFSettings, - SVTAV1Settings, - VP9Settings, - WebPSettings, - rav1eSettings, - x264Settings, - x265Settings, - NVEncCSettings, - NVEncCAVCSettings, - FFmpegNVENCSettings, - VCEEncCAVCSettings, - VCEEncCSettings, -) -from fastflix.shared import error_message - -language_list = sorted((k for k, v in Lang._data["name"].items() if v["pt2B"] and v["pt1"]), key=lambda x: x.lower()) - -logger = logging.getLogger("fastflix") - - -class ProfileWindow(QtWidgets.QWidget): - def __init__(self, app: FastFlixApp, main, *args, **kwargs): - super().__init__(None, *args, **kwargs) - self.app = app - self.main = main - self.config_file = self.app.fastflix.config.config_path - self.setWindowTitle(t("New Profile")) - self.setMinimumSize(500, 150) - layout = QtWidgets.QGridLayout() - - profile_name_label = QtWidgets.QLabel(t("Profile Name")) - self.profile_name = QtWidgets.QLineEdit() - - self.auto_crop = QtWidgets.QCheckBox(t("Auto Crop")) - - audio_language_label = QtWidgets.QLabel(t("Audio select language")) - self.audio_language = QtWidgets.QComboBox() - self.audio_language.addItems([t("All"), t("None")] + language_list) - self.audio_language.insertSeparator(1) - self.audio_language.insertSeparator(3) - self.audio_first_only = QtWidgets.QCheckBox(t("Only select first matching Audio Track")) - - sub_language_label = QtWidgets.QLabel(t("Subtitle select language")) - self.sub_language = QtWidgets.QComboBox() - self.sub_language.addItems([t("All"), t("None")] + language_list) - self.sub_language.insertSeparator(1) - self.sub_language.insertSeparator(3) - self.sub_first_only = QtWidgets.QCheckBox(t("Only select first matching Subtitle Track")) - - self.sub_burn_in = QtWidgets.QCheckBox(t("Auto Burn-in first forced or default subtitle track")) - - self.encoder = x265Settings(crf=18) - self.encoder_settings = QtWidgets.QLabel() - self.encoder_settings.setStyleSheet("font-family: monospace;") - self.encoder_label = QtWidgets.QLabel(f"{t('Encoder')}: {self.encoder.name}") - - save_button = QtWidgets.QPushButton(t("Create Profile")) - save_button.clicked.connect(self.save) - save_button.setMaximumWidth(150) - - layout.addWidget(profile_name_label, 0, 0) - layout.addWidget(self.profile_name, 0, 1) - layout.addWidget(self.auto_crop, 1, 0) - layout.addWidget(audio_language_label, 2, 0) - layout.addWidget(self.audio_language, 2, 1) - layout.addWidget(self.audio_first_only, 3, 1) - layout.addWidget(sub_language_label, 4, 0) - layout.addWidget(self.sub_language, 4, 1) - layout.addWidget(self.sub_first_only, 5, 1) - layout.addWidget(self.sub_burn_in, 6, 0, 1, 2) - layout.addWidget(self.encoder_label, 7, 0, 1, 2) - layout.addWidget(self.encoder_settings, 8, 0, 10, 2) - layout.addWidget(save_button, 20, 1, alignment=QtCore.Qt.AlignRight) - - self.update_settings() - - self.setLayout(layout) - - def update_settings(self): - try: - encoder = self.app.fastflix.current_video.video_settings.video_encoder_settings - except AttributeError: - pass - else: - if encoder: - self.encoder = encoder - settings = "\n".join(f"{k:<30} {v}" for k, v in self.encoder.dict().items()) - self.encoder_label.setText(f"{t('Encoder')}: {self.encoder.name}") - self.encoder_settings.setText(f"
{settings}
") - - def save(self): - profile_name = self.profile_name.text().strip() - if not profile_name: - return error_message(t("Please provide a profile name")) - if profile_name in self.app.fastflix.config.profiles: - return error_message(f'{t("Profile")} {self.profile_name.text().strip()} {t("already exists")}') - - audio_lang = "en" - audio_select = True - audio_select_preferred_language = False - if self.audio_language.currentIndex() == 2: # None - audio_select_preferred_language = False - audio_select = False - elif self.audio_language.currentIndex() != 0: - audio_select_preferred_language = True - audio_lang = Lang(self.audio_language.currentText()).pt2b - - sub_lang = "en" - subtitle_select = True - subtitle_select_preferred_language = False - if self.sub_language.currentIndex() == 2: # None - subtitle_select_preferred_language = False - subtitle_select = False - elif self.sub_language.currentIndex() != 0: - subtitle_select_preferred_language = True - sub_lang = Lang(self.sub_language.currentText()).pt2b - - v_flip, h_flip = self.main.get_flips() - - new_profile = Profile( - auto_crop=self.auto_crop.isChecked(), - keep_aspect_ratio=self.main.widgets.scale.keep_aspect.isChecked(), - fast_seek=self.main.fast_time, - rotate=self.main.widgets.rotate.currentIndex(), - vertical_flip=v_flip, - horizontal_flip=h_flip, - copy_chapters=self.main.copy_chapters, - remove_metadata=self.main.remove_metadata, - remove_hdr=self.main.remove_hdr, - audio_language=audio_lang, - audio_select=audio_select, - audio_select_preferred_language=audio_select_preferred_language, - audio_select_first_matching=self.audio_first_only.isChecked(), - subtitle_language=sub_lang, - subtitle_select=subtitle_select, - subtitle_automatic_burn_in=self.sub_burn_in.isChecked(), - subtitle_select_preferred_language=subtitle_select_preferred_language, - subtitle_select_first_matching=self.sub_first_only.isChecked(), - encoder=self.encoder.name, - ) - - if isinstance(self.encoder, x265Settings): - new_profile.x265 = self.encoder - elif isinstance(self.encoder, x264Settings): - new_profile.x264 = self.encoder - elif isinstance(self.encoder, rav1eSettings): - new_profile.rav1e = self.encoder - elif isinstance(self.encoder, SVTAV1Settings): - new_profile.svt_av1 = self.encoder - elif isinstance(self.encoder, VP9Settings): - new_profile.vp9 = self.encoder - elif isinstance(self.encoder, AOMAV1Settings): - new_profile.aom_av1 = self.encoder - elif isinstance(self.encoder, GIFSettings): - new_profile.gif = self.encoder - elif isinstance(self.encoder, WebPSettings): - new_profile.webp = self.encoder - elif isinstance(self.encoder, CopySettings): - new_profile.copy_settings = self.encoder - elif isinstance(self.encoder, NVEncCSettings): - new_profile.nvencc_hevc = self.encoder - elif isinstance(self.encoder, NVEncCAVCSettings): - new_profile.nvencc_avc = self.encoder - elif isinstance(self.encoder, FFmpegNVENCSettings): - new_profile.ffmpeg_hevc_nvenc = self.encoder - elif isinstance(self.encoder, VCEEncCSettings): - new_profile.vceencc_hevc = self.encoder - elif isinstance(self.encoder, VCEEncCAVCSettings): - new_profile.vceencc_avc = self.encoder - else: - logger.error("Profile cannot be saved! Unknown encoder type.") - return - - self.app.fastflix.config.profiles[profile_name] = new_profile - self.app.fastflix.config.selected_profile = profile_name - self.app.fastflix.config.save() - self.main.widgets.profile_box.addItem(profile_name) - self.main.widgets.profile_box.setCurrentText(profile_name) - self.hide() - - def delete_current_profile(self): - if self.app.fastflix.config.selected_profile in get_preset_defaults(): - return error_message( - f"{self.app.fastflix.config.selected_profile} " f"{t('is a default profile and will not be removed')}" - ) - self.main.loading_video = True - del self.app.fastflix.config.profiles[self.app.fastflix.config.selected_profile] - self.app.fastflix.config.selected_profile = "Standard Profile" - self.app.fastflix.config.save() - self.main.widgets.profile_box.clear() - self.main.widgets.profile_box.addItems(self.app.fastflix.config.profiles.keys()) - self.main.loading_video = False - self.main.widgets.profile_box.setCurrentText("Standard Profile") - self.main.widgets.convert_to.setCurrentIndex(0) diff --git a/fastflix/widgets/progress_bar.py b/fastflix/widgets/progress_bar.py index ee823d20..2c462e4c 100644 --- a/fastflix/widgets/progress_bar.py +++ b/fastflix/widgets/progress_bar.py @@ -35,6 +35,7 @@ def __init__( self.tasks = tasks self.signal_task = signal_task + self.cancelled = False self.setObjectName("ProgressBar") self.setStyleSheet("#ProgressBar{border: 1px solid #aaa}") @@ -60,7 +61,10 @@ def __init__( self.run() def cancel(self): - self.stop_signal.emit() + if self.signal_task: + self.stop_signal.emit() + else: + self.cancelled = True self.close() @reusables.log_exception("fastflix") @@ -68,7 +72,7 @@ def run(self): if not self.tasks: logger.error("Progress bar RUN called without any tasks") return - ratio = 100 // len(self.tasks) + ratio = 100 / len(self.tasks) self.progress_bar.setValue(0) if self.signal_task: @@ -88,6 +92,8 @@ def run(self): logger.exception(f"Could not run task {task.name} with config {self.app.fastflix.config}") raise self.progress_bar.setValue(int(i * ratio)) + if self.cancelled: + return def update_progress(self, value): self.progress_bar.setValue(value) diff --git a/fastflix/widgets/video_options.py b/fastflix/widgets/video_options.py index 00342930..17286095 100644 --- a/fastflix/widgets/video_options.py +++ b/fastflix/widgets/video_options.py @@ -159,8 +159,11 @@ def get_settings(self): def new_source(self): if not self.app.fastflix.current_video: return + profile = self.app.fastflix.config.profiles[self.app.fastflix.config.selected_profile] if getattr(self.main.current_encoder, "enable_audio", False): self.audio.new_source(self.audio_formats) + streams = copy.deepcopy(self.app.fastflix.current_video.streams) + self.audio.apply_profile_settings(profile, streams.audio, self.audio_formats) if getattr(self.main.current_encoder, "enable_subtitles", False): self.subtitles.new_source() if getattr(self.main.current_encoder, "enable_attachments", False): @@ -183,7 +186,14 @@ def refresh(self): def update_profile(self): self.current_settings.update_profile() if self.app.fastflix.current_video: + streams = copy.deepcopy(self.app.fastflix.current_video.streams) + # settings = copy.deepcopy(self.app.fastflix.current_video.video_settings) + # audio_tracks = settings.audio_tracks + # subtitle_tracks = settings.subtitle_tracks + profile = self.app.fastflix.config.profile + if getattr(self.main.current_encoder, "enable_audio", False): + self.audio.apply_profile_settings(profile, streams.audio, self.audio_formats) self.audio.update_audio_settings() if getattr(self.main.current_encoder, "enable_subtitles", False): self.subtitles.get_settings() diff --git a/fastflix/widgets/windows/__init__.py b/fastflix/widgets/windows/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastflix/widgets/windows/audio_select.py b/fastflix/widgets/windows/audio_select.py new file mode 100644 index 00000000..22310c54 --- /dev/null +++ b/fastflix/widgets/windows/audio_select.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +import logging +from pathlib import Path +from subprocess import run, PIPE +from typing import Optional +import secrets + +from PySide6 import QtWidgets, QtCore, QtGui + +from fastflix.flix import ( + generate_thumbnail_command, +) +from fastflix.encoders.common import helpers +from fastflix.resources import get_icon +from fastflix.language import t + +__all__ = ["AudioSelect"] + +logger = logging.getLogger("fastflix") + + +class AudioSelect(QtWidgets.QWidget): + def __init__(self, parent): + super().__init__() + self.main = parent diff --git a/fastflix/widgets/concat.py b/fastflix/widgets/windows/concat.py similarity index 82% rename from fastflix/widgets/concat.py rename to fastflix/widgets/windows/concat.py index afdf306c..761d5a51 100644 --- a/fastflix/widgets/concat.py +++ b/fastflix/widgets/windows/concat.py @@ -8,6 +8,7 @@ from fastflix.language import t from fastflix.flix import probe from fastflix.shared import yes_no_message, error_message +from fastflix.widgets.progress_bar import ProgressBar, Task logger = logging.getLogger("fastflix") @@ -155,12 +156,6 @@ def __init__(self, app, main, items=None): def set_folder_name(self, name): self.base_folder_label.setText(f'{t("Base Folder")}: {name}') - def get_video_details(self, file): - details = probe(self.app, file) - for stream in details.streams: - if stream.codec_type == "video": - return file.name, f"{stream.width}x{stream.height}", stream.codec_name - def select_folder(self): if self.concat_area.table.model.rowCount() > 0: if not yes_no_message( @@ -175,19 +170,37 @@ def select_folder(self): return self.folder_name = folder_name self.set_folder_name(folder_name) + + def check_to_add(file, list_of_items, bad_items, **_): + try: + data = None + details = probe(self.app, file) + for stream in details.streams: + if stream.codec_type == "video": + data = (file.name, f"{stream.width}x{stream.height}", stream.codec_name) + if not data: + raise Exception() + except Exception: + logger.warning(f"Skipping {file.name} as it is not a video/image file") + bad_items.append(file.name) + else: + list_of_items.append(data) + items = [] skipped = [] + tasks = [] for file in Path(folder_name).glob("*"): if file.is_file(): - try: - details = self.get_video_details(file) - if not details: - raise Exception() - except Exception: - logger.warning(f"Skipping {file.name} as it is not a video/image file") - skipped.append(file.name) - else: - items.append(details) + tasks.append( + Task( + f"Evaluating {file.name}", + command=check_to_add, + kwargs={"file": file, "list_of_items": items, "bad_items": skipped}, + ) + ) + + ProgressBar(self.app, tasks, can_cancel=True, auto_run=True) + self.concat_area.table.update_items(items) if skipped: error_message( @@ -211,5 +224,14 @@ def save(self): self.main.update_video_info() self.concat_area.table.model.clear() self.concat_area.table.buttons = [] + self.main.widgets.end_time.setText("0") + self.main.widgets.start_time.setText("0") + self.main.app.fastflix.current_video.interlaced = False + self.main.widgets.deinterlace.setChecked(False) self.hide() self.main.page_update(build_thumbnail=True) + error_message( + "Make sure to manually supply the frame rate in the Advanced tab " + "(usually want to set both input and output to the same thing.)", + title="Set FPS in Advanced Tab", + ) diff --git a/fastflix/widgets/large_preview.py b/fastflix/widgets/windows/large_preview.py similarity index 96% rename from fastflix/widgets/large_preview.py rename to fastflix/widgets/windows/large_preview.py index 61e139e0..b30f1949 100644 --- a/fastflix/widgets/large_preview.py +++ b/fastflix/widgets/windows/large_preview.py @@ -63,6 +63,7 @@ def generate_image(self): settings["remove_hdr"] = True filters = helpers.generate_filters( + enable_opencl=self.main.app.fastflix.opencl_support, start_filters="select=eq(pict_type\\,I)" if self.main.widgets.thumb_key.isChecked() else None, **settings, ) @@ -75,6 +76,7 @@ def generate_image(self): output=output, filters=filters, start_time=self.main.preview_place, + enable_opencl=self.main.app.fastflix.opencl_support, input_track=self.main.app.fastflix.current_video.video_settings.selected_track, ) if thumb_command == self.last_command: diff --git a/fastflix/widgets/windows/profile_window.py b/fastflix/widgets/windows/profile_window.py new file mode 100644 index 00000000..d9e77807 --- /dev/null +++ b/fastflix/widgets/windows/profile_window.py @@ -0,0 +1,544 @@ +# -*- coding: utf-8 -*- + +import logging + +from box import Box +from iso639 import Lang +from PySide6 import QtCore, QtGui, QtWidgets + +from fastflix.flix import ffmpeg_valid_color_primaries, ffmpeg_valid_color_transfers, ffmpeg_valid_color_space +from fastflix.language import t +from fastflix.widgets.panels.abstract_list import FlixList +from fastflix.models.config import get_preset_defaults +from fastflix.models.fastflix_app import FastFlixApp +from fastflix.models.encode import ( + AOMAV1Settings, + CopySettings, + GIFSettings, + SVTAV1Settings, + VP9Settings, + WebPSettings, + rav1eSettings, + x264Settings, + x265Settings, + NVEncCSettings, + NVEncCAVCSettings, + FFmpegNVENCSettings, + VCEEncCAVCSettings, + VCEEncCSettings, +) +from fastflix.models.profiles import AudioMatch, Profile, MatchItem, MatchType, AdvancedOptions +from fastflix.shared import error_message + +language_list = sorted((k for k, v in Lang._data["name"].items() if v["pt2B"] and v["pt1"]), key=lambda x: x.lower()) + +logger = logging.getLogger("fastflix") + +match_type_eng = [MatchType.ALL, MatchType.FIRST, MatchType.LAST] +match_type_locale = [t("All"), t("First"), t("Last")] + +match_item_enums = [MatchItem.ALL, MatchItem.TITLE, MatchItem.TRACK, MatchItem.LANGUAGE, MatchItem.CHANNELS] +match_item_locale = [t("All"), t("Title"), t("Track Number"), t("Language"), t("Channels")] + +sub_match_item_enums = [MatchItem.ALL, MatchItem.TRACK, MatchItem.LANGUAGE] +sub_match_item_locale = [t("All"), t("Track Number"), t("Language")] + + +class AudioProfile(QtWidgets.QTabWidget): + def __init__(self, parent_list, app, main, parent, index): + super(AudioProfile, self).__init__(parent) + self.enabled = True + self.index = index + self.parent = parent + self.parent_list = parent_list + self.match_type = QtWidgets.QComboBox() + self.match_type.addItems(match_type_locale) + self.match_type.currentIndexChanged.connect(self.update_combos) + self.match_type.view().setFixedWidth(self.match_type.minimumSizeHint().width() + 50) + self.setFixedHeight(120) + + self.match_item = QtWidgets.QComboBox() + self.match_item.addItems(match_item_locale) + self.match_item.currentIndexChanged.connect(self.update_combos) + self.match_item.view().setFixedWidth(self.match_item.minimumSizeHint().width() + 50) + + self.match_input_boxes = [ + QtWidgets.QLineEdit("*"), + QtWidgets.QLineEdit(""), + QtWidgets.QComboBox(), + QtWidgets.QComboBox(), + QtWidgets.QComboBox(), + ] + self.match_input = self.match_input_boxes[0] + self.match_input_boxes[0].setDisabled(True) + self.match_input_boxes[1].setPlaceholderText(t("contains")) + self.match_input_boxes[2].addItems([str(x) for x in range(1, 24)]) + self.match_input_boxes[3].addItems(language_list) + self.match_input_boxes[3].setCurrentText("English") + self.match_input_boxes[4].addItems( + ["none | unknown", "mono", "stereo", "3 | 2.1", "4", "5", "6 | 5.1", "7", "8 | 7.1", "9", "10"] + ) + + self.match_input_boxes[2].view().setFixedWidth(self.match_input_boxes[2].minimumSizeHint().width() + 50) + self.match_input_boxes[3].view().setFixedWidth(self.match_input_boxes[3].minimumSizeHint().width() + 50) + self.match_input_boxes[4].view().setFixedWidth(self.match_input_boxes[4].minimumSizeHint().width() + 50) + + self.kill_myself = QtWidgets.QPushButton("X") + self.kill_myself.clicked.connect(lambda: self.parent_list.remove_track(self.index)) + + # First Row + self.grid = QtWidgets.QGridLayout() + self.grid.addWidget(QtWidgets.QLabel(t("Match")), 0, 0) + self.grid.addWidget(self.match_type, 0, 1) + self.grid.addWidget(QtWidgets.QLabel(t("Select By")), 0, 2) + self.grid.addWidget(self.match_item, 0, 3) + self.grid.addWidget(self.match_input, 0, 4) + self.grid.addWidget(self.kill_myself, 0, 5, 1, 5) + + self.downmix = QtWidgets.QComboBox() + self.downmix.addItems(["No Downmix"] + [str(x) for x in range(1, 16)]) + self.downmix.setCurrentIndex(0) + self.downmix.view().setFixedWidth(self.downmix.minimumSizeHint().width() + 50) + + self.convert_to = QtWidgets.QComboBox() + self.convert_to.addItems(["None | Passthrough"] + main.video_options.audio_formats) + self.convert_to.currentIndexChanged.connect(self.update_conversion) + self.convert_to.view().setFixedWidth(self.convert_to.minimumSizeHint().width() + 50) + + self.bitrate = QtWidgets.QComboBox() + self.bitrate.addItems([str(x) for x in range(32, 1024, 32)]) + self.bitrate.view().setFixedWidth(self.bitrate.minimumSizeHint().width() + 50) + + self.bitrate.setDisabled(True) + self.downmix.setDisabled(True) + + self.grid.addWidget(QtWidgets.QLabel(t("Conversion")), 1, 0) + self.grid.addWidget(self.convert_to, 1, 1) + self.grid.addWidget(QtWidgets.QLabel(t("Bitrate")), 1, 2) + self.grid.addWidget(self.bitrate, 1, 3) + self.grid.addWidget(self.downmix, 1, 4) + self.grid.setColumnStretch(3, 0) + self.grid.setColumnStretch(4, 0) + self.grid.setColumnStretch(5, 0) + + self.setLayout(self.grid) + + def update_combos(self): + self.match_input.hide() + self.match_input = self.match_input_boxes[self.match_item.currentIndex()] + + self.grid.addWidget(self.match_input, 0, 4) + self.match_input.show() + + def update_conversion(self): + if self.convert_to.currentIndex() == 0: + self.bitrate.setDisabled(True) + self.downmix.setDisabled(True) + else: + self.bitrate.setEnabled(True) + self.downmix.setEnabled(True) + + def set_outdex(self, pos): + pass + + def set_first(self, pos): + pass + + def set_last(self, pos): + pass + + def get_settings(self): + match_item_enum = match_item_enums[self.match_item.currentIndex()] + if match_item_enum in (MatchItem.ALL, MatchItem.TITLE): + match_input_value = self.match_input.text() + elif match_item_enum == MatchItem.TRACK: + match_input_value = self.match_input.currentText() + elif match_item_enum == MatchItem.LANGUAGE: + match_input_value = Lang(self.match_input.currentText()).pt2b + elif match_item_enum == MatchItem.CHANNELS: + match_input_value = str(self.match_input.currentIndex()) + else: + raise Exception("Internal error, what do we do sir?") + + return AudioMatch( + match_type=match_type_eng[self.match_type.currentIndex()], + match_item=match_item_enum, + match_input=match_input_value, + conversion=self.convert_to.currentText() if self.convert_to.currentIndex() > 0 else None, + bitrate=self.bitrate.currentText(), + downmix=self.downmix.currentIndex(), + ) + + +class AudioSelect(FlixList): + def __init__(self, app, parent, main): + super().__init__(app, parent, "Audio Select", "Audio") + self.tracks = [] + self.main = main + + self.passthrough_checkbox = QtWidgets.QCheckBox(t("Passthrough All")) + self.add_button = QtWidgets.QPushButton(f' {t("Add Pattern Match")} ') + if self.app.fastflix.config.theme == "onyx": + self.add_button.setStyleSheet("border-radius: 10px;") + + self.passthrough_checkbox.toggled.connect(self.passthrough_check) + + self.add_button.clicked.connect(self.add_track) + + layout = self.layout() + # self.scroll_area = super().scroll_area + layout.removeWidget(self.scroll_area) + + layout.addWidget(self.passthrough_checkbox, 0, 0) + layout.addWidget(self.add_button, 0, 1, alignment=QtCore.Qt.AlignRight) + layout.addWidget(self.scroll_area, 1, 0, 1, 2) + self.passthrough_checkbox.setChecked(True) + # self.passthrough_checkbox.setChecked(True) + super()._new_source(self.tracks) + + def add_track(self): + self.tracks.append(AudioProfile(self, self.app, self.main, self.inner_widget, len(self.tracks))) + self.reorder(height=126) + + def remove_track(self, index): + self.tracks.pop(index).close() + for i, track in enumerate(self.tracks): + track.index = i + self.reorder(height=126) + + def passthrough_check(self): + if self.passthrough_checkbox.isChecked(): + self.scroll_area.setDisabled(True) + self.add_button.setDisabled(True) + else: + self.scroll_area.setEnabled(True) + self.add_button.setEnabled(True) + + def get_settings(self): + if self.passthrough_checkbox.isChecked(): + return None + filters = [] + for track in self.tracks: + filters.append(track.get_settings()) + return filters + + +class SubtitleSelect(QtWidgets.QWidget): + def __init__(self, app, parent): + super().__init__() + + self.app = app + self.parent = parent + + sub_language_label = QtWidgets.QLabel(t("Subtitle select language")) + self.sub_language = QtWidgets.QComboBox() + self.sub_language.addItems([t("All"), t("None")] + language_list) + self.sub_language.insertSeparator(1) + self.sub_language.insertSeparator(3) + self.sub_language.setFixedWidth(250) + self.sub_first_only = QtWidgets.QCheckBox(t("Only select first matching Subtitle Track")) + self.sub_language.view().setFixedWidth(self.sub_language.minimumSizeHint().width() + 50) + + self.sub_burn_in = QtWidgets.QCheckBox(t("Auto Burn-in first forced or default subtitle track")) + + layout = QtWidgets.QGridLayout() + layout.addWidget(sub_language_label, 0, 0) + layout.addWidget(self.sub_language, 0, 1) + layout.addWidget(self.sub_first_only, 1, 0) + layout.addWidget(self.sub_burn_in, 2, 0, 1, 2) + layout.addWidget(QtWidgets.QWidget(), 3, 0, 1, 2) + layout.setRowStretch(3, True) + self.setLayout(layout) + + +class AdvancedTab(QtWidgets.QTabWidget): + def __init__(self, advanced_settings): + super().__init__() + + layout = QtWidgets.QVBoxLayout() + self.label = QtWidgets.QLabel() + + self.color_primaries_widget = QtWidgets.QComboBox() + self.color_primaries_widget.addItem(t("Unspecified")) + self.color_primaries_widget.addItems(ffmpeg_valid_color_primaries) + + self.color_transfer_widget = QtWidgets.QComboBox() + self.color_transfer_widget.addItem(t("Unspecified")) + self.color_transfer_widget.addItems(ffmpeg_valid_color_transfers) + + self.color_space_widget = QtWidgets.QComboBox() + self.color_space_widget.addItem(t("Unspecified")) + self.color_space_widget.addItems(ffmpeg_valid_color_space) + + primaries_layout = QtWidgets.QHBoxLayout() + primaries_layout.addWidget(QtWidgets.QLabel(t("Color Primaries"))) + primaries_layout.addWidget(self.color_primaries_widget) + + transfer_layout = QtWidgets.QHBoxLayout() + transfer_layout.addWidget(QtWidgets.QLabel(t("Color Transfer"))) + transfer_layout.addWidget(self.color_transfer_widget) + + space_layout = QtWidgets.QHBoxLayout() + space_layout.addWidget(QtWidgets.QLabel(t("Color Space"))) + space_layout.addWidget(self.color_space_widget) + + layout.addLayout(primaries_layout) + layout.addLayout(transfer_layout) + layout.addLayout(space_layout) + layout.addStretch(1) + layout.addWidget(self.label) + layout.addStretch(1) + self.text_update(advanced_settings) + self.setLayout(layout) + + def text_update(self, advanced_settings): + ignored = ("color_primaries", "color_transfer", "color_space", "denoise_type_index", "denoise_strength_index") + settings = "\n".join(f"{k:<30} {v}" for k, v in advanced_settings.dict().items() if k not in ignored) + self.label.setText(f"
{settings}
") + + +class PrimaryOptions(QtWidgets.QTabWidget): + def __init__(self, main_options): + super().__init__() + + layout = QtWidgets.QVBoxLayout() + self.label = QtWidgets.QLabel() + settings = "\n".join(f"{k:<30} {v}" for k, v in main_options.items()) + self.label.setText(f"
{settings}
") + + self.auto_crop = QtWidgets.QCheckBox(t("Auto Crop")) + + layout.addWidget(self.auto_crop) + layout.addStretch(1) + layout.addWidget(self.label) + layout.addStretch(1) + self.setLayout(layout) + + +class EncoderOptions(QtWidgets.QTabWidget): + def __init__(self, app, main): + super().__init__() + self.main = main + self.app = app + self.label = QtWidgets.QLabel() + + layout = QtWidgets.QVBoxLayout() + + layout.addWidget(self.label) + layout.addStretch(1) + + self.update_settings() + + self.setLayout(layout) + + def update_settings(self): + settings = "\n".join(f"{k:<30} {v}" for k, v in self.main.encoder.dict().items()) + self.label.setText(f"
{settings}
") + + +class ProfileWindow(QtWidgets.QWidget): + def __init__(self, app: FastFlixApp, main, *args, **kwargs): + super().__init__(None, *args, **kwargs) + self.app = app + self.main = main + self.config_file = self.app.fastflix.config.config_path + self.setWindowTitle(t("New Profile")) + self.setMinimumSize(500, 450) + layout = QtWidgets.QGridLayout() + + profile_name_label = QtWidgets.QLabel(t("Profile Name")) + profile_name_label.setFixedHeight(40) + self.profile_name = QtWidgets.QLineEdit() + if self.app.fastflix.config.theme == "onyx": + self.profile_name.setStyleSheet("background-color: #707070; border-radius: 10px; color: black") + self.profile_name.setFixedWidth(300) + + self.advanced_options: AdvancedOptions = main.video_options.advanced.get_settings() + + self.encoder = x265Settings(crf=18) + + theme = "QPushButton{ padding: 0 10px; font-size: 14px; }" + if self.app.fastflix.config.theme in ("dark", "onyx"): + theme = """ + QPushButton { + padding: 0 10px; + font-size: 14px; + background-color: #4f4f4f; + border: none; + border-radius: 10px; + color: white; } + QPushButton:hover { + background-color: #6b6b6b; }""" + + save_button = QtWidgets.QPushButton(t("Create Profile")) + save_button.setStyleSheet(theme) + save_button.clicked.connect(self.save) + save_button.setMaximumWidth(150) + save_button.setFixedHeight(60) + + v_flip, h_flip = self.main.get_flips() + self.main_settings = Box( + keep_aspect_ratio=self.main.widgets.scale.keep_aspect.isChecked(), + fast_seek=self.main.fast_time, + rotate=self.main.widgets.rotate.currentIndex(), + vertical_flip=v_flip, + horizontal_flip=h_flip, + copy_chapters=self.main.copy_chapters, + remove_metadata=self.main.remove_metadata, + remove_hdr=self.main.remove_hdr, + ) + + self.tab_area = QtWidgets.QTabWidget() + self.tab_area.setMinimumWidth(500) + self.audio_select = AudioSelect(self.app, self, self.main) + self.subtitle_select = SubtitleSelect(self.app, self) + self.advanced_tab = AdvancedTab(self.advanced_options) + self.primary_tab = PrimaryOptions(self.main_settings) + self.encoder_tab = EncoderOptions(self.app, self) + self.tab_area.addTab(self.primary_tab, "Primary Settings") + self.tab_area.addTab(self.encoder_tab, "Video") + self.tab_area.addTab(self.audio_select, "Audio") + self.tab_area.addTab(self.subtitle_select, "Subtitles") + self.tab_area.addTab(self.advanced_tab, "Advanced Options") + # self.tab_area.addTab(self.subtitle_select, "Subtitles") + # self.tab_area.addTab(SubtitleSelect(self.app, self, "Subtitle Select", "subtitles"), "Subtitle Select") + + layout.addWidget(profile_name_label, 0, 0) + layout.addWidget(self.profile_name, 0, 1, 1, 2, alignment=QtCore.Qt.AlignCenter) + # layout.addWidget(self.auto_crop, 1, 0) + # layout.addWidget(audio_language_label, 2, 0) + # layout.addWidget(self.audio_language, 2, 1) + # layout.addWidget(self.audio_first_only, 3, 1) + # layout.addWidget(self.encoder_label, 7, 0, 1, 2) + # layout.addWidget(self.encoder_settings, 8, 0, 10, 2) + layout.addWidget(save_button, 0, 5, alignment=QtCore.Qt.AlignRight) + + layout.addWidget(self.tab_area, 1, 0, 20, 6) + + layout.setColumnStretch(0, 0) + layout.setColumnStretch(1, 0) + layout.setColumnStretch(2, 1) + self.update_settings() + + self.setLayout(layout) + + def update_settings(self): + try: + encoder = self.app.fastflix.current_video.video_settings.video_encoder_settings + except AttributeError: + pass + else: + if encoder: + self.encoder = encoder + self.encoder_tab.update_settings() + self.advanced_options = self.main.video_options.advanced.get_settings() + self.advanced_tab.text_update(self.advanced_options) + + def save(self): + profile_name = self.profile_name.text().strip() + if not profile_name: + return error_message(t("Please provide a profile name")) + if profile_name in self.app.fastflix.config.profiles: + return error_message(f'{t("Profile")} {self.profile_name.text().strip()} {t("already exists")}') + + sub_lang = "en" + subtitle_enabled = True + subtitle_select_preferred_language = False + if self.subtitle_select.sub_language.currentIndex() == 2: # None + subtitle_select_preferred_language = False + subtitle_enabled = False + elif self.subtitle_select.sub_language.currentIndex() != 0: + subtitle_select_preferred_language = True + sub_lang = Lang(self.subtitle_select.sub_language.currentText()).pt2b + + self.advanced_options.color_space = ( + None + if self.advanced_tab.color_space_widget.currentIndex() == 0 + else self.advanced_tab.color_space_widget.currentText() + ) + self.advanced_options.color_transfer = ( + None + if self.advanced_tab.color_transfer_widget.currentIndex() == 0 + else self.advanced_tab.color_transfer_widget.currentText() + ) + self.advanced_options.color_primaries = ( + None + if self.advanced_tab.color_primaries_widget.currentIndex() == 0 + else self.advanced_tab.color_primaries_widget.currentText() + ) + + new_profile = Profile( + profile_version=2, + auto_crop=self.primary_tab.auto_crop.isChecked(), + keep_aspect_ratio=self.main_settings.keep_aspect_ratio, + fast_seek=self.main_settings.fast_seek, + rotate=self.main_settings.rotate, + vertical_flip=self.main_settings.vertical_flip, + horizontal_flip=self.main_settings.horizontal_flip, + copy_chapters=self.main_settings.copy_chapters, + remove_metadata=self.main_settings.remove_metadata, + remove_hdr=self.main_settings.remove_hdr, + audio_filters=self.audio_select.get_settings(), + # subtitle_filters=self.subtitle_select.get_settings(), + subtitle_language=sub_lang, + subtitle_select=subtitle_enabled, + subtitle_automatic_burn_in=self.subtitle_select.sub_burn_in.isChecked(), + subtitle_select_preferred_language=subtitle_select_preferred_language, + subtitle_select_first_matching=self.subtitle_select.sub_first_only.isChecked(), + encoder=self.encoder.name, + advanced_options=self.advanced_options, + ) + + if isinstance(self.encoder, x265Settings): + new_profile.x265 = self.encoder + elif isinstance(self.encoder, x264Settings): + new_profile.x264 = self.encoder + elif isinstance(self.encoder, rav1eSettings): + new_profile.rav1e = self.encoder + elif isinstance(self.encoder, SVTAV1Settings): + new_profile.svt_av1 = self.encoder + elif isinstance(self.encoder, VP9Settings): + new_profile.vp9 = self.encoder + elif isinstance(self.encoder, AOMAV1Settings): + new_profile.aom_av1 = self.encoder + elif isinstance(self.encoder, GIFSettings): + new_profile.gif = self.encoder + elif isinstance(self.encoder, WebPSettings): + new_profile.webp = self.encoder + elif isinstance(self.encoder, CopySettings): + new_profile.copy_settings = self.encoder + elif isinstance(self.encoder, NVEncCSettings): + new_profile.nvencc_hevc = self.encoder + elif isinstance(self.encoder, NVEncCAVCSettings): + new_profile.nvencc_avc = self.encoder + elif isinstance(self.encoder, FFmpegNVENCSettings): + new_profile.ffmpeg_hevc_nvenc = self.encoder + elif isinstance(self.encoder, VCEEncCSettings): + new_profile.vceencc_hevc = self.encoder + elif isinstance(self.encoder, VCEEncCAVCSettings): + new_profile.vceencc_avc = self.encoder + else: + logger.error("Profile cannot be saved! Unknown encoder type.") + return + + self.app.fastflix.config.profiles[profile_name] = new_profile + self.app.fastflix.config.selected_profile = profile_name + self.app.fastflix.config.save() + self.main.widgets.profile_box.addItem(profile_name) + self.main.widgets.profile_box.setCurrentText(profile_name) + self.hide() + + def delete_current_profile(self): + if self.app.fastflix.config.selected_profile in get_preset_defaults(): + return error_message( + f"{self.app.fastflix.config.selected_profile} " f"{t('is a default profile and will not be removed')}" + ) + self.main.loading_video = True + del self.app.fastflix.config.profiles[self.app.fastflix.config.selected_profile] + self.app.fastflix.config.selected_profile = "Standard Profile" + self.app.fastflix.config.save() + self.main.widgets.profile_box.clear() + self.main.widgets.profile_box.addItems(self.app.fastflix.config.profiles.keys()) + self.main.loading_video = False + self.main.widgets.profile_box.setCurrentText("Standard Profile") + self.main.widgets.convert_to.setCurrentIndex(0) diff --git a/requirements.txt b/requirements.txt index f0497e1a..9fe78b4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ -appdirs==1.4.4 -colorama==0.4.4 -coloredlogs==15.0 +appdirs~=1.4.4 +colorama~=0.4.4 +coloredlogs~=15.0 iso639-lang==0.0.9 -mistune==0.8.4 -pathvalidate==2.4.1 -psutil==5.8.0 -pydantic==1.8.2 -PySide6==6.2.2.1 -python-box[all]==6.0.0rc3 -requests==2.25.1 -reusables==0.9.6 +mistune~=0.8.4 +pathvalidate~=2.4.1 +psutil~=5.8.0 +pydantic~=1.8.2 +PySide6~=6.2.2.1 +python-box[all]~=6.0.0rc4 +requests~=2.25.1 +reusables~=0.9.6