From 322618038b875d622f059973bd913cfb837959dc Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sun, 24 Sep 2023 09:28:34 -0500 Subject: [PATCH 01/15] * Fixing VVC encode options (thanks to Chriss) * Removing distutils in favor of packaging --- CHANGES | 5 +++++ fastflix/encoders/vvc/command_builder.py | 12 ++++++++---- fastflix/shared.py | 4 ++-- fastflix/version.py | 2 +- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/CHANGES b/CHANGES index 54dcfc5c..d3d62497 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,10 @@ # Changelog +## Version 5.5.8 + +* Fixing VVC encode options (thanks to Chriss) +* Removing distutils in favor of packaging + ## Version 5.5.7 * Fixing #503 missing CRF mode for SVT-AV1 (thanks to ignace72) diff --git a/fastflix/encoders/vvc/command_builder.py b/fastflix/encoders/vvc/command_builder.py index 41e01b7a..cd996142 100644 --- a/fastflix/encoders/vvc/command_builder.py +++ b/fastflix/encoders/vvc/command_builder.py @@ -5,6 +5,7 @@ from fastflix.encoders.common.helpers import Command, generate_all, null from fastflix.models.encode import VVCSettings from fastflix.models.fastflix import FastFlix +from fastflix.shared import clean_file_string, quoted_path vvc_valid_color_primaries = [ "bt709", @@ -106,13 +107,16 @@ def get_vvc_params(params=()): return '-vvenc-params "{}" '.format(":".join(all_params)) if all_params else "" if settings.bitrate: + params = get_vvc_params(["pass=1", f"rcstatsfile={quoted_path(clean_file_string(pass_log_file))}"]) command_1 = ( - f'{beginning} {get_vvc_params(["pass=1", "no-slow-firstpass=1"])} ' - f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra if settings.extra_both_passes else ""} ' + f"{beginning} {params} " + f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} ' + f'-preset:v {settings.preset} {settings.extra if settings.extra_both_passes else ""} ' f" -an -sn -dn {output_fps} -f mp4 {null}" ) + params2 = get_vvc_params(["pass=2", f"rcstatsfile={quoted_path(clean_file_string(pass_log_file))}"]) command_2 = ( - f'{beginning} {get_vvc_params(["pass=2"])} -passlogfile "{pass_log_file}" ' + f'{beginning} {params2} -passlogfile "{pass_log_file}" ' f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} {ending}" ) return [ @@ -122,7 +126,7 @@ def get_vvc_params(params=()): elif settings.qp: command = ( - f"{beginning} {get_vvc_params()} -qp:v {settings.qp} " + f"{beginning} {get_vvc_params()} -qp:v {settings.qp} -b:v 0 " f"-preset:v {settings.preset} {settings.extra} {ending}" ) return [Command(command=command, name="Single pass CRF", exe="ffmpeg")] diff --git a/fastflix/shared.py b/fastflix/shared.py index 4ce82781..d1861364 100644 --- a/fastflix/shared.py +++ b/fastflix/shared.py @@ -4,7 +4,7 @@ import os import sys from datetime import datetime, timedelta -from distutils.version import StrictVersion +from packaging import version as packaging_version from pathlib import Path from subprocess import run import platform @@ -165,7 +165,7 @@ def latest_fastflix(app, show_new_dialog=False): release = [x for x in data if x["tag_name"] == use_version][0] - if use_version != __version__ and StrictVersion(use_version) > StrictVersion(__version__): + if use_version != __version__ and packaging_version.parse(use_version) > packaging_version.parse(__version__): portable, installer = None, None for asset in release["assets"]: if asset["name"].endswith("win64.zip"): diff --git a/fastflix/version.py b/fastflix/version.py index 93e651c0..86291754 100644 --- a/fastflix/version.py +++ b/fastflix/version.py @@ -1,4 +1,4 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -__version__ = "5.5.7" +__version__ = "5.5.8b0" __author__ = "Chris Griffith" From 5c787c7fab8a0d47aea381cdc9dea968d40f6608 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sun, 5 Nov 2023 14:58:02 -0600 Subject: [PATCH 02/15] * Adding Passes option for bitrate mode in x265 and x264 (thanks to Chriss) * Fixing #532 Trying to crop spams crop error (gendalv) * Removing #525 invalid 12 bit options for x264 (thanks to Chriss) * Removing #497 #519 advanced checks for hardware encoders, due to them not always being detected (thanks to CptnFluffy) --- CHANGES | 5 +- fastflix/application.py | 38 +- fastflix/data/languages.yaml | 363 ++++++++++++++---- fastflix/encoders/avc_x264/command_builder.py | 28 +- fastflix/encoders/avc_x264/settings_panel.py | 6 +- fastflix/encoders/common/setting_panel.py | 19 +- .../encoders/hevc_x265/command_builder.py | 30 +- fastflix/encoders/hevc_x265/settings_panel.py | 3 +- fastflix/models/encode.py | 2 + fastflix/version.py | 2 +- fastflix/widgets/main.py | 31 +- 11 files changed, 396 insertions(+), 131 deletions(-) diff --git a/CHANGES b/CHANGES index d3d62497..ade0d083 100644 --- a/CHANGES +++ b/CHANGES @@ -1,9 +1,12 @@ # Changelog -## Version 5.5.8 +## Version 5.6.0 +* Adding Passes option for bitrate mode in x265 and x264 (thanks to Chriss) * Fixing VVC encode options (thanks to Chriss) * Removing distutils in favor of packaging +* Removing #525 invalid 12 bit options for x264 (thanks to Chriss) +* Removing #497 #519 advanced checks for hardware encoders, due to them not always being detected (thanks to CptnFluffy) ## Version 5.5.7 diff --git a/fastflix/application.py b/fastflix/application.py index dfb4389a..ef670394 100644 --- a/fastflix/application.py +++ b/fastflix/application.py @@ -115,29 +115,29 @@ def init_encoders(app: FastFlixApp, **_): encoders.insert(encoders.index(avc_plugin), vceencc_avc_plugin) else: if app.fastflix.config.qsvencc: - if "H.265/HEVC" in app.fastflix.config.qsvencc_encoders: - encoders.insert(1, qsvencc_plugin) - if "AV1" in app.fastflix.config.qsvencc_encoders: - encoders.insert(encoders.index(av1_plugin), qsvencc_av1_plugin) - if "H.264/AVC" in app.fastflix.config.qsvencc_encoders: - encoders.insert(encoders.index(avc_plugin), qsvencc_avc_plugin) + # if "H.265/HEVC" in app.fastflix.config.qsvencc_encoders: + encoders.insert(1, qsvencc_plugin) + # if "AV1" in app.fastflix.config.qsvencc_encoders: + encoders.insert(encoders.index(av1_plugin), qsvencc_av1_plugin) + # if "H.264/AVC" in app.fastflix.config.qsvencc_encoders: + encoders.insert(encoders.index(avc_plugin), qsvencc_avc_plugin) if app.fastflix.config.nvencc: - if "H.265/HEVC" in app.fastflix.config.nvencc_encoders: - encoders.insert(1, nvencc_plugin) - if "AV1" in app.fastflix.config.nvencc_encoders: - encoders.insert(encoders.index(av1_plugin), nvencc_av1_plugin) - if "H.264/AVC" in app.fastflix.config.nvencc_encoders: - encoders.insert(encoders.index(avc_plugin), nvencc_avc_plugin) + # if "H.265/HEVC" in app.fastflix.config.nvencc_encoders: + encoders.insert(1, nvencc_plugin) + # if "AV1" in app.fastflix.config.nvencc_encoders: + encoders.insert(encoders.index(av1_plugin), nvencc_av1_plugin) + # if "H.264/AVC" in app.fastflix.config.nvencc_encoders: + encoders.insert(encoders.index(avc_plugin), nvencc_avc_plugin) if app.fastflix.config.vceencc: - if reusables.win_based and "H.265/HEVC" in app.fastflix.config.vceencc_encoders: - # HEVC AMF support only works on windows currently - encoders.insert(1, vceencc_hevc_plugin) - if "AV1" in app.fastflix.config.vceencc_encoders: - encoders.insert(encoders.index(av1_plugin), vceencc_av1_plugin) - if "H.264/AVC" in app.fastflix.config.vceencc_encoders: - encoders.insert(encoders.index(avc_plugin), vceencc_avc_plugin) + # if reusables.win_based: # and "H.265/HEVC" in app.fastflix.config.vceencc_encoders: + # HEVC AMF support only works on windows currently + encoders.insert(1, vceencc_hevc_plugin) + # if "AV1" in app.fastflix.config.vceencc_encoders: + encoders.insert(encoders.index(av1_plugin), vceencc_av1_plugin) + # if "H.264/AVC" in app.fastflix.config.vceencc_encoders: + encoders.insert(encoders.index(avc_plugin), vceencc_avc_plugin) app.fastflix.encoders = { encoder.name: encoder diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index dddfb764..675f91cf 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -839,7 +839,8 @@ Command worker received request to pause encoding after the current item complet jpn: コマンドワーカーが、現在のアイテムが完了した後にエンコードを一時停止するリクエストを受信しました rus: Оператор получил запрос на приостановку кодирования после завершения текущего элемента - por: O worker recebeu uma solicitação para pausar a codificação atual após a sua conclusão + por: O worker recebeu uma solicitação para pausar a codificação atual após a sua + conclusão swe: Kommandotjänstemannen har mottagit en begäran om att pausa kodningen efter det att det aktuella objektet har avslutats. pol: Pracownik poleceń otrzymał żądanie wstrzymania kodowania po zakończeniu bieżącego @@ -1446,8 +1447,8 @@ Default is an autodetected count based on the number of CPU cores and whether WP jpn: デフォルトでは、CPUコア数とWPPが有効かどうかに基づいて自動検出されたカウントです。 rus: По умолчанию это автоопределяемый подсчет, основанный на количестве ядер ЦП и на том, включен или нет WPP. - por: O padrão é uma contagem calculada pelo número de núcleos da CPU e se o WPP está - habilitado ou não. + por: O padrão é uma contagem calculada pelo número de núcleos da CPU e se o WPP + está habilitado ou não. swe: Standardvärdet är ett automatiskt registrerat antal baserat på antalet CPU-kärnor och om WPP är aktiverat eller inte. pol: Domyślnie jest to automatycznie wykryta liczba na podstawie liczby rdzeni CPU @@ -1463,8 +1464,8 @@ Default is an autodetected count based on the number of CPU cores and whether WP fra: 'Par défaut : AQ activé avec auto-variance' ita: 'Default: AQ abilitato con auto-varianza' spa: 'Por defecto: AQ habilitado con auto-varianza' - chs: '默认值为enabled + auto-variance' - jpn: 'デフォルトは自動分散でAQを有効にする' + chs: 默认值为enabled + auto-variance + jpn: デフォルトは自動分散でAQを有効にする rus: 'По умолчанию: AQ включен с автоматической дисперсией' por: 'Padrão: AQ habilitado com auto-variação' swe: 'Standard: AQ aktiverad med automatisk variation' @@ -1637,8 +1638,8 @@ Dither is an intentionally applied form of noise used to randomize quantization jpn: ディザとは、量子化誤差をランダムにするために意図的にかけるノイズのことです。 rus: Дизеринг - это намеренно применяемая форма шума, используемая для рандомизации ошибки квантования, - por: Dither é uma forma intencionalmente aplicada de ruído usado para randomizar o - erro de quantização, + por: Dither é uma forma intencionalmente aplicada de ruído usado para randomizar + o erro de quantização, swe: Dither är en avsiktligt tillämpad form av brus som används för att slumpa kvantiseringsfel, pol: Dither to celowo zastosowana forma szumu używana do randomizacji błędu kwantyzacji, ukr: Дизер - це навмисно застосована форма шуму, яка використовується для рандомізації @@ -1842,8 +1843,8 @@ Enables true lossless coding by bypassing scaling, transform, quantization and i jpn: スケーリング、トランスフォーム、量子化、インループフィルタリングをバイパスすることで、真のロスレスコーディングを可能にします。 rus: Обеспечивает прямое кодирование без потерь, минуя масштабирование, преобразование, квантование и фильтрацию в контуре. - por: Habilita a codificação lossless ao ignorar dimensionamento, transformação, quantização - e filtragem em loop. + por: Habilita a codificação lossless ao ignorar dimensionamento, transformação, + quantização e filtragem em loop. swe: Möjliggör verklig förlustfri kodning genom att kringgå skalning, omvandling, kvantisering och filtrering i loopen. pol: Umożliwia prawdziwie bezstratne kodowanie poprzez ominięcie skalowania, transformacji, @@ -3036,8 +3037,8 @@ Lossless encodes implicitly have no rate control, all rate control options are i jpn: ロスレスエンコードでは、レートコントロールが行われず、すべてのレートコントロールオプションは無視されます。 rus: Кодирование без потерь неявно не имеет контроля скорости, все опции контроля скорости игнорируются. - por: Os códigos lossless não têm implicitamente nenhum controle de taxa, todas - as opções de controle de taxa são ignoradas. + por: Os códigos lossless não têm implicitamente nenhum controle de taxa, todas as + opções de controle de taxa são ignoradas. swe: Förlustfria kodningar har implicit ingen hastighetsreglering, alla alternativ för hastighetsreglering ignoreras. pol: Kodowanie bezstratne domyślnie nie ma kontroli szybkości, wszystkie opcje kontroli @@ -4355,8 +4356,8 @@ Row multithreading: fra: 'Courir après le commandement fait :' ita: 'Esecuzione dopo aver eseguito il comando:' spa: 'Corriendo tras el mando hecho:' - chs: '在完成命令后运行。' - jpn: 'コマンド完了後に実行。' + chs: 在完成命令后运行。 + jpn: コマンド完了後に実行。 rus: 'Запуск после выполнения команды:' por: 'Executando o comando pós-conclusão:' swe: 'Körs efter kommandot done:' @@ -4617,8 +4618,8 @@ Set the level of effort in determining B frame placement.: fra: 'Régler après avoir fait la commande à :' ita: 'Impostare dopo il comando su:' spa: 'Estableciendo después de hacer el comando para:' - chs: '设置完成后命令为' - jpn: 'コマンドを実行した後の設定' + chs: 设置完成后命令为 + jpn: コマンドを実行した後の設定 rus: 'Установка после выполненной команды на:' por: 'Definindo o comando após a conclusão para:' swe: 'Ställ in kommandot efter utfört kommando till:' @@ -4731,8 +4732,8 @@ Slower presets will generally achieve better compression efficiency (and generat jpn: 一般的には、遅いプリセットの方が圧縮効率が良くなります(より小さなビットストリームを生成します)。 rus: Более медленные пресеты обычно обеспечивают лучшую эффективность сжатия (и генерируют меньшие битовые потоки). - por: Presets mais lentos geralmente alcançam melhor eficiência de compressão (e geram - bitstreams menores). + por: Presets mais lentos geralmente alcançam melhor eficiência de compressão (e + geram bitstreams menores). swe: Långsammare förinställningar ger i allmänhet bättre kompressionseffektivitet (och genererar mindre bitströmmar). pol: Wolniejsze presety generalnie osiągają lepszą wydajność kompresji (i generują @@ -4959,7 +4960,7 @@ Support FastFlix: ita: Supporto FastFlix spa: Soporta FastFlix chs: 支持FastFlix - jpn: 'FastFlixを応援/寄付' + jpn: FastFlixを応援/寄付 rus: Поддержка FastFlix por: Suporte FastFlix swe: Stöd för FastFlix @@ -5151,8 +5152,8 @@ This is intended for use when you do not have a container to keep the stream hea jpn: ストリームヘッダーを保持してくれるコンテナがない場合に使用することを想定しています。 rus: Это предназначено для использования, когда у вас нет контейнера для хранения заголовков потока. - por: Isso é destinado para uso quando você não tem um contêiner para manter os - cabeçalhos do stream para você + por: Isso é destinado para uso quando você não tem um contêiner para manter os cabeçalhos + do stream para você swe: Detta är avsett att användas när du inte har en behållare som behåller stream headers åt dig. pol: To jest przeznaczone do użycia, gdy nie masz kontenera, który przechowuje nagłówki @@ -5549,8 +5550,8 @@ VBR Target: fra: 'Les valeurs : 0:aucun ; 1:rapide ; 2:plein(treillis) par défaut' ita: 'Valori: 0:nessuno; 1:veloce; 2:pieno (traliccio) predefinito' spa: 'Valores: 0:ninguno; 1:rápido; 2:completo (enrejado) por defecto' - chs: '取值:0:none;1:fast;2:full(trellis)(默认)' - jpn: '値を指定します。0:なし、1:速い、2:フル(tresllis)デフォルト' + chs: 取值:0:none;1:fast;2:full(trellis)(默认) + jpn: 値を指定します。0:なし、1:速い、2:フル(tresllis)デフォルト rus: 'Значения: 0:нет; 1:быстро; 2:полностью (решетка) по умолчанию' por: 'Valores: 0:nenhum; 1:rápido; 2:completo(trellis) padrão' swe: 'Värden: 0:ingen; 1:snabb; 2:full(trellis) standard' @@ -5838,8 +5839,8 @@ With b-adapt 0, the GOP structure is fixed based on the values of --keyint and - jpn: b-adapt 0では、--keyintおよび--bframesの値に基づいてGOP構造が固定されます。 rus: При b-adapt 0 структура GOP фиксируется на основе значений параметров --keyint и --bframes. - por: Com b-adapt 0, a estrutura GOP é definida com base nos valores de --keyint e - --bframes. + por: Com b-adapt 0, a estrutura GOP é definida com base nos valores de --keyint + e --bframes. swe: Med b-adapt 0 fastställs GOP-strukturen baserat på värdena för --keyint och --bframes. pol: Przy b-adapt 0, struktura GOP jest ustalana na podstawie wartości --keyint @@ -5976,8 +5977,8 @@ and the amount of work performed by the full trellis version of --b-adapt lookah chs: lookahead在full(trellis)模式下执行的工作量有二次方的影响。 jpn: と、フルtrellis版の--b-adapt lookaheadによる作業量を示しています。 rus: и объем работы, выполняемой версией полной решетки --b-adapt lookahead. - por: e a quantidade de trabalho realizado pela versão trellis completa de - --b-adapt lookahead. + por: e a quantidade de trabalho realizado pela versão trellis completa de --b-adapt + lookahead. swe: och den mängd arbete som utförs av den fullständiga trellisversionen av --b-adapt lookahead. pol: oraz ilość pracy wykonanej przez wersję full trellis z --b-adapt lookahead. @@ -6077,8 +6078,8 @@ b-adapt: 'b-adapt: Set the level of effort in determining B frame placement.': deu: 'b-adapt: Festlegen des Grades des Aufwands bei der Bestimmung der B-Frame-Platzierung.' eng: 'b-adapt: Set the level of effort in determining B frame placement.' - fra: "b-adapt : Fixe le niveau d'effort pour déterminer le placement de l'image\ - \ B." + fra: "b-adapt : Fixe le niveau d'effort pour déterminer le placement de l'image + B." ita: 'b-adatta: Impostare il livello di sforzo nel determinare il posizionamento del telaio B.' spa: 'b-adaptado: Establece el nivel de esfuerzo para determinar la colocación del @@ -6156,7 +6157,7 @@ bframes: ita: 'bframes: Numero massimo di b-frame consecutivi. ' spa: 'bframes: Número máximo de b-frames consecutivos. ' chs: bframes:连续B帧的最大数量。 - jpn: 'bframes:連続するb-フレームの最大数。' + jpn: bframes:連続するb-フレームの最大数。 rus: 'bframes: Максимальное количество последовательных b-кадров.' por: 'bframes: Número máximo de b-frames consecutivos.' swe: 'bframes: Maximalt antal på varandra följande b-frames.' @@ -6275,8 +6276,8 @@ data tracks found: fra: 'dhdr10-opt : Réduire les frais généraux du SEI' ita: 'dhdr10-opt: Riduce le spese generali SEI' spa: 'dhdr10-opt: Reduce los gastos de SEI' - chs: 'dhdr10-opt:减少SEI开销' - jpn: 'dhdr10-opt:SEI のオーバーヘッドを削減' + chs: dhdr10-opt:减少SEI开销 + jpn: dhdr10-opt:SEI のオーバーヘッドを削減 rus: 'dhdr10-opt: Уменьшает накладные расходы SEI' por: 'dhdr10-opt: Reduz os custos gerais do SEI' swe: 'dhdr10-opt: Minskar SEI:s omkostnader' @@ -6290,8 +6291,8 @@ data tracks found: fra: 'exemples : level-idc=4.1:rc-lookahead=10' ita: 'esempi: level-idc=4.1:rc-lookahead=10' spa: 'ejemplos: level-idc=4.1:rc-lookahead=10' - chs: '示例:level-idc=4.1:rc-lookahead=10。' - jpn: '例:level-idc=4.1:rc-lookahead=10' + chs: 示例:level-idc=4.1:rc-lookahead=10。 + jpn: 例:level-idc=4.1:rc-lookahead=10 rus: 'примеры: level-idc=4.1:rc-lookahead=10' por: 'exemplos: level-idc=4.1:rc-lookahead=10' swe: 'exempel: level-idc=4.1:rc-lookahead=10' @@ -6305,8 +6306,8 @@ data tracks found: fra: 'des fils de trame : Nombre de trames codées simultanément.' ita: 'telaio-filettature: Numero di frame codificati simultaneamente.' spa: 'Hilos de marcos: Número de cuadros codificados simultáneamente.' - chs: 'frame-threads:同时编码的帧数。' - jpn: 'frame-threads。同時にエンコードされるフレームの数。' + chs: frame-threads:同时编码的帧数。 + jpn: frame-threads。同時にエンコードされるフレームの数。 rus: 'frame-threads: Количество параллельно кодируемых кадров.' por: 'frame-threads: Número de frames codificados simultaneamente.' swe: 'ramtrådar: Antal samtidigt kodade ramar.' @@ -6334,10 +6335,10 @@ good is the default and recommended for most applications: deu: 'hdr10-opt: Aktiviert die Luma- und Chroma-QP-Optimierung auf Blockebene für HDR10-Inhalte.' eng: 'hdr10-opt: Enable block-level luma and chroma QP optimization for HDR10 content.' - fra: "hdr10-opt : Activer l'optimisation QP luma et chroma au niveau des blocs pour\ - \ le contenu HDR10." - ita: "hdr10-opt: Attivare l'ottimizzazione a livello di blocco luma e chroma QP\ - \ per i contenuti HDR10." + fra: "hdr10-opt : Activer l'optimisation QP luma et chroma au niveau des blocs pour + le contenu HDR10." + ita: "hdr10-opt: Attivare l'ottimizzazione a livello di blocco luma e chroma QP + per i contenuti HDR10." spa: 'hdr10-opt: Habilitar la optimización de la luma a nivel de bloque y la QP cromática para el contenido de HDR10.' chs: hdr10-opt:启用HDR10内容的块级亮度和色度量化参数(Quantization Parameter, QP)优化。 @@ -6401,10 +6402,10 @@ installer: 'intra-refresh: Enables Periodic Intra Refresh(PIR) instead of keyframe insertion.': deu: 'intra-refresh: Aktiviert Periodic Intra Refresh(PIR) anstelle der Keyframe-Einblendung.' eng: 'intra-refresh: Enables Periodic Intra Refresh(PIR) instead of keyframe insertion.' - fra: "intra-refresh : Active le rafraîchissement périodique intra (PIR) au lieu\ - \ de l'insertion d'images clés." - ita: "intra-refresh: Abilita l'Intra Refresh(PIR) periodico invece dell'inserimento\ - \ del keyframe." + fra: "intra-refresh : Active le rafraîchissement périodique intra (PIR) au lieu + de l'insertion d'images clés." + ita: "intra-refresh: Abilita l'Intra Refresh(PIR) periodico invece dell'inserimento + del keyframe." spa: 'intra-refresco: Habilita el Refresco Intra Periódico (PIR) en lugar de la inserción de fotogramas clave.' chs: intra-refresh:启用周期性帧内刷新(Periodic Intra Refresh, PIR)代替关键帧插入。 @@ -6472,15 +6473,15 @@ it will generally just increase memory use.: (Blu-ray-Spezifikation)' eng: 'keyint: Enable Intra-Encoding by forcing keyframes every 1 second (Blu-ray spec)' - fra: "keyint : Activer l'intra-encodage en forçant les images clés toutes les 1\ - \ seconde (spécification Blu-ray)" - ita: "keyint: Attivare l'Intra-Encoding forzando i keyframe ogni 1 secondo (Blu-ray\ - \ spec)" + fra: "keyint : Activer l'intra-encodage en forçant les images clés toutes les 1 + seconde (spécification Blu-ray)" + ita: "keyint: Attivare l'Intra-Encoding forzando i keyframe ogni 1 secondo (Blu-ray + spec)" spa: 'keyint: Habilitar la intracodificación forzando los fotogramas clave cada 1 segundo (Blu-ray spec)' chs: 'keyint: Enable Intra-Encoding by forcing keyframes every 1 second (Blu-ray spec)' - jpn: 'keyint:1秒ごとにキーフレームを強制的に生成してイントラエンコードを有効にする(Blu-ray仕様)。' + jpn: keyint:1秒ごとにキーフレームを強制的に生成してイントラエンコードを有効にする(Blu-ray仕様)。 rus: 'keyint: Включить внутреннее кодирование путем принудительного воспроизведения ключевых кадров каждые 1 секунду (спецификация Blu-ray).' por: 'keyint: Habilitar Intra-Encoding forçando keyframes a cada 1 segundo (especificação @@ -6514,10 +6515,10 @@ lossless: output stream" zu beheben' eng: 'max_muxing_queue_size: Raise to fix "Too many packets buffered for output stream" error' - fra: "max_muxing_queue_size : Augmenter pour corriger l'erreur \"Too many packets\ - \ buffered for output stream" - ita: "max_muxing_queue_size: Alzare per correggere l'errore \"Troppi pacchetti bufferizzati\ - \ per il flusso di uscita" + fra: "max_muxing_queue_size : Augmenter pour corriger l'erreur \"Too many packets + buffered for output stream" + ita: "max_muxing_queue_size: Alzare per correggere l'errore \"Troppi pacchetti bufferizzati + per il flusso di uscita" spa: 'tamaño_muxing_queue_size: Subir para corregir el error "Demasiados paquetes almacenados en la memoria intermedia para el flujo de salida".' chs: max_muxing_queue_size:提高以解决 "输出流缓冲的数据包太多(Too many packets buffered for output @@ -6525,7 +6526,8 @@ lossless: jpn: max_muxing_queue_size 「出力ストリームにバッファリングされるパケット数が多すぎる」というエラーが発生した場合は値を上げてください。 rus: 'max_muxing_queue_size: Повышение для исправления ошибки "Слишком много пакетов буферизировано для выходного потока"' - por: 'max_muxing_queue_size: Aumentar para corrigir o erro "Too many packets buffered for output stream"' + por: 'max_muxing_queue_size: Aumentar para corrigir o erro "Too many packets buffered + for output stream"' swe: 'max_muxing_queue_size: Höj för att åtgärda felet "För många paket buffras för utdataströmmen".' pol: 'max_muxing_queue_size: Podnieś, aby naprawić błąd "Zbyt wiele pakietów buforowanych @@ -6619,8 +6621,8 @@ preset: ita: 'preimpostata: Più lento è il preset, migliore è la compressione e la qualità' spa: 'preestablecido: Cuanto más lento el preajuste, mejor será la compresión y la calidad' - chs: 'preset:较慢的预设能提供更好的压缩比和质量。' - jpn: 'プリセットの速度が遅いほど、圧縮率と品質が向上します。' + chs: preset:较慢的预设能提供更好的压缩比和质量。 + jpn: プリセットの速度が遅いほど、圧縮率と品質が向上します。 rus: 'предустановка: Чем медленнее предустановка, тем лучше сжатие и качество' por: 'preset: Quanto mais lento o preset, melhor a compressão e a qualidade' swe: 'förinställd: Ju långsammare förinställning, desto bättre komprimering och @@ -6667,8 +6669,8 @@ profile: fra: 'profil : Appliquer un profil de codage' ita: 'profilo: Applicare un profilo di codifica' spa: 'perfil: Hacer cumplir un perfil codificado' - chs: '配置:应用一个编码配置' - jpn: 'プロフィール:エンコードプロファイルを適用してください。' + chs: 配置:应用一个编码配置 + jpn: プロフィール:エンコードプロファイルを適用してください。 rus: 'профиль: Применить профиль кодирования' por: 'profile: Forçar um perfil de codificação' swe: 'profil: Genomdriva en kodningsprofil' @@ -6684,8 +6686,8 @@ profile: punta' spa: 'perfil: Perfil de codificación del VP9 - debe coincidir con la profundidad del bit' - chs: '配置:VP9编码规格——必须与位深度相匹配。' - jpn: 'プロファイルを使用しています。VP9コーディングプロファイル - ビット深度と一致する必要があります' + chs: 配置:VP9编码规格——必须与位深度相匹配。 + jpn: プロファイルを使用しています。VP9コーディングプロファイル - ビット深度と一致する必要があります rus: 'профиль: Профиль кодирования VP9 - должен соответствовать битовой глубине' por: 'profile: Perfil de codificação VP9 - deve corresponder à profundidade do bit' swe: 'profil: VP9-kodningsprofil - måste matcha bitdjupet' @@ -6739,8 +6741,8 @@ rav1e github: jpn: repeat-headersを有効にすると、x265はキーフレームごとにVPS、SPS、PPSの各ヘッダを出力します。 rus: 'повторять заголовки: Если включено, x265 будет выдавать заголовки VPS, SPS и PPS с каждым ключевым кадром.' - por: 'repeat-headers: Se ativado, x265 emitirá cabeçalhos VPS, SPS e PPS em - cada kkeyframe.' + por: 'repeat-headers: Se ativado, x265 emitirá cabeçalhos VPS, SPS e PPS em cada + kkeyframe.' swe: 'upprepa rubriker: Om den är aktiverad kommer x265 att skicka ut VPS-, SPS och PPS-rubriker med varje nyckelbild.' pol: 'repeat-headers: Jeśli włączone, x265 będzie emitować nagłówki VPS, SPS i PPS @@ -6763,8 +6765,8 @@ since the entire reference frames are always available for motion compensation,: chs: 因为总是可以获取完整的参考帧来进行运动补偿, jpn: は、リファレンスフレーム全体が常に動きの補正に利用できるからです。 rus: поскольку для компенсации движения всегда доступны все опорные кадры, - por: uma vez que todos os frames de referência estão sempre disponíveis para a - compensação de movimento, + por: uma vez que todos os frames de referência estão sempre disponíveis para a compensação + de movimento, swe: eftersom hela referensramar alltid är tillgängliga för rörelsekompensation, pol: ponieważ całe ramki odniesienia są zawsze dostępne dla kompensacji ruchu, ukr: оскільки для компенсації руху завжди доступні всі системи відліку, @@ -7243,8 +7245,8 @@ Decoder: fra: "Hardware : utiliser libavformat + décodeur matériel pour l'entrée" ita: "Hardware: usa libavformat + decoder hardware per l'ingresso" spa: 'Hardware: utilizar libavformat + decodificador de hardware para la entrada' - chs: 'Hardware:使用libavformat+硬件解码器' - jpn: 'ハードウェア:入力にlibavformat+ハードウェアデコーダを使用' + chs: Hardware:使用libavformat+硬件解码器 + jpn: ハードウェア:入力にlibavformat+ハードウェアデコーダを使用 rus: 'Аппаратное обеспечение: использование libavformat + аппаратного декодера для ввода' por: 'Hardware: usar libavformat + decodificador de hardware para entrada' @@ -7260,8 +7262,8 @@ Decoder: fra: 'Software : utiliser avcodec + décodeur logiciel' ita: 'Software: usa avcodec + decoder software' spa: 'Software: utilizar avcodec + decodificador de software' - chs: 'Software:使用avcodec + 软件解码器' - jpn: 'ソフトウェア:avcodec + ソフトウェアデコーダを使用' + chs: Software:使用avcodec + 软件解码器 + jpn: ソフトウェア:avcodec + ソフトウェアデコーダを使用 rus: 'Программное обеспечение: используйте avcodec + программный декодер' por: 'Software: usar avcodec + decodificador de software' swe: 'Programvara: Använd avcodec + mjukvaruavkodare' @@ -7574,8 +7576,8 @@ does not support concatenating files together: 'WARNING: This feature is not provided by the encoder software directly': deu: 'WARNUNG: Diese Funktion wird nicht direkt von der Encoder-Software bereitgestellt.' eng: 'WARNING: This feature is not provided by the encoder software directly' - fra: "AVERTISSEMENT : Cette fonction n'est pas fournie directement par le logiciel\ - \ de l'encodeur." + fra: "AVERTISSEMENT : Cette fonction n'est pas fournie directement par le logiciel + de l'encodeur." ita: 'ATTENZIONE: Questa funzione non è fornita direttamente dal software di codifica' spa: 'ADVERTENCIA: Esta función no es proporcionada por el software del codificador directamente' @@ -8988,8 +8990,8 @@ That video was added with an encoder that is no longer available, unable to load jpn: その動画は、利用できなくなったエンコーダーで追加されたため、キューから読み込むことができません。 rus: Это видео было добавлено с помощью кодировщика, который больше не доступен, не удается загрузить из очереди - por: Esse vídeo foi adicionado com um codificador que não está mais disponível, não - é possível carregar da fila + por: Esse vídeo foi adicionado com um codificador que não está mais disponível, + não é possível carregar da fila swe: Videon lades till med en kodare som inte längre är tillgänglig, kan inte laddas från kön. pol: Ten film został dodany za pomocą kodera, który nie jest już dostępny, nie można @@ -9280,8 +9282,8 @@ Adaptive CQM: rus: Адаптивный выбор одной из определяемых реализацией матриц квантования для каждого кадра для улучшения субъективного визуального качества при определенных условиях. por: Selecionar de forma adaptativa uma das matrizes de quantização definidas pela - implementação para cada frame, para melhorar a qualidade visual subjetiva em - determinadas condições. + implementação para cada frame, para melhorar a qualidade visual subjetiva em determinadas + condições. swe: Adaptivt välja en av de kvantiseringsmatriser som definieras av implementeringen för varje bild för att förbättra den subjektiva visuella kvaliteten under vissa förhållanden. @@ -9868,7 +9870,8 @@ Async Depth: de un solo canal. Esta opción no funciona si el controlador no implementa la función vaSyncBuffer. chs: 最大的处理并行性。增加这个选项可以提高单通道的性能。如果驱动程序没有实现vaSyncBuffer函数,这个选项就不起作用。 - jpn: 最大処理並列度。シングルチャンネルの性能を向上させるために、これを増やします。ドライバがvaSyncBuffer関数を実装していない場合、このオプションは機能しません。 + jpn: + 最大処理並列度。シングルチャンネルの性能を向上させるために、これを増やします。ドライバがvaSyncBuffer関数を実装していない場合、このオプションは機能しません。 rus: Максимальная параллельность обработки. Увеличьте это значение для повышения производительности одного канала. Эта опция не работает, если драйвер не реализует функцию vaSyncBuffer. @@ -10339,16 +10342,94 @@ Single Pass Encoding: ukr: Однопрохідне кодування Conversion suites: eng: Conversion suites + deu: Konvertierungssuiten + fra: Suites de conversion + ita: Suite di conversione + spa: Salas de conversión + jpn: コンバージョン・スイート + rus: Конверсионные комплексы + por: Suites de conversão + swe: Sviter för konvertering + pol: Zestawy do konwersji + chs: 改装套件 + ukr: Номери для переобладнання + kor: 전환 제품군 + ron: Suite de conversie Encoders: eng: Encoders + deu: Drehgeber + fra: Encodeurs + ita: Encoder + spa: Codificadores + jpn: エンコーダ + rus: Энкодеры + por: Codificadores + swe: Kodare + pol: Kodery + chs: 编码器 + ukr: Енкодери + kor: 인코더 + ron: Codificatoare Supporting libraries: eng: Supporting libraries + deu: Unterstützung von Bibliotheken + fra: Soutenir les bibliothèques + ita: Biblioteche di supporto + spa: Apoyo a las bibliotecas + jpn: 図書館のサポート + rus: Поддержка библиотек + por: Apoio às bibliotecas + swe: Stöd till bibliotek + pol: Wspieranie bibliotek + chs: 支持图书馆 + ukr: Підтримка бібліотек + kor: 지원 라이브러리 + ron: Sprijinirea bibliotecilor Packaged with: eng: Packaged with + deu: Verpackt mit + fra: Emballé avec + ita: Confezionato con + spa: Envasado con + jpn: パッケージ + rus: В комплекте с + por: Embalado com + swe: Förpackad med + pol: W zestawie + chs: 包装包括 + ukr: У комплекті з + kor: 함께 제공되는 패키지 + ron: Ambalat cu Fast Seek: eng: Fast Seek + deu: Schnellsuche + fra: Recherche rapide + ita: Ricerca veloce + spa: Búsqueda rápida + jpn: ファスト・シーク + rus: Быстрый поиск + por: Procura rápida + swe: Snabb sökning + pol: Szybkie wyszukiwanie + chs: 快速查找 + ukr: '"Форсаж' + kor: 빠른 검색 + ron: Căutare rapidă Rotate: eng: Rotate + deu: Drehen Sie + fra: Rotation + ita: Ruotare + spa: Gire + jpn: 回転 + rus: Поворот + por: Rodar + swe: Rotera + pol: Obrót + chs: 旋转 + ukr: Поворот + kor: 회전 + ron: Rotiți Remove Hdr: deu: HDR entfernen eng: Remove HDR @@ -10366,17 +10447,151 @@ Remove Hdr: ron: Îndepărtați HDR Resolution Method: eng: Resolution Method + deu: Auflösung Methode + fra: Méthode de résolution + ita: Metodo di risoluzione + spa: Método de resolución + jpn: 解決方法 + rus: Метод разрешения + por: Método de resolução + swe: Metod för upplösning + pol: Metoda rozdzielczości + chs: 解决方法 + ukr: Метод розв'язання + kor: 해결 방법 + ron: Metoda de rezoluție Resolution Custom: eng: Resolution Custom + deu: Auflösung Benutzerdefiniert + fra: Résolution Personnalisée + ita: Risoluzione personalizzata + spa: Resolución Personalizada + jpn: 解像度カスタム + rus: Разрешение Пользовательское + por: Resolução Personalizada + swe: Resolution Anpassad + pol: Rozdzielczość niestandardowa + chs: 自定义分辨率 + ukr: Роздільна здатність Користувацька + kor: 해상도 사용자 지정 + ron: Rezoluție Personalizată Output Type: eng: Output Type + deu: Ausgangstyp + fra: Type de sortie + ita: Tipo di uscita + spa: Tipo de salida + jpn: 出力タイプ + rus: Тип выхода + por: Tipo de saída + swe: Typ av utgång + pol: Typ wyjścia + chs: 输出类型 + ukr: Тип виходу + kor: 출력 유형 + ron: Tip de ieșire Subtitle Language: eng: Subtitle Language + deu: Sprache des Untertitels + fra: Langue des sous-titres + ita: Lingua dei sottotitoli + spa: Idioma de los subtítulos + jpn: 字幕言語 + rus: Язык субтитров + por: Língua da legenda + swe: Språk för undertexter + pol: Język napisów + chs: 字幕语言 + ukr: Мова субтитрів + kor: 자막 언어 + ron: Subtitrare Limba Subtitle Select: eng: Subtitle Select + deu: Untertitel auswählen + fra: Sélection des sous-titres + ita: Selezione dei sottotitoli + spa: Selección de subtítulos + jpn: 字幕セレクト + rus: Выбор субтитров + por: Seleção de legendas + swe: Välj undertext + pol: Wybór napisów + chs: 字幕选择 + ukr: Вибір субтитрів + kor: 자막 선택 + ron: Selectare subtitrare Subtitle Select Preferred Language: eng: Subtitle Select Preferred Language + deu: Untertitel Bevorzugte Sprache auswählen + fra: Sous-titres Sélectionner la langue préférée + ita: Sottotitoli Selezionare la lingua preferita + spa: Subtítulos Seleccionar idioma preferido + jpn: 字幕 選択した言語 + rus: Субтитры Выбор предпочтительного языка + por: Legendas Selecionar o idioma preferido + swe: Undertexter Välj önskat språk + pol: Napisy Wybierz preferowany język + chs: 字幕 选择首选语言 + ukr: Субтитри Виберіть мову субтитрів + kor: 자막 기본 언어 선택 + ron: Subtitrare Selectați limba preferată Subtitle Automatic Burn In: eng: Subtitle Automatic Burn In + deu: Untertitel Automatisches Einbrennen + fra: Gravure automatique des sous-titres + ita: Sottotitolo Burn In automatico + spa: Subtitulación Quemado automático + jpn: 字幕自動焼き込み + rus: Автоматическое включение субтитров + por: Subtitle Automatic Burn In (Gravação automática de legendas) + swe: Automatisk inbränning av undertexter + pol: Automatyczne włączanie napisów + chs: 字幕自动烧入 + ukr: Субтитри Автоматичне вигоряння + kor: 자막 자동 번인 + ron: Subtitrare Automatic Burn In Subtitle Select First Matching: eng: Subtitle Select First Matching + deu: Untertitel Erste Übereinstimmung auswählen + fra: Sélection des sous-titres Première correspondance + ita: Selezione del sottotitolo Prima corrispondenza + spa: Subtítulo Seleccionar primera coincidencia + jpn: 字幕選択 最初のマッチング + rus: Выбор субтитров Первое совпадение + por: Seleção de legendas Primeira correspondência + swe: Undertext Välj första matchning + pol: Wybór napisów Pierwsze dopasowanie + chs: 字幕选择首次匹配 + ukr: Субтитри Виберіть перший збіг + kor: 자막 첫 번째 매칭 선택 + ron: Subtitrare Selectați prima potrivire +Passes: + eng: Passes + deu: Pässe + fra: Adopté + ita: Passaggi + spa: Pases + jpn: パス + rus: Проходит + por: Passa + swe: Godkänt + pol: Przełęcze + chs: 通过 + ukr: Перепустки + kor: 패스 + ron: Trece +Custom: + eng: Custom + deu: Benutzerdefiniert + fra: Sur mesure + ita: Personalizzato + spa: A medida + jpn: カスタム + rus: Пользовательский + por: Personalizado + swe: Anpassad + pol: Niestandardowe + chs: 定制 + ukr: Нестандартний + kor: 사용자 지정 + ron: Personalizat diff --git a/fastflix/encoders/avc_x264/command_builder.py b/fastflix/encoders/avc_x264/command_builder.py index 766305e8..07172bc5 100644 --- a/fastflix/encoders/avc_x264/command_builder.py +++ b/fastflix/encoders/avc_x264/command_builder.py @@ -20,18 +20,22 @@ def build(fastflix: FastFlix): pass_log_file = fastflix.current_video.work_path / f"pass_log_file_{secrets.token_hex(10)}" if settings.bitrate: - command_1 = ( - f"{beginning} -pass 1 " - f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra if settings.extra_both_passes else ""} -an -sn -dn {output_fps} -f mp4 {null}' - ) - command_2 = ( - f'{beginning} -pass 2 -passlogfile "{pass_log_file}" ' - f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} " - ) + ending - return [ - Command(command=command_1, name="First pass bitrate", exe="ffmpeg"), - Command(command=command_2, name="Second pass bitrate", exe="ffmpeg"), - ] + if settings.bitrate_passes == 2: + command_1 = ( + f"{beginning} -pass 1 " + f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra if settings.extra_both_passes else ""} -an -sn -dn {output_fps} -f mp4 {null}' + ) + command_2 = ( + f'{beginning} -pass 2 -passlogfile "{pass_log_file}" ' + f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} " + ) + ending + return [ + Command(command=command_1, name="First pass bitrate", exe="ffmpeg"), + Command(command=command_2, name="Second pass bitrate", exe="ffmpeg"), + ] + else: + command = f"{beginning} -b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} {ending}" + return [Command(command=command, name="Single pass bitrate", exe="ffmpeg")] elif settings.crf: command = f"{beginning} -crf:v {settings.crf} " f"-preset:v {settings.preset} {settings.extra} {ending}" diff --git a/fastflix/encoders/avc_x264/settings_panel.py b/fastflix/encoders/avc_x264/settings_panel.py index ec370353..6b7a44df 100644 --- a/fastflix/encoders/avc_x264/settings_panel.py +++ b/fastflix/encoders/avc_x264/settings_panel.py @@ -51,13 +51,10 @@ pix_fmts = [ "8-bit: yuv420p", "10-bit: yuv420p10le", - "12-bit: yuv420p12le", "8-bit 422: yuv422p", "8-bit 444: yuv444p", "10-bit 422: yuv422p10le", "10-bit 444: yuv444p10le", - "12-bit 422: yuv422p12le", - "12-bit 444: yuv444p12le", ] @@ -154,7 +151,7 @@ def init_pix_fmt(self): ) def init_modes(self): - return self._add_modes(recommended_bitrates, recommended_crfs, qp_name="crf") + return self._add_modes(recommended_bitrates, recommended_crfs, qp_name="crf", show_bitrate_passes=True) def mode_update(self): self.widgets.custom_crf.setDisabled(self.widgets.crf.currentText() != "Custom") @@ -181,6 +178,7 @@ def update_video_encoder_settings(self): extra=self.ffmpeg_extras, tune=tune if tune.lower() != "default" else None, extra_both_passes=self.widgets.extra_both_passes.isChecked(), + bitrate_passes=int(self.widgets.bitrate_passes.currentText()), ) encode_type, q_value = self.get_mode_settings() settings.crf = q_value if encode_type == "qp" else None diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py index 5bc669ca..1c94a457 100644 --- a/fastflix/encoders/common/setting_panel.py +++ b/fastflix/encoders/common/setting_panel.py @@ -332,7 +332,15 @@ def dhdr10_update(self): self.widgets.hdr10plus_metadata.setText(filename[0]) self.main.page_update() - def _add_modes(self, recommended_bitrates, recommended_qps, qp_name="crf", add_qp=True, disable_custom_qp=False): + def _add_modes( + self, + recommended_bitrates, + recommended_qps, + qp_name="crf", + add_qp=True, + disable_custom_qp=False, + show_bitrate_passes=False, + ): self.recommended_bitrates = recommended_bitrates self.recommended_qps = recommended_qps self.qp_name = qp_name @@ -352,6 +360,10 @@ def _add_modes(self, recommended_bitrates, recommended_qps, qp_name="crf", add_q self.widgets.bitrate = QtWidgets.QComboBox() # self.widgets.bitrate.setFixedWidth(250) self.widgets.bitrate.addItems(recommended_bitrates) + self.widgets.bitrate_passes = QtWidgets.QComboBox() + self.widgets.bitrate_passes.addItems(["1", "2"]) + self.widgets.bitrate_passes.setCurrentIndex(1) + self.widgets.bitrate_passes.currentIndexChanged.connect(lambda: self.mode_update()) config_opt = self.app.fastflix.config.encoder_opt(self.profile_name, "bitrate") custom_bitrate = False try: @@ -372,8 +384,11 @@ def _add_modes(self, recommended_bitrates, recommended_qps, qp_name="crf", add_q bitrate_box_layout.addWidget(self.bitrate_radio) bitrate_box_layout.addWidget(self.widgets.bitrate, 1) bitrate_box_layout.addStretch(1) + if show_bitrate_passes: + bitrate_box_layout.addWidget(QtWidgets.QLabel(t("Passes") + ":")) + bitrate_box_layout.addWidget(self.widgets.bitrate_passes) bitrate_box_layout.addStretch(1) - bitrate_box_layout.addWidget(QtWidgets.QLabel("Custom:")) + bitrate_box_layout.addWidget(QtWidgets.QLabel(t("Custom") + ":")) bitrate_box_layout.addWidget(self.widgets.custom_bitrate) bitrate_box_layout.addWidget(QtWidgets.QLabel("k")) diff --git a/fastflix/encoders/hevc_x265/command_builder.py b/fastflix/encoders/hevc_x265/command_builder.py index 62b67e06..660d1743 100644 --- a/fastflix/encoders/hevc_x265/command_builder.py +++ b/fastflix/encoders/hevc_x265/command_builder.py @@ -179,19 +179,23 @@ def get_x265_params(params=()): return '-x265-params "{}" '.format(":".join(all_params)) if all_params else "" if settings.bitrate: - command_1 = ( - f'{beginning} {get_x265_params(["pass=1", "no-slow-firstpass=1"])} ' - f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra if settings.extra_both_passes else ""} ' - f" -an -sn -dn {output_fps} -f mp4 {null}" - ) - command_2 = ( - f'{beginning} {get_x265_params(["pass=2"])} -passlogfile "{pass_log_file}" ' - f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} {ending}" - ) - return [ - Command(command=command_1, name="First pass bitrate", exe="ffmpeg"), - Command(command=command_2, name="Second pass bitrate", exe="ffmpeg"), - ] + if settings.bitrate_passes == 2: + command_1 = ( + f'{beginning} {get_x265_params(["pass=1", "no-slow-firstpass=1"])} ' + f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra if settings.extra_both_passes else ""} ' + f" -an -sn -dn {output_fps} -f mp4 {null}" + ) + command_2 = ( + f'{beginning} {get_x265_params(["pass=2"])} -passlogfile "{pass_log_file}" ' + f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} {ending}" + ) + return [ + Command(command=command_1, name="First pass bitrate", exe="ffmpeg"), + Command(command=command_2, name="Second pass bitrate", exe="ffmpeg"), + ] + else: + command = f"{beginning} {get_x265_params()} -b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} {ending}" + return [Command(command=command, name="Single pass bitrate", exe="ffmpeg")] elif settings.crf: command = ( diff --git a/fastflix/encoders/hevc_x265/settings_panel.py b/fastflix/encoders/hevc_x265/settings_panel.py index 7f06a50d..bf682666 100644 --- a/fastflix/encoders/hevc_x265/settings_panel.py +++ b/fastflix/encoders/hevc_x265/settings_panel.py @@ -563,7 +563,7 @@ def init_gop(self): ) def init_modes(self): - return self._add_modes(recommended_bitrates, recommended_crfs, qp_name="crf") + return self._add_modes(recommended_bitrates, recommended_crfs, qp_name="crf", show_bitrate_passes=True) def mode_update(self): self.widgets.custom_crf.setDisabled(self.widgets.crf.currentText() != "Custom") @@ -697,6 +697,7 @@ def update_video_encoder_settings(self): lossless=self.widgets.lossless.isChecked(), extra=self.ffmpeg_extras, extra_both_passes=self.widgets.extra_both_passes.isChecked(), + bitrate_passes=int(self.widgets.bitrate_passes.currentText()), # gop_size=int(self.widgets.gop_size.currentText()) if self.widgets.gop_size.currentIndex() > 0 else 0, ) diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index 6132ed11..42901662 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -72,6 +72,7 @@ class x265Settings(EncoderSettings): intra_smoothing: bool = True frame_threads: int = 0 # gop_size: int = 0 + bitrate_passes: int = 2 class VVCSettings(EncoderSettings): @@ -94,6 +95,7 @@ class x264Settings(EncoderSettings): pix_fmt: str = "yuv420p" crf: Optional[Union[int, float]] = 23 bitrate: Optional[str] = None + bitrate_passes: int = 2 class FFmpegNVENCSettings(EncoderSettings): diff --git a/fastflix/version.py b/fastflix/version.py index 86291754..b2c943c0 100644 --- a/fastflix/version.py +++ b/fastflix/version.py @@ -1,4 +1,4 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -__version__ = "5.5.8b0" +__version__ = "5.6.0" __author__ = "Chris Griffith" diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 7ac794cc..44a3683d 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1294,16 +1294,39 @@ def build_crop(self) -> Union[Crop, None]: return None try: assert crop.top >= 0, t("Top must be positive number") - assert crop.left >= 0, t("Left must be positive number") - assert crop.width > 0, t("Total video width must be greater than 0") assert crop.height > 0, t("Total video height must be greater than 0") - assert crop.width <= self.app.fastflix.current_video.width, t("Width must be smaller than video width") assert crop.height <= self.app.fastflix.current_video.height, t( "Height must be smaller than video height" ) except AssertionError as err: - error_message(f"{t('Invalid Crop')}: {err}") + logger.warning(f"{t('Invalid Crop')}: {err}") + self.widgets.crop.top.setStyleSheet("color: red") + self.widgets.crop.bottom.setStyleSheet("color: red") return None + try: + assert crop.left >= 0, t("Left must be positive number") + assert crop.width > 0, t("Total video width must be greater than 0") + + assert crop.width <= self.app.fastflix.current_video.width, t("Width must be smaller than video width") + + except AssertionError as err: + logger.warning(f"{t('Invalid Crop')}: {err}") + self.widgets.crop.left.setStyleSheet("color: red") + self.widgets.crop.right.setStyleSheet("color: red") + # error_message(f"{t('Invalid Crop')}: {err}") + return None + self.widgets.crop.left.setStyleSheet( + "color: black" if self.app.fastflix.config.theme != "dark" else "color: white" + ) + self.widgets.crop.right.setStyleSheet( + "color: black" if self.app.fastflix.config.theme != "dark" else "color: white" + ) + self.widgets.crop.top.setStyleSheet( + "color: black" if self.app.fastflix.config.theme != "dark" else "color: white" + ) + self.widgets.crop.bottom.setStyleSheet( + "color: black" if self.app.fastflix.config.theme != "dark" else "color: white" + ) return crop def disable_all(self): From c7f9c2322c2f8e60f30b6409ded27aca000c2927 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Thu, 8 Feb 2024 15:51:26 -0600 Subject: [PATCH 03/15] Initial work for hdr10plus injection --- fastflix/widgets/container.py | 11 ++ fastflix/widgets/windows/hdr10plus_inject.py | 134 +++++++++++++++++++ tests/test_application.py | 18 +++ 3 files changed, 163 insertions(+) create mode 100644 fastflix/widgets/windows/hdr10plus_inject.py diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py index 915f34d7..fb2bbba5 100644 --- a/fastflix/widgets/container.py +++ b/fastflix/widgets/container.py @@ -29,6 +29,7 @@ from fastflix.widgets.settings import Settings from fastflix.widgets.windows.concat import ConcatWindow from fastflix.widgets.windows.multiple_files import MultipleFilesWindow +from fastflix.widgets.windows.hdr10plus_inject import HDR10PlusInjectWindow logger = logging.getLogger("fastflix") @@ -213,6 +214,12 @@ def init_menu(self): concat_action.triggered.connect(self.show_concat) tools_menu.addAction(concat_action) + hdr10p_inject_action = QAction( + QtGui.QIcon(get_icon("onyx-queue", self.app.fastflix.config.theme)), t("HDR10+ Inject"), self + ) + hdr10p_inject_action.triggered.connect(self.show_hdr10p_inject) + tools_menu.addAction(hdr10p_inject_action) + wiki_action = QAction(self.si(QtWidgets.QStyle.SP_FileDialogInfoView), t("FastFlix Wiki"), self) wiki_action.triggered.connect(self.show_wiki) @@ -266,6 +273,10 @@ def show_concat(self): self.concat = ConcatWindow(app=self.app, main=self.main) self.concat.show() + def show_hdr10p_inject(self): + self.hdr10p_inject = HDR10PlusInjectWindow(app=self.app, main=self.main) + self.hdr10p_inject.show() + def show_about(self): self.about = About(app=self.app) self.about.show() diff --git a/fastflix/widgets/windows/hdr10plus_inject.py b/fastflix/widgets/windows/hdr10plus_inject.py new file mode 100644 index 00000000..40a3f4b1 --- /dev/null +++ b/fastflix/widgets/windows/hdr10plus_inject.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +from pathlib import Path +import os +import logging +import secrets + +from PySide6 import QtWidgets, QtGui, QtCore +from PySide6.QtWidgets import QAbstractItemView + +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 +from fastflix.resources import group_box_style, get_icon + +logger = logging.getLogger("fastflix") + + +class HDR10PlusInjectWindow(QtWidgets.QWidget): + def __init__(self, app, main, items=None): + super().__init__(None) + self.app = app + self.main = main + self.selected_stream = None + + self.movie_file = QtWidgets.QLineEdit() + self.movie_file.setEnabled(False) + self.movie_file.setFixedWidth(400) + self.movie_file_button = QtWidgets.QPushButton( + icon=QtGui.QIcon(get_icon("onyx-output", self.app.fastflix.config.theme)) + ) + self.movie_file_button.clicked.connect(self.movie_open) + + self.hdr10p_file = QtWidgets.QLineEdit() + self.hdr10p_file.setEnabled(False) + self.hdr10p_file_button = QtWidgets.QPushButton( + icon=QtGui.QIcon(get_icon("onyx-output", self.app.fastflix.config.theme)) + ) + self.hdr10p_file_button.clicked.connect(self.hdr10p_open) + + self.output_file = QtWidgets.QLineEdit() + self.output_file.setFixedWidth(400) + self.output_file_button = QtWidgets.QPushButton( + icon=QtGui.QIcon(get_icon("onyx-output", self.app.fastflix.config.theme)) + ) + self.output_file_button.clicked.connect(self.set_output_file) + self.output_file.textChanged.connect(self.prep_command) + + line_1 = QtWidgets.QHBoxLayout() + line_1.addWidget(QtWidgets.QLabel("Movie File")) + line_1.addWidget(self.movie_file) + line_1.addWidget(self.movie_file_button) + + line_3 = QtWidgets.QHBoxLayout() + line_3.addWidget(QtWidgets.QLabel("HDR10+ File")) + line_3.addWidget(self.hdr10p_file) + line_3.addWidget(self.hdr10p_file_button) + + self.info_bubble = QtWidgets.QLabel("") + self.command_bubble = QtWidgets.QLabel("") + self.command_bubble.setFixedWidth(400) + self.command_bubble.setWordWrap(True) + self.command_bubble.setFixedHeight(400) + + layout = QtWidgets.QVBoxLayout() + + output_lin = QtWidgets.QHBoxLayout() + output_lin.addWidget(QtWidgets.QLabel("Output File")) + output_lin.addWidget(self.output_file) + output_lin.addWidget(self.output_file_button) + + layout.addLayout(line_1) + layout.addWidget(self.info_bubble) + layout.addLayout(line_3) + layout.addLayout(output_lin) + layout.addWidget(self.command_bubble) + self.setLayout(layout) + + def movie_open(self): + self.selected_stream = None + self.movie_file.setText("") + movie_name = QtWidgets.QFileDialog.getOpenFileName(self) + if not movie_name or not movie_name[0]: + return + try: + results = probe(self.app, movie_name[0]) + except Exception as err: + error_message(f"Invalid file: {err}") + return + for result in results["streams"]: + if result["codec_type"] == "video": + if result["codec_name"] == "hevc": + self.selected_stream = result + break + if not self.selected_stream: + error_message("No HEVC video stream found") + return + self.info_bubble.setText(f"Selected stream index: {self.selected_stream['index']}") + self.movie_file.setText(movie_name[0]) + self.prep_command() + + def hdr10p_open(self): + hdr10p_file = QtWidgets.QFileDialog.getOpenFileName(self) + if not hdr10p_file or not hdr10p_file[0]: + return + self.hdr10p_file.setText(hdr10p_file[0]) + self.prep_command() + + def set_output_file(self): + filename = QtWidgets.QFileDialog.getSaveFileName( + self, + caption="Save Video As", + # dir=str(Path(*self.generate_output_filename)) + f"{self.widgets.output_type_combo.currentText()}", + # filter=f"Save File (*.{extension})", + ) + if filename and filename[0]: + self.output_file.setText(filename[0]) + self.prep_command() + + def prep_command(self): + print("called prep") + if not self.movie_file.text() or not self.hdr10p_file.text() or not self.output_file.text(): + print("Nope", "1", self.movie_file.text(), "2", self.hdr10p_file.text(), "3", self.output_file.text()) + return + + command = ( + f'{self.app.fastflix.config.ffmpeg} -loglevel panic -i "{self.movie_file.text()}" ' + f'-map 0:{self.selected_stream["index"]} -c:v copy -vbsf hevc_mp4toannexb -f hevc - | ' + f"{self.app.fastflix.config.hdr10plus_parser} inject -i - -j {self.hdr10p_file.text()} -o - | " + f'{self.app.fastflix.config.ffmpeg} -loglevel panic -i - -i {self.movie_file.text()} -map 0:0 -c:0 copy -map 1:a -map 1:s -map 1:d -c:1 copy "{self.output_file.text()}"' + ) + print(command) + + self.command_bubble.setText(command) diff --git a/tests/test_application.py b/tests/test_application.py index 56018a23..9a29c75e 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,9 +1,15 @@ # -*- coding: utf-8 -*- +from unittest.mock import MagicMock + from box import Box from fastflix.application import init_encoders, create_app from fastflix.models.config import Config +# from fastflix.widgets.container import Container + +# import pytest + fake_app = Box(default_box=True) fake_app.fastflix.config = Config() @@ -13,6 +19,18 @@ def test_init_encoders(): assert "encoders" in fake_app.fastflix +# def test_app(qtbot): +# # app = create_app(enable_scaling=False) +# container = Container(MagicMock()) +# qtbot.addWidget(container) +# from pytestqt.qt_compat import qt_api +# +# qtbot.mouseClick(container.menuBar(), qt_api.QtCore.Qt.MouseButton.LeftButton) +# +# assert container.menuBar().actions()[0].text() == "File" +# #assert widget.greet_label.text() == "Hello!" + + # def test_get_ffmpeg_version(): # ffmpeg_configuration(fake_app, fake_app.fastflix.config) # assert getattr(fake_app.fastflix, "ffmpeg_version") From 88b44f071c62cb1ea5a8bea9073aeec1a18830e1 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Thu, 8 Feb 2024 22:13:41 -0600 Subject: [PATCH 04/15] Adding improvements --- fastflix/encoders/copy/command_builder.py | 2 ++ fastflix/flix.py | 21 ++++++++++++++----- fastflix/widgets/main.py | 1 + fastflix/widgets/progress_bar.py | 4 +++- fastflix/widgets/video_options.py | 1 + fastflix/widgets/windows/hdr10plus_inject.py | 22 ++++++++++++++++---- 6 files changed, 41 insertions(+), 10 deletions(-) diff --git a/fastflix/encoders/copy/command_builder.py b/fastflix/encoders/copy/command_builder.py index b7379653..54d25212 100644 --- a/fastflix/encoders/copy/command_builder.py +++ b/fastflix/encoders/copy/command_builder.py @@ -16,6 +16,8 @@ def build(fastflix: FastFlix): rotation = abs(int(fastflix.current_video.current_video_stream.side_data_list[0].rotation)) rot = "" + # if fastflix.current_video.video_settings.rotate != 0: + # rot = f"-display_rotation:s:v {rotation + (fastflix.current_video.video_settings.rotate * 90)}" if fastflix.current_video.video_settings.output_path.name.lower().endswith("mp4"): rot = f"-metadata:s:v rotate={rotation + (fastflix.current_video.video_settings.rotate * 90)}" diff --git a/fastflix/flix.py b/fastflix/flix.py index 2b12b875..383681e6 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -23,6 +23,8 @@ logger = logging.getLogger("fastflix") +HDR10_parser_version = None + ffmpeg_valid_color_primaries = [ "bt709", "bt470m", @@ -558,16 +560,25 @@ def parse_hdr_details(app: FastFlixApp, **_): ) +def get_hdr10_parser_version(config: Config) -> LooseVersion: + global HDR10_parser_version + if HDR10_parser_version: + return HDR10_parser_version + 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 {str(HDR10_parser_version).strip()}") + return HDR10_parser_version + + def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): if not config.hdr10plus_parser or not config.hdr10plus_parser.exists(): return hdr10plus_streams = [] - 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 {str(hdr10_parser_version).strip()}") + parser_version = get_hdr10_parser_version(config) for stream in app.fastflix.current_video.streams.video: logger.debug(f"Checking for hdr10+ in stream {stream.index}") @@ -595,7 +606,7 @@ def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): ) hdr10_parser_command = [str(config.hdr10plus_parser), "--verify", "-"] - if hdr10_parser_version >= LooseVersion("1.0.0"): + if parser_version >= LooseVersion("1.0.0"): hdr10_parser_command.insert(-1, "extract") process_two = Popen( diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 44a3683d..e5b2ea99 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1558,6 +1558,7 @@ def update_video_info(self, hide_progress=False): self.widgets.deinterlace.setChecked(self.app.fastflix.current_video.video_settings.deinterlace) + logger.info("Updating video info") self.video_options.new_source() self.enable_all() # self.widgets.convert_button.setDisabled(False) diff --git a/fastflix/widgets/progress_bar.py b/fastflix/widgets/progress_bar.py index ea13548c..edf55633 100644 --- a/fastflix/widgets/progress_bar.py +++ b/fastflix/widgets/progress_bar.py @@ -7,6 +7,7 @@ from PySide6 import QtCore, QtWidgets from fastflix.language import t +from fastflix.models.fastflix_app import FastFlixApp logger = logging.getLogger("fastflix") @@ -24,7 +25,7 @@ class ProgressBar(QtWidgets.QFrame): def __init__( self, - app: QtWidgets.QApplication, + app: FastFlixApp, tasks: list[Task], signal_task: bool = False, auto_run: bool = True, @@ -86,6 +87,7 @@ def run(self): else: for i, task in enumerate(self.tasks, start=1): + logger.info(f"Running task {task.name}") self.status.setText(task.name) self.app.processEvents() if self.app.fastflix.shutting_down: diff --git a/fastflix/widgets/video_options.py b/fastflix/widgets/video_options.py index f91308a6..938ef73a 100644 --- a/fastflix/widgets/video_options.py +++ b/fastflix/widgets/video_options.py @@ -192,6 +192,7 @@ def new_source(self): self.current_settings.new_source() self.queue.new_source() self.advanced.new_source() + # TODO disable on loading from directory self.info.reset() self.debug.reset() diff --git a/fastflix/widgets/windows/hdr10plus_inject.py b/fastflix/widgets/windows/hdr10plus_inject.py index 40a3f4b1..39772e5f 100644 --- a/fastflix/widgets/windows/hdr10plus_inject.py +++ b/fastflix/widgets/windows/hdr10plus_inject.py @@ -3,6 +3,7 @@ import os import logging import secrets +from subprocess import run, PIPE from PySide6 import QtWidgets, QtGui, QtCore from PySide6.QtWidgets import QAbstractItemView @@ -57,10 +58,10 @@ def __init__(self, app, main, items=None): line_3.addWidget(self.hdr10p_file_button) self.info_bubble = QtWidgets.QLabel("") - self.command_bubble = QtWidgets.QLabel("") + self.command_bubble = QtWidgets.QLineEdit("") self.command_bubble.setFixedWidth(400) - self.command_bubble.setWordWrap(True) - self.command_bubble.setFixedHeight(400) + # self.command_bubble.setWordWrap(True) + # self.command_bubble.setFixedHeight(400) layout = QtWidgets.QVBoxLayout() @@ -69,11 +70,20 @@ def __init__(self, app, main, items=None): output_lin.addWidget(self.output_file) output_lin.addWidget(self.output_file_button) + bottom_line = QtWidgets.QHBoxLayout() + cancel = QtWidgets.QPushButton("Cancel") + cancel.clicked.connect(self.hide) + bottom_line.addWidget(cancel) + start = QtWidgets.QPushButton("Start") + start.clicked.connect(self.start) + bottom_line.addWidget(start) + layout.addLayout(line_1) layout.addWidget(self.info_bubble) layout.addLayout(line_3) layout.addLayout(output_lin) layout.addWidget(self.command_bubble) + layout.addLayout(bottom_line) self.setLayout(layout) def movie_open(self): @@ -129,6 +139,10 @@ def prep_command(self): f"{self.app.fastflix.config.hdr10plus_parser} inject -i - -j {self.hdr10p_file.text()} -o - | " f'{self.app.fastflix.config.ffmpeg} -loglevel panic -i - -i {self.movie_file.text()} -map 0:0 -c:0 copy -map 1:a -map 1:s -map 1:d -c:1 copy "{self.output_file.text()}"' ) - print(command) + print(command) self.command_bubble.setText(command) + + def start(self): + run(self.command_bubble.text(), shell=True) + error_message("Done") From 36ad9dbb98d8cbc83b633aca5fc424f50442f32b Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Fri, 23 Feb 2024 17:58:42 -0600 Subject: [PATCH 05/15] * Fixing #543 systems with more than one opencl device would break thumbnails and some encodings (thanks to swadomlic) * Fixing #505 (maybe) trying new methods to clean file paths for subtitles (thanks to Maddie Davis) --- .github/workflows/build.yaml | 4 ++-- .github/workflows/pythonpublish.yml | 2 +- .github/workflows/test.yaml | 4 ++-- CHANGES | 5 +++++ LICENSE | 2 +- README.md | 2 +- fastflix/encoders/common/helpers.py | 2 +- fastflix/flix.py | 4 ++-- fastflix/shared.py | 16 +++++++++++++++- fastflix/widgets/container.py | 19 ++++++++++--------- pyproject.toml | 6 +++--- 11 files changed, 43 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e1cc29b5..f48cb765 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" - name: Gather build version (*nix) run: | @@ -74,7 +74,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" - name: Gather build version shell: powershell diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index efad57e2..de732a22 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: '3.12' - name: Install Dependencies run: | diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6052c1ef..48ff4010 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-python@v3 with: - python-version: "3.11" + python-version: "3.12" - run: pip install black==23.7.0 - run: python -m black --check . @@ -29,7 +29,7 @@ jobs: - uses: actions/setup-python@v3 with: - python-version: "3.11" + python-version: "3.12" - name: Install PySide6 requirements run: | diff --git a/CHANGES b/CHANGES index ade0d083..6cb94ff0 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,10 @@ # Changelog +## Version 5.6.1 + +* Fixing #543 systems with more than one opencl device would break thumbnails and some encodings (thanks to swadomlic) +* Fixing #505 (maybe) trying new methods to clean file paths for subtitles (thanks to Maddie Davis) + ## Version 5.6.0 * Adding Passes option for bitrate mode in x265 and x264 (thanks to Chriss) diff --git a/LICENSE b/LICENSE index 9e804e90..5f8d3b14 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2019-2023 Chris Griffith +Copyright (c) 2019-2024 Chris Griffith Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index bebeb99c..3a1bd272 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ Special thanks to [leonardyan](https://github.com/leonardyan) for numerous Chine # License -Copyright (C) 2019-2023 Chris Griffith +Copyright (C) 2019-2024 Chris Griffith The code itself is licensed under the MIT which you can read in the `LICENSE` file.
Read more about the release licensing in the [docs](docs/README.md) folder.
diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py index 5abe48ed..2dd30b90 100644 --- a/fastflix/encoders/common/helpers.py +++ b/fastflix/encoders/common/helpers.py @@ -77,7 +77,7 @@ def generate_ffmpeg_start( [ f'"{ffmpeg}"', start_extra, - ("-init_hw_device opencl=ocl -filter_hw_device ocl " if enable_opencl and remove_hdr else ""), + ("-init_hw_device opencl:0.0=ocl -filter_hw_device ocl " if enable_opencl and remove_hdr else ""), "-y", time_one, incoming_fps, diff --git a/fastflix/flix.py b/fastflix/flix.py index 383681e6..09379a2a 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -331,7 +331,7 @@ def generate_thumbnail_command( # Hardware acceleration with OpenCL if enable_opencl: - command += ["-init_hw_device", "opencl=ocl", "-filter_hw_device", "ocl"] + command += ["-init_hw_device", "opencl:0.0=ocl", "-filter_hw_device", "ocl"] command += shlex.split(filters) @@ -462,7 +462,7 @@ def ffmpeg_audio_encoders(app, config: Config) -> List: def ffmpeg_opencl_support(app, config: Config) -> bool: - cmd = execute([f"{config.ffmpeg}", "-hide_banner", "-log_level", "error", "-init_hw_device", "opencl", "-h"]) + cmd = execute([f"{config.ffmpeg}", "-hide_banner", "-log_level", "error", "-init_hw_device", "opencl:0.0", "-h"]) app.fastflix.opencl_support = cmd.returncode == 0 return app.fastflix.opencl_support diff --git a/fastflix/shared.py b/fastflix/shared.py index d1861364..b9b61906 100644 --- a/fastflix/shared.py +++ b/fastflix/shared.py @@ -316,7 +316,21 @@ def clean_file_string(source): def quoted_path(source): - return str(source).strip().replace("\\", "\\\\").replace(":", "\\:").replace("'", "'\\\\\\''") + cleaned_string = ( + str(source) + .strip() + .replace("\\", "\\\\") + .replace(":", "\\:") + .replace("'", "'\\\\\\''") + .replace("\r\n", "") + .replace("\n", "") + .replace("\r", "") + ) + if " " in cleaned_string[0:4]: + logger.warning(f"Unexpected space at start of quoted path, attempting to fix: {cleaned_string}") + cleaned_string = cleaned_string[0:4].replace(" ", "") + cleaned_string[4:] + logger.warning(f"New path set to: {cleaned_string}") + return cleaned_string def sanitize(source): diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py index fb2bbba5..9198701a 100644 --- a/fastflix/widgets/container.py +++ b/fastflix/widgets/container.py @@ -29,7 +29,8 @@ from fastflix.widgets.settings import Settings from fastflix.widgets.windows.concat import ConcatWindow from fastflix.widgets.windows.multiple_files import MultipleFilesWindow -from fastflix.widgets.windows.hdr10plus_inject import HDR10PlusInjectWindow + +# from fastflix.widgets.windows.hdr10plus_inject import HDR10PlusInjectWindow logger = logging.getLogger("fastflix") @@ -214,11 +215,11 @@ def init_menu(self): concat_action.triggered.connect(self.show_concat) tools_menu.addAction(concat_action) - hdr10p_inject_action = QAction( - QtGui.QIcon(get_icon("onyx-queue", self.app.fastflix.config.theme)), t("HDR10+ Inject"), self - ) - hdr10p_inject_action.triggered.connect(self.show_hdr10p_inject) - tools_menu.addAction(hdr10p_inject_action) + # hdr10p_inject_action = QAction( + # QtGui.QIcon(get_icon("onyx-queue", self.app.fastflix.config.theme)), t("HDR10+ Inject"), self + # ) + # hdr10p_inject_action.triggered.connect(self.show_hdr10p_inject) + # tools_menu.addAction(hdr10p_inject_action) wiki_action = QAction(self.si(QtWidgets.QStyle.SP_FileDialogInfoView), t("FastFlix Wiki"), self) wiki_action.triggered.connect(self.show_wiki) @@ -273,9 +274,9 @@ def show_concat(self): self.concat = ConcatWindow(app=self.app, main=self.main) self.concat.show() - def show_hdr10p_inject(self): - self.hdr10p_inject = HDR10PlusInjectWindow(app=self.app, main=self.main) - self.hdr10p_inject.show() + # def show_hdr10p_inject(self): + # self.hdr10p_inject = HDR10PlusInjectWindow(app=self.app, main=self.main) + # self.hdr10p_inject.show() def show_about(self): self.about = About(app=self.app) diff --git a/pyproject.toml b/pyproject.toml index ebf122d7..93defac4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 120 -target-version = ['py311'] +target-version = ['py312'] exclude = ''' /( \.eggs @@ -21,7 +21,7 @@ authors = [{ name = "Chris Griffith", email = "chris@cdgriffith.com" }] readme = "README.md" #url = "https://fastflix.org" #download_url = "https://github.com/cdgriffith/FastFlix/releases" -requires-python = ">=3.11,<3.12" +requires-python = ">=3.12" dynamic = ["version"] dependencies = [ "appdirs~=1.4", @@ -33,7 +33,7 @@ dependencies = [ "pathvalidate>=2.4,<3.0", "psutil>=5.9,<6.0", "pydantic>=1.9,<2.0", - "pyside6>=6.4.2,<7.0", + "pyside6>=6.4.2", "python-box[all]>=6.0,<7.0", "requests>=2.28,<3.0", "reusables>=0.9.6,<0.10.0", From 09b698912e9e1c3fd8a9370ae223a4eea934b215 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 16 Apr 2024 19:43:28 -0500 Subject: [PATCH 06/15] * Adding new audio encoding panel * Adding support for audio quality targeting instead of bitrate * Fixing that audio and subtitles would be reset on change of encoder --- CHANGES | 5 +- fastflix/data/languages.yaml | 120 ++++ fastflix/encoders/common/audio.py | 5 +- fastflix/encoders/common/encc_helpers.py | 9 +- fastflix/encoders/common/helpers.py | 6 +- fastflix/encoders/common/subtitles.py | 2 + .../encoders/nvencc_av1/command_builder.py | 4 +- .../encoders/nvencc_avc/command_builder.py | 4 +- .../encoders/nvencc_hevc/command_builder.py | 4 +- .../encoders/qsvencc_av1/command_builder.py | 4 +- .../encoders/qsvencc_avc/command_builder.py | 4 +- .../encoders/qsvencc_hevc/command_builder.py | 4 +- .../encoders/vceencc_av1/command_builder.py | 4 +- .../encoders/vceencc_avc/command_builder.py | 4 +- .../encoders/vceencc_hevc/command_builder.py | 4 +- fastflix/ff_queue.py | 14 +- fastflix/flix.py | 17 +- fastflix/models/config.py | 9 +- fastflix/models/encode.py | 5 +- fastflix/models/video.py | 10 +- fastflix/version.py | 2 +- fastflix/widgets/background_tasks.py | 6 +- fastflix/widgets/main.py | 9 - fastflix/widgets/panels/abstract_list.py | 4 +- fastflix/widgets/panels/audio_panel.py | 534 ++++++------------ fastflix/widgets/panels/cover_panel.py | 14 +- fastflix/widgets/panels/queue_panel.py | 14 +- fastflix/widgets/panels/subtitle_panel.py | 163 +++--- fastflix/widgets/video_options.py | 17 +- fastflix/widgets/windows/audio_conversion.py | 192 +++++++ fastflix/widgets/windows/disposition.py | 42 +- pyproject.toml | 1 + tests/test_version_check.py | 9 +- velocemente/__init__.py | 0 34 files changed, 713 insertions(+), 532 deletions(-) create mode 100644 fastflix/widgets/windows/audio_conversion.py create mode 100644 velocemente/__init__.py diff --git a/CHANGES b/CHANGES index 6cb94ff0..14a9d1de 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,10 @@ # Changelog -## Version 5.6.1 +## Version 5.7.0 +* Adding new audio encoding panel +* Adding support for audio quality targeting instead of bitrate +* Fixing that audio and subtitles would be reset on change of encoder * Fixing #543 systems with more than one opencl device would break thumbnails and some encodings (thanks to swadomlic) * Fixing #505 (maybe) trying new methods to clean file paths for subtitles (thanks to Maddie Davis) diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index 675f91cf..0a8ce704 100644 --- a/fastflix/data/languages.yaml +++ b/fastflix/data/languages.yaml @@ -10595,3 +10595,123 @@ Custom: ukr: Нестандартний kor: 사용자 지정 ron: Personalizat +Codec: + eng: Codec + deu: Codec + fra: Codec + ita: Codec + spa: Códec + jpn: コーデック + rus: Кодек + por: Codec + swe: Kodning + pol: Kodek + chs: 编解码器 + ukr: Кодек + kor: 코덱 + ron: Codec +Near Lossless: + eng: Near Lossless + deu: Fast verlustfrei + fra: Presque sans perte + ita: Quasi senza perdita + spa: Casi sin pérdidas + jpn: ニア・ロスレス + rus: Почти без потерь + por: Quase sem perdas + swe: Nära förlustfri + pol: Near Lossless + chs: 近乎无损 + ukr: Майже без втрат + kor: 거의 무손실 + ron: Aproape fără pierderi +High Quality: + eng: High Quality + deu: Hohe Qualität + fra: Haute qualité + ita: Alta qualità + spa: Alta calidad + jpn: 高品質 + rus: Высокое качество + por: Alta qualidade + swe: Hög kvalitet + pol: Wysoka jakość + chs: 高质量 + ukr: Висока якість + kor: 고품질 + ron: De înaltă calitate +Medium Quality: + eng: Medium Quality + deu: Mittlere Qualität + fra: Qualité moyenne + ita: Qualità media + spa: Calidad media + jpn: ミディアム・クオリティ + rus: Среднее качество + por: Qualidade média + swe: Medelhög kvalitet + pol: Średnia jakość + chs: 中等质量 + ukr: Середня якість + kor: 중간 품질 + ron: Calitate medie +Low Quality: + eng: Low Quality + deu: Geringe Qualität + fra: Faible qualité + ita: Bassa qualità + spa: Baja calidad + jpn: 低品質 + rus: Низкое качество + por: Baixa qualidade + swe: Låg kvalitet + pol: Niska jakość + chs: 低质量 + ukr: Низька якість + kor: 낮은 품질 + ron: Calitate scăzută +Custom Bitrate: + eng: Custom Bitrate + deu: Benutzerdefinierte Bitrate + fra: Bitrate personnalisé + ita: Bitrate personalizzato + spa: Velocidad de bits personalizada + jpn: カスタム・ビットレート + rus: Пользовательский битрейт + por: Taxa de bits personalizada + swe: Anpassad bitrate + pol: Niestandardowa szybkość transmisji + chs: 自定义比特率 + ukr: Користувацький бітрейт + kor: 사용자 지정 비트레이트 + ron: Bitrate personalizat +Audio Quality: + eng: Audio Quality + deu: Audio-Qualität + fra: Qualité audio + ita: Qualità audio + spa: Calidad de audio + jpn: オーディオ品質 + rus: Качество звука + por: Qualidade áudio + swe: Ljudkvalitet + pol: Jakość dźwięku + chs: 音频质量 + ukr: Якість звуку + kor: 오디오 품질 + ron: Calitatea audio +Channel Layout: + eng: Channel Layout + deu: Kanal-Layout + fra: Disposition des canaux + ita: Layout del canale + spa: Disposición de los canales + jpn: チャンネルレイアウト + rus: Расположение каналов + por: Disposição dos canais + swe: Kanalens layout + pol: Układ kanałów + chs: 通道布局 + ukr: Розташування каналів + kor: 채널 레이아웃 + ron: Canal Layout diff --git a/fastflix/encoders/common/audio.py b/fastflix/encoders/common/audio.py index 4b924f3d..207bb12d 100644 --- a/fastflix/encoders/common/audio.py +++ b/fastflix/encoders/common/audio.py @@ -30,6 +30,8 @@ def build_audio(audio_tracks, audio_file_index=0): command_list = [] for track in audio_tracks: + if not track.enabled: + continue command_list.append( f"-map {audio_file_index}:{track.index} " f'-metadata:s:{track.outdex} title="{track.title}" ' @@ -47,7 +49,8 @@ def build_audio(audio_tracks, audio_file_index=0): ) bitrate = "" if track.conversion_codec not in lossless: - bitrate = f"-b:{track.outdex} {track.conversion_bitrate} " + channel_layout = f'-filter:{track.outdex} aformat=channel_layouts="{track.raw_info.channel_layout}"' + bitrate = f"-b:{track.outdex} {track.conversion_bitrate} {channel_layout}" command_list.append(f"-c:{track.outdex} {track.conversion_codec} {bitrate} {downmix}") if getattr(track, "dispositions", None): diff --git a/fastflix/encoders/common/encc_helpers.py b/fastflix/encoders/common/encc_helpers.py index 8efcfbbf..05f1f84b 100644 --- a/fastflix/encoders/common/encc_helpers.py +++ b/fastflix/encoders/common/encc_helpers.py @@ -86,6 +86,8 @@ def build_audio(audio_tracks: list[AudioTrack], audio_streams): stream_ids = get_stream_pos(audio_streams) for track in sorted(audio_tracks, key=lambda x: x.outdex): + if not track.enabled: + continue if track.index in track_ids: logger.warning("*EncC does not support copy and duplicate of audio tracks!") track_ids.add(track.index) @@ -98,7 +100,10 @@ def build_audio(audio_tracks: list[AudioTrack], audio_streams): downmix = f"--audio-stream {audio_id}?:{track.downmix}" if track.downmix else "" bitrate = "" if track.conversion_codec not in lossless: - bitrate = f"--audio-bitrate {audio_id}?{track.conversion_bitrate.rstrip('k')} " + if track.conversion_bitrate: + bitrate = f"--audio-bitrate {audio_id}?{track.conversion_bitrate} " + else: + bitrate = f"--audio-quality {audio_id}?{track.conversion_aq} " command_list.append( f"{downmix} --audio-codec {audio_id}?{track.conversion_codec} {bitrate} " f"--audio-metadata {audio_id}?clear" @@ -130,6 +135,8 @@ def build_subtitle(subtitle_tracks: list[SubtitleTrack], subtitle_streams, video scale = ",scale=2.0" if video_height > 1800 else "" for track in sorted(subtitle_tracks, key=lambda x: x.outdex): + if not track.enabled: + continue sub_id = stream_ids[track.index] if track.burn_in: command_list.append(f"--vpp-subburn track={sub_id}{scale}") diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py index 2dd30b90..637fad8e 100644 --- a/fastflix/encoders/common/helpers.py +++ b/fastflix/encoders/common/helpers.py @@ -250,18 +250,18 @@ def generate_all( ) -> Tuple[str, str, str]: settings = fastflix.current_video.video_settings.video_encoder_settings - audio = build_audio(fastflix.current_video.video_settings.audio_tracks) if audio else "" + audio = build_audio(fastflix.current_video.audio_tracks) if audio else "" subtitles, burn_in_track, burn_in_type = "", None, None if subs: - subtitles, burn_in_track, burn_in_type = build_subtitle(fastflix.current_video.video_settings.subtitle_tracks) + subtitles, burn_in_track, burn_in_type = build_subtitle(fastflix.current_video.subtitle_tracks) if burn_in_type == "text": for i, x in enumerate(fastflix.current_video.streams["subtitle"]): if x["index"] == burn_in_track: burn_in_track = i break - attachments = build_attachments(fastflix.current_video.video_settings.attachment_tracks) + attachments = build_attachments(fastflix.current_video.attachment_tracks) enable_opencl = fastflix.opencl_support if "enable_opencl" in filters_extra: diff --git a/fastflix/encoders/common/subtitles.py b/fastflix/encoders/common/subtitles.py index 6726b063..1103f545 100644 --- a/fastflix/encoders/common/subtitles.py +++ b/fastflix/encoders/common/subtitles.py @@ -14,6 +14,8 @@ def build_subtitle( burn_in_type = None subs_enabled = False for track in subtitle_tracks: + if not track.enabled: + continue if track.burn_in: burn_in_track = track.index burn_in_type = track.subtitle_type diff --git a/fastflix/encoders/nvencc_av1/command_builder.py b/fastflix/encoders/nvencc_av1/command_builder.py index 522a537a..011054b6 100644 --- a/fastflix/encoders/nvencc_av1/command_builder.py +++ b/fastflix/encoders/nvencc_av1/command_builder.py @@ -162,8 +162,8 @@ def build(fastflix: FastFlix): (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), remove_hdr, "--psnr --ssim" if settings.metrics else "", - build_audio(video.video_settings.audio_tracks, video.streams.audio), - build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height), + build_audio(video.audio_tracks, video.streams.audio), + build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height), settings.extra, "-o", f'"{clean_file_string(video.video_settings.output_path)}"', diff --git a/fastflix/encoders/nvencc_avc/command_builder.py b/fastflix/encoders/nvencc_avc/command_builder.py index 80d04a27..2ccf23f6 100644 --- a/fastflix/encoders/nvencc_avc/command_builder.py +++ b/fastflix/encoders/nvencc_avc/command_builder.py @@ -131,8 +131,8 @@ def build(fastflix: FastFlix): (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), remove_hdr, "--psnr --ssim" if settings.metrics else "", - build_audio(video.video_settings.audio_tracks, video.streams.audio), - build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height), + build_audio(video.audio_tracks, video.streams.audio), + build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height), settings.extra, "-o", f'"{clean_file_string(video.video_settings.output_path)}"', diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py index 1484dc68..c72c96f8 100644 --- a/fastflix/encoders/nvencc_hevc/command_builder.py +++ b/fastflix/encoders/nvencc_hevc/command_builder.py @@ -162,8 +162,8 @@ def build(fastflix: FastFlix): (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), remove_hdr, "--psnr --ssim" if settings.metrics else "", - build_audio(video.video_settings.audio_tracks, video.streams.audio), - build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height), + build_audio(video.audio_tracks, video.streams.audio), + build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height), settings.extra, "-o", f'"{clean_file_string(video.video_settings.output_path)}"', diff --git a/fastflix/encoders/qsvencc_av1/command_builder.py b/fastflix/encoders/qsvencc_av1/command_builder.py index 019afe23..45a8b60a 100644 --- a/fastflix/encoders/qsvencc_av1/command_builder.py +++ b/fastflix/encoders/qsvencc_av1/command_builder.py @@ -143,8 +143,8 @@ def build(fastflix: FastFlix): ("--adapt-ltr" if settings.adapt_ltr else ""), ("--adapt-cqm" if settings.adapt_cqm else ""), "--psnr --ssim" if settings.metrics else "", - build_audio(video.video_settings.audio_tracks, video.streams.audio), - build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height), + build_audio(video.audio_tracks, video.streams.audio), + build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height), settings.extra, "-o", f'"{clean_file_string(video.video_settings.output_path)}"', diff --git a/fastflix/encoders/qsvencc_avc/command_builder.py b/fastflix/encoders/qsvencc_avc/command_builder.py index 2d54642f..0b9ac31a 100644 --- a/fastflix/encoders/qsvencc_avc/command_builder.py +++ b/fastflix/encoders/qsvencc_avc/command_builder.py @@ -124,8 +124,8 @@ def build(fastflix: FastFlix): ("--adapt-ltr" if settings.adapt_ltr else ""), ("--adapt-cqm" if settings.adapt_cqm else ""), "--psnr --ssim" if settings.metrics else "", - build_audio(video.video_settings.audio_tracks, video.streams.audio), - build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height), + build_audio(video.audio_tracks, video.streams.audio), + build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height), settings.extra, "-o", f'"{clean_file_string(video.video_settings.output_path)}"', diff --git a/fastflix/encoders/qsvencc_hevc/command_builder.py b/fastflix/encoders/qsvencc_hevc/command_builder.py index dfe65639..c1ef50ed 100644 --- a/fastflix/encoders/qsvencc_hevc/command_builder.py +++ b/fastflix/encoders/qsvencc_hevc/command_builder.py @@ -143,8 +143,8 @@ def build(fastflix: FastFlix): ("--adapt-ltr" if settings.adapt_ltr else ""), ("--adapt-cqm" if settings.adapt_cqm else ""), "--psnr --ssim" if settings.metrics else "", - build_audio(video.video_settings.audio_tracks, video.streams.audio), - build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height), + build_audio(video.audio_tracks, video.streams.audio), + build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height), settings.extra, "-o", f'"{clean_file_string(video.video_settings.output_path)}"', diff --git a/fastflix/encoders/vceencc_av1/command_builder.py b/fastflix/encoders/vceencc_av1/command_builder.py index 4e6ff4fb..84391ffc 100644 --- a/fastflix/encoders/vceencc_av1/command_builder.py +++ b/fastflix/encoders/vceencc_av1/command_builder.py @@ -141,8 +141,8 @@ def build(fastflix: FastFlix): (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), remove_hdr, "--psnr --ssim" if settings.metrics else "", - build_audio(video.video_settings.audio_tracks, video.streams.audio), - build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height), + build_audio(video.audio_tracks, video.streams.audio), + build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height), settings.extra, "-o", f'"{clean_file_string(video.video_settings.output_path)}"', diff --git a/fastflix/encoders/vceencc_avc/command_builder.py b/fastflix/encoders/vceencc_avc/command_builder.py index 4c5f17c3..8382056d 100644 --- a/fastflix/encoders/vceencc_avc/command_builder.py +++ b/fastflix/encoders/vceencc_avc/command_builder.py @@ -126,8 +126,8 @@ def build(fastflix: FastFlix): (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), remove_hdr, "--psnr --ssim" if settings.metrics else "", - build_audio(video.video_settings.audio_tracks, video.streams.audio), - build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height), + build_audio(video.audio_tracks, video.streams.audio), + build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height), settings.extra, "-o", f'"{clean_file_string(video.video_settings.output_path)}"', diff --git a/fastflix/encoders/vceencc_hevc/command_builder.py b/fastflix/encoders/vceencc_hevc/command_builder.py index d3606fc0..bda09b85 100644 --- a/fastflix/encoders/vceencc_hevc/command_builder.py +++ b/fastflix/encoders/vceencc_hevc/command_builder.py @@ -143,8 +143,8 @@ def build(fastflix: FastFlix): (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""), remove_hdr, "--psnr --ssim" if settings.metrics else "", - build_audio(video.video_settings.audio_tracks, video.streams.audio), - build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height), + build_audio(video.audio_tracks, video.streams.audio), + build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height), settings.extra, "-o", f'"{clean_file_string(video.video_settings.output_path)}"', diff --git a/fastflix/ff_queue.py b/fastflix/ff_queue.py index f6cd7cde..8ee9de73 100644 --- a/fastflix/ff_queue.py +++ b/fastflix/ff_queue.py @@ -35,10 +35,10 @@ def get_queue(queue_file: Path) -> list[Video]: video["video_settings"]["output_path"] = Path(video["video_settings"]["output_path"]) encoder_settings = video["video_settings"]["video_encoder_settings"] ves = [x(**encoder_settings) for x in setting_types.values() if x().name == encoder_settings["name"]][0] - audio = [AudioTrack(**x) for x in video["video_settings"]["audio_tracks"]] - subtitles = [SubtitleTrack(**x) for x in video["video_settings"]["subtitle_tracks"]] + # TODO breaks - audio = [AudioTrack(**x) for x in video["audio_tracks"]] + # TODO breaks subtitles = [SubtitleTrack(**x) for x in video["subtitle_tracks"]] attachments = [] - for x in video["video_settings"]["attachment_tracks"]: + for x in video["attachment_tracks"]: try: attachment_path = x.pop("file_path") except KeyError: @@ -50,9 +50,9 @@ def get_queue(queue_file: Path) -> list[Video]: crop = None if video["video_settings"]["crop"]: crop = Crop(**video["video_settings"]["crop"]) - del video["video_settings"]["audio_tracks"] - del video["video_settings"]["subtitle_tracks"] - del video["video_settings"]["attachment_tracks"] + del video["audio_tracks"] + del video["subtitle_tracks"] + del video["attachment_tracks"] del video["video_settings"]["video_encoder_settings"] del video["status"] del video["video_settings"]["crop"] @@ -108,7 +108,7 @@ def update_conversion_command(vid, old_path: str, new_path: str): str(new_metadata_file), ) video["video_settings"]["video_encoder_settings"]["hdr10plus_metadata"] = str(new_metadata_file) - for track in video["video_settings"]["attachment_tracks"]: + for track in video["attachment_tracks"]: if track.get("file_path"): if not Path(track["file_path"]).exists(): logger.exception("Could not save cover to queue recovery location, removing cover") diff --git a/fastflix/flix.py b/fastflix/flix.py index 09379a2a..d8956981 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -5,7 +5,7 @@ from pathlib import Path from subprocess import PIPE, CompletedProcess, Popen, TimeoutExpired, run, check_output from typing import List, Tuple, Union -from distutils.version import LooseVersion +from packaging import version import shlex import reusables @@ -275,6 +275,8 @@ def parse(app: FastFlixApp, **_): def extract_attachments(app: FastFlixApp, **_): + if app.fastflix.config.disable_cover_extraction: + return for track in app.fastflix.current_video.streams.attachment: filename = track.get("tags", {}).get("filename", "") if filename.rsplit(".", 1)[0] in ("cover", "small_cover", "cover_land", "small_cover_land"): @@ -376,6 +378,9 @@ def get_auto_crop( ) width, height, x_crop, y_crop = None, None, None, None + if not output.stderr: + return 0, 0, 0, 0 + for line in output.stderr.splitlines(): if line.startswith("[Parsed_cropdetect"): w, h, x, y = [int(x) for x in line.rsplit("=")[1].split(":")] @@ -429,6 +434,10 @@ def detect_interlaced(app: FastFlixApp, config: Config, source: Path, **_): logger.exception("Error while running the interlace detection command") return + if not output.stderr: + logger.warning("Could not extract interlaced information") + return + for line in output.stderr.splitlines(): if "Single frame detection" in line: try: @@ -560,14 +569,14 @@ def parse_hdr_details(app: FastFlixApp, **_): ) -def get_hdr10_parser_version(config: Config) -> LooseVersion: +def get_hdr10_parser_version(config: Config) -> version: global HDR10_parser_version if HDR10_parser_version: return HDR10_parser_version 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) + HDR10_parser_version = version.parse(version_string) logger.debug(f"Using HDR10 parser version {str(HDR10_parser_version).strip()}") return HDR10_parser_version @@ -606,7 +615,7 @@ def detect_hdr10_plus(app: FastFlixApp, config: Config, **_): ) hdr10_parser_command = [str(config.hdr10plus_parser), "--verify", "-"] - if parser_version >= LooseVersion("1.0.0"): + if parser_version >= version.parse("1.0.0"): hdr10_parser_command.insert(-1, "extract") process_two = Popen( diff --git a/fastflix/models/config.py b/fastflix/models/config.py index a4993ccf..f8a5074b 100644 --- a/fastflix/models/config.py +++ b/fastflix/models/config.py @@ -3,11 +3,10 @@ import logging import os import shutil -from distutils.version import StrictVersion +from packaging import version from pathlib import Path from typing import Literal import json -import sys from appdirs import user_data_dir from box import Box, BoxError @@ -163,6 +162,8 @@ class Config(BaseModel): sticky_tabs: bool = False disable_complete_message: bool = False + disable_cover_extraction: bool = False + def encoder_opt(self, profile_name, profile_option_name): encoder_settings = getattr(self.profiles[self.selected_profile], profile_name) if encoder_settings: @@ -266,7 +267,7 @@ def load(self, portable_mode=False): if "version" not in data: raise ConfigError(f"Corrupt config file. Please fix or remove {self.config_path}") - if StrictVersion(__version__) < StrictVersion(data.version): + if version.parse(__version__) < version.parse(data.version): logger.warning( f"This FastFlix version ({__version__}) is older " f"than the one that generated the config file ({data.version}), " @@ -315,7 +316,7 @@ def load(self, portable_mode=False): # 5.2.0 remove ext self.output_name_format = self.output_name_format.replace(".{ext}", "").replace("{ext}", "") - # if StrictVersion(__version__) > StrictVersion(data.version): + # if version.parse(__version__) > version.parse(data.version): # logger.info(f"Clearing possible old config values from fastflix {data.verion}") # self.vceencc_encoders = [] # self.nvencc_encoders = [] diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py index 42901662..4714e572 100644 --- a/fastflix/models/encode.py +++ b/fastflix/models/encode.py @@ -14,7 +14,8 @@ class AudioTrack(BaseModel): downmix: Optional[str] = None title: str = "" language: str = "" - conversion_bitrate: str = "" + conversion_aq: Optional[int] = None + conversion_bitrate: Optional[str] = None conversion_codec: str = "" profile: Optional[str] = None enabled: bool = True @@ -33,6 +34,8 @@ class SubtitleTrack(BaseModel): language: str = "" subtitle_type: str = "" dispositions: dict = Field(default_factory=dict) + enabled: bool = True + long_name: str = "" class AttachmentTrack(BaseModel): diff --git a/fastflix/models/video.py b/fastflix/models/video.py index 5c7fddbf..37ced71c 100644 --- a/fastflix/models/video.py +++ b/fastflix/models/video.py @@ -138,9 +138,9 @@ class VideoSettings(BaseModel): VAAPIVP9Settings, VAAPIMPEG2Settings, ] = None - audio_tracks: list[AudioTrack] = Field(default_factory=list) - subtitle_tracks: list[SubtitleTrack] = Field(default_factory=list) - attachment_tracks: list[AttachmentTrack] = Field(default_factory=list) + # audio_tracks: list[AudioTrack] = Field(default_factory=list) + # subtitle_tracks: list[SubtitleTrack] = Field(default_factory=list) + # attachment_tracks: list[AttachmentTrack] = Field(default_factory=list) conversion_commands: List = Field(default_factory=list) @@ -181,6 +181,10 @@ class Video(BaseModel): hdr10_plus: list[int] = Field(default_factory=list) video_settings: VideoSettings = Field(default_factory=VideoSettings) + audio_tracks: list[AudioTrack] = Field(default_factory=list) + subtitle_tracks: list[SubtitleTrack] = Field(default_factory=list) + attachment_tracks: list[AttachmentTrack] = Field(default_factory=list) + status: Status = Field(default_factory=Status) uuid: str = Field(default_factory=lambda: str(uuid.uuid4())) diff --git a/fastflix/version.py b/fastflix/version.py index b2c943c0..08393cc6 100644 --- a/fastflix/version.py +++ b/fastflix/version.py @@ -1,4 +1,4 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -__version__ = "5.6.0" +__version__ = "5.7.0" __author__ = "Chris Griffith" diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 24f11467..5acabfa9 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -3,7 +3,7 @@ import logging from pathlib import Path from subprocess import PIPE, STDOUT, Popen, run, check_output -from distutils.version import LooseVersion +from packaging import version from PySide6 import QtCore @@ -114,7 +114,7 @@ def run(self): [str(self.app.fastflix.config.hdr10plus_parser), "--version"], encoding="utf-8" ) _, version_string = hdr10_parser_version_output.rsplit(sep=" ", maxsplit=1) - hdr10_parser_version = LooseVersion(version_string) + hdr10_parser_version = version.parse(version_string) self.main.thread_logging_signal.emit(f"Using HDR10 parser version {str(hdr10_parser_version).strip()}") ffmpeg_command = [ @@ -134,7 +134,7 @@ def run(self): ] hdr10_parser_command = [str(self.app.fastflix.config.hdr10plus_parser), "-o", clean_file_string(output), "-"] - if hdr10_parser_version >= LooseVersion("1.0.0"): + if hdr10_parser_version >= version.parse("1.0.0"): hdr10_parser_command.insert(1, "extract") self.main.thread_logging_signal.emit( diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index e5b2ea99..5675aa43 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -2141,12 +2141,3 @@ def run(self): return self.main.status_update_signal.emit(status) self.app.processEvents() - # if status[0] == "complete": - # logger.debug("GUI received status queue complete") - # self.main.completed.emit(0) - # elif status[0] == "error": - # logger.debug("GUI received status queue errored") - # self.main.completed.emit(1) - # elif status[0] == "cancelled": - # logger.debug("GUI received status queue errored") - # self.main.cancelled.emit("|".join(status[1:])) diff --git a/fastflix/widgets/panels/abstract_list.py b/fastflix/widgets/panels/abstract_list.py index 72477233..00da12d2 100644 --- a/fastflix/widgets/panels/abstract_list.py +++ b/fastflix/widgets/panels/abstract_list.py @@ -80,9 +80,9 @@ def reorder(self, update=True, height=66): if ( self.app.fastflix.current_video and self.app.fastflix.current_video.video_settings - and isinstance(self.app.fastflix.current_video.video_settings.audio_tracks, list) + and isinstance(self.app.fastflix.current_video.audio_tracks, list) ): - start = len(self.app.fastflix.current_video.video_settings.audio_tracks) + 1 + start = len([x for x in self.app.fastflix.current_video.audio_tracks if x.enabled]) + 1 for index, widget in enumerate(self.tracks, start): self.inner_layout.addWidget(widget) diff --git a/fastflix/widgets/panels/audio_panel.py b/fastflix/widgets/panels/audio_panel.py index 0e79fb36..097f0ff6 100644 --- a/fastflix/widgets/panels/audio_panel.py +++ b/fastflix/widgets/panels/audio_panel.py @@ -17,6 +17,7 @@ from fastflix.shared import no_border, error_message, yes_no_message, clear_list from fastflix.widgets.panels.abstract_list import FlixList from fastflix.audio_processing import apply_audio_filters +from fastflix.widgets.windows.audio_conversion import AudioConversion from fastflix.widgets.windows.disposition import Disposition language_list = sorted((k for k, v in Lang._data["name"].items() if v["pt2B"] and v["pt1"]), key=lambda x: x.lower()) @@ -42,66 +43,36 @@ class Audio(QtWidgets.QTabWidget): def __init__( self, + app, parent, - audio, index, - codec, - available_audio_encoders, - title="", - language="", - profile="", - outdex=None, - enabled=True, - original=False, - first=False, - last=False, - codecs=(), - channels=2, - all_info=None, - disable_dup=False, - dispositions=None, + disabled_dup=False, ): self.loading = True super(Audio, self).__init__(parent) + self.app = app self.setObjectName("Audio") - self.parent = parent - self.audio = audio - self.setFixedHeight(60) - self.original = original - self.outdex = index if self.original else outdex - self.first = first - self.track_name = title - self.profile = profile - self.last = last + self.parent: "AudioList" = parent self.index = index - self.codec = codec - self.codecs = codecs - self.channels = channels - self.available_audio_encoders = available_audio_encoders - self.all_info = all_info + self.first = False + self.last = False + self.setFixedHeight(60) + audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[index] self.widgets = Box( - track_number=QtWidgets.QLabel(f"{index}:{self.outdex}" if enabled else "❌"), - title=QtWidgets.QLineEdit(title), - audio_info=QtWidgets.QLabel(audio), - up_button=QtWidgets.QPushButton( - QtGui.QIcon(get_icon("up-arrow", self.parent.app.fastflix.config.theme)), "" - ), - down_button=QtWidgets.QPushButton( - QtGui.QIcon(get_icon("down-arrow", self.parent.app.fastflix.config.theme)), "" - ), + track_number=QtWidgets.QLabel(f"{audio_track.index}:{audio_track.outdex}" if audio_track.enabled else "❌"), + title=QtWidgets.QLineEdit(audio_track.title), + audio_info=QtWidgets.QLabel(audio_track.friendly_info), + up_button=QtWidgets.QPushButton(QtGui.QIcon(get_icon("up-arrow", self.app.fastflix.config.theme)), ""), + down_button=QtWidgets.QPushButton(QtGui.QIcon(get_icon("down-arrow", self.app.fastflix.config.theme)), ""), enable_check=QtWidgets.QCheckBox(t("Enabled")), - dup_button=QtWidgets.QPushButton( - QtGui.QIcon(get_icon("onyx-copy", self.parent.app.fastflix.config.theme)), "" - ), - delete_button=QtWidgets.QPushButton( - QtGui.QIcon(get_icon("black-x", self.parent.app.fastflix.config.theme)), "" - ), + dup_button=QtWidgets.QPushButton(QtGui.QIcon(get_icon("onyx-copy", self.app.fastflix.config.theme)), ""), + delete_button=QtWidgets.QPushButton(QtGui.QIcon(get_icon("black-x", self.app.fastflix.config.theme)), ""), language=QtWidgets.QComboBox(), - downmix=QtWidgets.QComboBox(), convert_to=None, convert_bitrate=None, disposition=QtWidgets.QPushButton(), + conversion=QtWidgets.QPushButton(t("Conversion")), ) self.widgets.up_button.setStyleSheet(no_border) @@ -109,14 +80,13 @@ def __init__( self.widgets.dup_button.setStyleSheet(no_border) self.widgets.delete_button.setStyleSheet(no_border) - if all_info: - self.widgets.audio_info.setToolTip(all_info.to_yaml()) + self.widgets.audio_info.setToolTip(audio_track.raw_info.to_yaml()) self.widgets.language.addItems(["No Language Set"] + language_list) self.widgets.language.setMaximumWidth(150) - if language: + if audio_track.language: try: - lang = Lang(language).name + lang = Lang(audio_track.language).name except InvalidLanguageValue: pass else: @@ -128,18 +98,13 @@ def __init__( self.widgets.title.textChanged.connect(self.page_update) # 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.setChecked(audio_track.enabled) self.widgets.enable_check.toggled.connect(self.update_enable) self.widgets.dup_button.clicked.connect(lambda: self.dup_me()) self.widgets.dup_button.setFixedWidth(20) - if disable_dup: + if disabled_dup: self.widgets.dup_button.hide() self.widgets.dup_button.setDisabled(True) @@ -148,15 +113,16 @@ def __init__( self.widgets.track_number.setFixedWidth(20) - self.dispositions = dispositions or {k: False for k in disposition_options} - - self.disposition_widget = Disposition(self, f"Audio Track {index}", subs=True) - self.set_dis_button() + self.disposition_widget = Disposition( + app=app, parent=self, track_name=f"Audio Track {index}", track_index=index, audio=True + ) self.widgets.disposition.clicked.connect(self.disposition_widget.show) + self.widgets.conversion.clicked.connect(self.show_conversions) + disposition_layout = QtWidgets.QHBoxLayout() - disposition_layout.addWidget(QtWidgets.QLabel(t("Dispositions"))) disposition_layout.addWidget(self.widgets.disposition) + self.widgets.disposition.setText(t("Dispositions")) label = QtWidgets.QLabel(f"{t('Title')}: ") self.widgets.title.setFixedWidth(150) @@ -172,13 +138,12 @@ def __init__( grid.addWidget(self.widgets.audio_info, 0, 2) grid.addLayout(title_layout, 0, 3) grid.addLayout(disposition_layout, 0, 4) - grid.addLayout(self.init_conversion(), 0, 5) - grid.addWidget(self.widgets.downmix, 0, 6) - grid.addWidget(self.widgets.language, 0, 7) + grid.addWidget(self.widgets.conversion, 0, 5) + grid.addWidget(self.widgets.language, 0, 6) - right_button_start_index = 8 + right_button_start_index = 7 - if not original: + if not audio_track.original: spacer = QtWidgets.QLabel() spacer.setFixedWidth(63) grid.addWidget(spacer, 0, right_button_start_index) @@ -187,8 +152,24 @@ def __init__( grid.addWidget(self.widgets.enable_check, 0, right_button_start_index) grid.addWidget(self.widgets.dup_button, 0, right_button_start_index + 1) self.setLayout(grid) + self.conversion_box = None self.loading = False + def show_conversions(self): + try: + self.conversion_box.close() + except Exception: + pass + try: + del self.conversion_box + except Exception: + pass + + self.conversion_box = AudioConversion( + self.app, track_index=self.index, encoders=self.app.fastflix.audio_encoders + ) + self.conversion_box.show() + def init_move_buttons(self): layout = QtWidgets.QVBoxLayout() layout.setSpacing(0) @@ -205,41 +186,6 @@ def init_move_buttons(self): layout.addWidget(self.widgets.down_button) return layout - def init_conversion(self): - layout = QtWidgets.QHBoxLayout() - self.widgets.convert_to = QtWidgets.QComboBox() - - self.update_codecs(self.codecs) - - self.widgets.convert_bitrate = QtWidgets.QComboBox() - self.widgets.convert_bitrate.setFixedWidth(70) - 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(self.widgets.bitrate_label) - layout.addWidget(self.widgets.convert_bitrate) - - return layout - - def set_dis_button(self): - output = "" - for disposition, is_set in self.dispositions.items(): - if is_set: - output += f"{t(disposition)}," - if output: - self.widgets.disposition.setText(output.rstrip(",")) - else: - self.widgets.disposition.setText(t("none")) - def get_conversion_bitrates(self, channels=None): if not channels: channels = self.channels or 2 @@ -250,85 +196,19 @@ def get_conversion_bitrates(self, channels=None): def update_enable(self): enabled = self.widgets.enable_check.isChecked() - self.widgets.track_number.setText(f"{self.index}:{self.outdex}" if enabled else "❌") + audio_track = self.app.fastflix.current_video.audio_tracks[self.index] + audio_track.enabled = enabled + self.widgets.track_number.setText(f"{audio_track.index}:{audio_track.outdex}" if enabled else "❌") self.parent.reorder(update=True) - - def update_downmix(self): - channels = ( - channel_list[self.widgets.downmix.currentText()] - if self.widgets.downmix.currentIndex() > 0 - else self.channels - ) - if self.conversion["codec"] not in lossless: - self.widgets.convert_bitrate.clear() - self.widgets.convert_bitrate.addItems(self.get_conversion_bitrates(channels)) - self.widgets.convert_bitrate.setCurrentIndex(3) - self.page_update() - - 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.conversion["codec"] in lossless: - self.widgets.convert_bitrate.setDisabled(True) - self.widgets.convert_bitrate.addItem("lossless") - self.widgets.convert_bitrate.setCurrentText("lossless") - else: - self.widgets.convert_bitrate.setDisabled(False) - self.widgets.convert_bitrate.clear() - channels = ( - channel_list[self.widgets.downmix.currentText()] - if self.widgets.downmix.currentIndex() > 0 - else self.channels - ) - self.widgets.convert_bitrate.addItems(self.get_conversion_bitrates(channels)) - self.widgets.convert_bitrate.setCurrentIndex(3) - self.page_update() + self.parent.parent.subtitles.reorder() def page_update(self): if not self.loading: return self.parent.main.page_update(build_thumbnail=False) - def update_codecs(self, codec_list): - self.loading = True - current = self.widgets.convert_to.currentText() - self.widgets.convert_to.clear() - # passthrough_available = False - # if self.codec in codec_list: - passthrough_available = True - self.widgets.convert_to.addItem(t("none")) - self.widgets.convert_to.addItems(sorted(set(self.available_audio_encoders) & set(codec_list))) - if current in codec_list: - index = codec_list.index(current) - if passthrough_available: - index += 1 - self.widgets.convert_to.setCurrentIndex(index) - self.widgets.convert_to.setCurrentIndex(0) # Will either go to 'copy' or first listed - if self.widgets.convert_bitrate: - self.widgets.convert_bitrate.setDisabled(True) - self.loading = False - @property def enabled(self): - return self.widgets.enable_check.isChecked() - - @property - def conversion(self): - if self.widgets.convert_to.currentIndex() == 0: - return {"codec": "", "bitrate": ""} - return {"codec": self.widgets.convert_to.currentText(), "bitrate": self.widgets.convert_bitrate.currentText()} - - @property - def downmix(self) -> Optional[str]: - return self.widgets.downmix.currentText() if self.widgets.downmix.currentIndex() > 0 else None + return self.app.fastflix.current_video.audio_tracks[self.index].enabled @property def language(self) -> str: @@ -349,45 +229,60 @@ def set_last(self, last=True): self.widgets.down_button.setDisabled(self.last) def dup_me(self): - new = Audio( + # Add new track to the conversion list + new_track = self.app.fastflix.current_video.audio_tracks[self.index].copy() + new_track.outdex = len(self.app.fastflix.current_video.audio_tracks) + 1 + new_track.original = False + self.app.fastflix.current_video.audio_tracks.append(new_track) + + # Add new track to GUI + new_item = Audio( parent=self.parent, - audio=self.audio, - index=self.index, - language=self.language, - outdex=len(self.parent.tracks) + 1, - codec=self.codec, - available_audio_encoders=self.available_audio_encoders, - enabled=True, - original=False, - codecs=self.codecs, - channels=self.channels, - dispositions=self.dispositions, + app=self.app, + index=len(self.app.fastflix.current_video.audio_tracks) - 1, + disabled_dup=( + "nvencc" in self.parent.main.convert_to.lower() + or "vcenc" in self.parent.main.convert_to.lower() + or "qsvenc" in self.parent.main.convert_to.lower() + ), ) - - self.parent.tracks.append(new) + self.parent.tracks.append(new_item) self.parent.reorder() def del_me(self): self.parent.remove_track(self) + del self.app.fastflix.current_video.audio_tracks[self.index] def set_outdex(self, outdex): + self.app.fastflix.current_video.audio_tracks[self.index].outdex = outdex + audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[self.index] self.outdex = outdex - if not self.enabled: + if not audio_track.enabled: self.widgets.track_number.setText("❌") else: - self.widgets.track_number.setText(f"{self.index}:{self.outdex}") + self.widgets.track_number.setText(f"{audio_track.index}:{audio_track.outdex}") def close(self) -> bool: - del self.dispositions del self.widgets return super().close() + def update_track(self, conversion=None, bitrate=None, downmix=None): + audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[self.index] + if conversion: + audio_track.conversion_codec = conversion + if bitrate: + audio_track.conversion_bitrate = bitrate + if downmix: + audio_track.downmix = downmix + self.page_update() + class AudioList(FlixList): def __init__(self, parent, app: FastFlixApp): super(AudioList, self).__init__(app, parent, "Audio Tracks", "audio") self.available_audio_encoders = app.fastflix.audio_encoders self.app = app + self.parent = parent self._first_selected = False def _get_track_info(self, track): @@ -413,33 +308,35 @@ def disable_all(self): def new_source(self, codecs): clear_list(self.tracks, close=True) + self.app.fastflix.current_video.audio_tracks = [] self.tracks: list[Audio] = [] self._first_selected = False - disable_dup = ( - "nvencc" in self.main.convert_to.lower() - or "vcenc" in self.main.convert_to.lower() - or "qsvenc" in self.main.convert_to.lower() - ) - for i, x in enumerate(self.app.fastflix.current_video.streams.audio, start=1): + for i, x in enumerate(self.app.fastflix.current_video.streams.audio): track_info, tags = self._get_track_info(x) + self.app.fastflix.current_video.audio_tracks.append( + AudioTrack( + index=x.index, + outdex=i + 1, + title=tags.get("title", ""), + language=tags.get("language", ""), + profile=x.get("profile"), + channels=x.channels, + enabled=True, + original=True, + raw_info=x, + friendly_info=track_info, + dispositions={k: bool(v) for k, v in x.disposition.items()}, + ) + ) new_item = Audio( - self, - track_info, - title=tags.get("title"), - language=tags.get("language"), - profile=x.get("profile"), - original=True, - first=True if i == 0 else False, - index=x.index, - outdex=i, - codec=x.codec_name, - codecs=codecs, - channels=x.channels, - available_audio_encoders=self.available_audio_encoders, - enabled=True, - all_info=x, - disable_dup=disable_dup, - dispositions={k: bool(v) for k, v in x.disposition.items()}, + parent=self, + app=self.app, + index=i, + disabled_dup=( + "nvencc" in self.main.convert_to.lower() + or "vcenc" in self.main.convert_to.lower() + or "qsvenc" in self.main.convert_to.lower() + ), ) self.tracks.append(new_item) @@ -447,7 +344,7 @@ def new_source(self, codecs): self.tracks[0].set_first() self.tracks[-1].set_last() super()._new_source(self.tracks) - self.update_audio_settings() + # self.update_audio_settings() def allowed_formats(self, allowed_formats=None): disable_dups = ( @@ -457,8 +354,9 @@ def allowed_formats(self, allowed_formats=None): ) tracks_need_removed = False for track in self.tracks: + audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[track.index] track.widgets.dup_button.setDisabled(disable_dups) - if not track.original: + if not audio_track.original: if disable_dups: tracks_need_removed = True else: @@ -470,8 +368,8 @@ def allowed_formats(self, allowed_formats=None): error_message(t("This encoder does not support duplicating audio tracks, please remove copied tracks!")) if not allowed_formats: return - for track in self.tracks: - track.update_codecs(allowed_formats or set()) + # for track in self.tracks: + # track.update_codecs(allowed_formats or set()) def apply_profile_settings( self, @@ -488,64 +386,60 @@ def apply_profile_settings( clear_list(self.tracks) - def update_track(new_track, downmix=None, conversion=None, bitrate=None): - if conversion: - new_track.widgets.convert_to.setCurrentText(conversion) - # Downmix must come first - if downmix: - new_track.widgets.downmix.setCurrentText(downmix) - if conversion in lossless: - new_track.widgets.convert_bitrate.setDisabled(True) - new_track.widgets.convert_bitrate.addItem("lossless") - new_track.widgets.convert_bitrate.setCurrentText("lossless") - else: - if bitrate not in [ - new_track.widgets.convert_bitrate.itemText(i) - for i in range(new_track.widgets.convert_bitrate.count()) - ]: - new_track.widgets.convert_bitrate.addItem(bitrate) - new_track.widgets.convert_bitrate.setCurrentText(bitrate) - new_track.widgets.convert_bitrate.setDisabled(False) - 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=og_only, + self.app.fastflix.current_video.audio_tracks.append( + AudioTrack( + index=audio_track.index, + outdex=outdex, + title=tags.get("title", ""), + language=tags.get("language", ""), + profile=audio_track.get("profile"), + channels=audio_track.channels, + enabled=enabled, + original=og, + raw_info=audio_track, + friendly_info=track_info, + downmix=downmix, + conversion_codec=conversion, + conversion_bitrate=bitrate, + dispositions={k: bool(v) for k, v in audio_track.disposition.items()}, + ) ) + new_item = Audio( + parent=parent, + app=self.app, + index=i, + disabled_dup=( + "nvencc" in self.main.convert_to.lower() + or "vcenc" in self.main.convert_to.lower() + or "qsvenc" in self.main.convert_to.lower() + ), + ) + self.tracks.append(new_item) - update_track(new_track=new_track, downmix=downmix, conversion=conversion, bitrate=bitrate) + return new_item - return new_track + self.new_source(audio_formats) - # 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)) + # # 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 = apply_audio_filters(profile.audio_filters, original_tracks=original_tracks) + for track in self.tracks: + track.widgets.enable_check.setChecked(False) + if profile.audio_filters is not False and self.tracks and not tracks: enable = yes_no_message( t("No audio tracks matched for this profile, enable first track?"), title="No Audio Match" ) if enable: self.tracks[0].widgets.enable_check.setChecked(True) - return super()._new_source(self.tracks) + return # Apply first set of conversions to the original audio tracks current_id = -1 @@ -555,8 +449,7 @@ def gen_track( if track[0].index > current_id: current_id = track[0].index self.tracks[track[0].index - 1].widgets.enable_check.setChecked(True) - update_track( - self.tracks[track[0].index - 1], + self.tracks[track[0].index - 1].update_track( downmix=track[1].downmix, conversion=track[1].conversion, bitrate=track[1].bitrate, @@ -585,111 +478,40 @@ def gen_track( super()._new_source(self.tracks) def update_audio_settings(self): - tracks = [] - for track in self.tracks: - if track.enabled: - tracks.append( - AudioTrack( - index=track.index, - outdex=track.outdex, - conversion_bitrate=track.conversion["bitrate"], - conversion_codec=track.conversion["codec"], - codec=track.codec, - downmix=track.downmix, - title=track.title, - language=track.language, - profile=track.profile, - channels=track.channels, - enabled=track.enabled, - original=track.original, - raw_info=track.all_info, - friendly_info=track.audio, - dispositions=track.dispositions, - ) - ) - clear_list(self.app.fastflix.current_video.video_settings.audio_tracks) - self.app.fastflix.current_video.video_settings.audio_tracks = tracks + return # TODO remove def reload(self, original_tracks: list[AudioTrack], audio_formats): + clear_list(self.tracks) disable_dups = ( "nvencc" in self.main.convert_to.lower() or "vcenc" in self.main.convert_to.lower() or "qsvenc" in self.main.convert_to.lower() ) - repopulated_tracks = set() - for track in original_tracks: - if track.original: - repopulated_tracks.add(track.index) - - new_track = Audio( - parent=self, - audio=track.friendly_info, - all_info=Box(track.raw_info) if track.raw_info else None, - title=track.title, - language=track.language, - profile=track.profile, - original=track.original, - index=track.index, - outdex=track.outdex, - codec=track.codec, - codecs=audio_formats, - channels=track.channels, - available_audio_encoders=self.available_audio_encoders, - enabled=True, - disable_dup=disable_dups, - dispositions=track.dispositions, + for i, track in enumerate(self.app.fastflix.current_video.audio_tracks): + self.tracks.append( + Audio( + app=self.app, + parent=self, + index=i, + disabled_dup=disable_dups, + ) ) - new_track.widgets.downmix.setCurrentText(track.downmix) - new_track.widgets.convert_to.setCurrentText(track.conversion_codec) - if track.conversion_codec in lossless: - new_track.widgets.convert_bitrate.setDisabled(True) - new_track.widgets.convert_bitrate.addItem("lossless") - new_track.widgets.convert_bitrate.setCurrentText("lossless") - else: - if track.conversion_bitrate not in [ - new_track.widgets.convert_bitrate.itemText(i) - for i in range(new_track.widgets.convert_bitrate.count()) - ]: - new_track.widgets.convert_bitrate.addItem(track.conversion_bitrate) - new_track.widgets.convert_bitrate.setCurrentText(track.conversion_bitrate) - new_track.widgets.title.setText(track.title) - - if track.language: - new_track.widgets.language.setCurrentText(Lang(track.language).name) - else: - new_track.widgets.language.setCurrentIndex(0) - - self.tracks.append(new_track) + super()._new_source(self.tracks) - for i, x in enumerate(self.app.fastflix.current_video.streams.audio, start=1): - if x.index in repopulated_tracks: - continue - track_info, tags = self._get_track_info(x) - new_item = Audio( - self, - track_info, - title=tags.get("title"), - language=tags.get("language"), - profile=x.get("profile"), - original=True, - index=x.index, - outdex=i, - codec=x.codec_name, - codecs=audio_formats, - channels=x.channels, - available_audio_encoders=self.available_audio_encoders, - enabled=False, - all_info=x, - disable_dup=disable_dups, - ) - for idx, tk in enumerate(self.tracks): - if tk.index > new_item.index: - print(f"Inserting at {idx}") - self.tracks.insert(idx, new_item) - break - else: - self.tracks.append(new_item) + def move_up(self, widget): + self.app.fastflix.current_video.audio_tracks.insert( + widget.index - 1, self.app.fastflix.current_video.audio_tracks.pop(widget.index) + ) + index = self.tracks.index(widget) + self.tracks.insert(index - 1, self.tracks.pop(index)) + self.reorder() - super()._new_source(self.tracks) + def move_down(self, widget): + self.app.fastflix.current_video.audio_tracks.insert( + widget.index + 1, self.app.fastflix.current_video.audio_tracks.pop(widget.index) + ) + index = self.tracks.index(widget) + self.tracks.insert(index + 1, self.tracks.pop(index)) + self.reorder() diff --git a/fastflix/widgets/panels/cover_panel.py b/fastflix/widgets/panels/cover_panel.py index 519b7892..cb61e64a 100644 --- a/fastflix/widgets/panels/cover_panel.py +++ b/fastflix/widgets/panels/cover_panel.py @@ -212,8 +212,8 @@ def update_cover_settings(self): return start_outdex = ( 1 # Video Track - + len(self.app.fastflix.current_video.video_settings.audio_tracks) - + len(self.app.fastflix.current_video.video_settings.subtitle_tracks) + + len(self.app.fastflix.current_video.audio_tracks) + + len(self.app.fastflix.current_video.subtitle_tracks) ) attachments: list[AttachmentTrack] = [] @@ -230,7 +230,7 @@ def update_cover_settings(self): ) ) start_outdex += 1 - self.app.fastflix.current_video.video_settings.attachment_tracks = attachments + self.app.fastflix.current_video.attachment_tracks = attachments def cover_passthrough_check(self): checked = self.cover_passthrough_checkbox.isChecked() @@ -354,14 +354,14 @@ def new_source(self, attachments): self.cover_land_passthrough_checkbox.toggled.connect(lambda: self.cover_land_passthrough_check()) self.small_cover_land_passthrough_checkbox.toggled.connect(lambda: self.small_cover_land_passthrough_check()) - def reload_from_queue(self, streams, settings: VideoSettings): + def reload_from_queue(self, streams, attachment_tracks): self.new_source(streams.attachment) self.cover_passthrough_checkbox.setChecked(False) self.cover_land_passthrough_checkbox.setChecked(False) self.small_cover_land_passthrough_checkbox.setChecked(False) self.small_cover_passthrough_checkbox.setChecked(False) - for attachment in settings.attachment_tracks: + for attachment in attachment_tracks: if attachment.filename == "cover": if attachment.index is None: self.cover_path.setText(str(attachment.file_path)) @@ -385,8 +385,8 @@ def reload_from_queue(self, streams, settings: VideoSettings): # def update_cover_settings(self): # start_outdex = ( # 1 # Video Track -# + len(self.app.fastflix.current_video.video_settings.audio_tracks) -# + len(self.app.fastflix.current_video.video_settings.subtitle_tracks) +# + len(self.app.fastflix.current_video.audio_tracks) +# + len(self.app.fastflix.current_video.subtitle_tracks) # ) # attachments: list[AttachmentTrack] = [] # diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py index 4d19c46e..9a8f0416 100644 --- a/fastflix/widgets/panels/queue_panel.py +++ b/fastflix/widgets/panels/queue_panel.py @@ -87,11 +87,11 @@ def __init__(self, parent, video: Video, index, first=False): title.setFixedWidth(300) settings = Box(copy.deepcopy(video.video_settings.dict())) - settings.output_path = str(settings.output_path) - for i, o in enumerate(settings.attachment_tracks): - if o.get("file_path"): - o["file_path"] = str(o["file_path"]) - del settings.conversion_commands + # settings.output_path = str(settings.output_path) + # for i, o in enumerate(video.attachment_tracks): + # if o.file_path: + # o["file_path"] = str(o["file_path"]) + # del settings.conversion_commands title.setToolTip(settings.video_encoder_settings.to_yaml()) del settings @@ -144,8 +144,8 @@ def __init__(self, parent, video: Video, index, first=False): # grid.addWidget(self.widgets.track_number, 0, 1) grid.addWidget(title, 0, 1, 1, 3) grid.addWidget(QtWidgets.QLabel(f"{video.video_settings.video_encoder_settings.name}"), 0, 4) - grid.addWidget(QtWidgets.QLabel(f"{t('Audio Tracks')}: {len(video.video_settings.audio_tracks)}"), 0, 5) - grid.addWidget(QtWidgets.QLabel(f"{t('Subtitles')}: {len(video.video_settings.subtitle_tracks)}"), 0, 6) + grid.addWidget(QtWidgets.QLabel(f"{t('Audio Tracks')}: {len(video.audio_tracks)}"), 0, 5) + grid.addWidget(QtWidgets.QLabel(f"{t('Subtitles')}: {len(video.subtitle_tracks)}"), 0, 6) grid.addWidget(QtWidgets.QLabel(status), 0, 7) if not video.status.error and video.status.complete and not get_bool_env("FF_DOCKERMODE"): grid.addWidget(view_button, 0, 8) diff --git a/fastflix/widgets/panels/subtitle_panel.py b/fastflix/widgets/panels/subtitle_panel.py index 09a22244..deb79fb2 100644 --- a/fastflix/widgets/panels/subtitle_panel.py +++ b/fastflix/widgets/panels/subtitle_panel.py @@ -49,22 +49,22 @@ class Subtitle(QtWidgets.QTabWidget): extract_completed_signal = QtCore.Signal() - def __init__(self, parent, subtitle, index, enabled=True, first=False, dispositions=None): + def __init__(self, app, parent, index, enabled=True, first=False): self.loading = True super(Subtitle, self).__init__(parent) - self.parent = parent + self.app = app + self.parent: "SubtitleList" = parent + self.setObjectName("Subtitle") self.index = index self.outdex = None - self.subtitle = Box(subtitle, default_box=True) self.first = first self.last = False - self.subtitle_lang = subtitle.get("tags", {}).get("language") - self.subtitle_type = subtitle_types.get(subtitle.get("codec_name", "text"), "text") self.setFixedHeight(60) + sub_track: SubtitleTrack = self.app.fastflix.current_video.subtitle_tracks[index] self.widgets = Box( - track_number=QtWidgets.QLabel(f"{self.index}:{self.outdex}" if enabled else "❌"), - title=QtWidgets.QLabel(f" {self.subtitle.codec_long_name}"), + track_number=QtWidgets.QLabel(f"{sub_track.index}:{sub_track.outdex}" if enabled else "❌"), + title=QtWidgets.QLabel(f" {sub_track.long_name}"), up_button=QtWidgets.QPushButton( QtGui.QIcon(get_icon("up-arrow", self.parent.app.fastflix.config.theme)), "" ), @@ -72,7 +72,7 @@ def __init__(self, parent, subtitle, index, enabled=True, first=False, dispositi QtGui.QIcon(get_icon("down-arrow", self.parent.app.fastflix.config.theme)), "" ), enable_check=QtWidgets.QCheckBox(t("Preserve")), - disposition=QtWidgets.QPushButton(), + disposition=QtWidgets.QPushButton(t("Dispositions")), language=QtWidgets.QComboBox(), burn_in=QtWidgets.QCheckBox(t("Burn In")), ) @@ -97,7 +97,7 @@ def __init__(self, parent, subtitle, index, enabled=True, first=False, dispositi # self.widgets.disposition.setCurrentIndex(dispositions.index("forced")) self.setFixedHeight(60) - self.widgets.title.setToolTip(self.subtitle.to_yaml()) + # self.widgets.title.setToolTip(self.subtitle.to_yaml()) self.widgets.burn_in.setToolTip( f"""{t("Overlay this subtitle track onto the video during conversion.")}\n {t("Please make sure seek method is set to exact")}.\n @@ -113,14 +113,10 @@ def __init__(self, parent, subtitle, index, enabled=True, first=False, dispositi self.gif_label.setMovie(self.movie) # self.movie.start() - self.dispositions = dispositions if dispositions else {k: False for k in disposition_options} - if not dispositions: - for disposition, is_set in self.subtitle.disposition.items(): - if is_set: - self.dispositions[disposition] = True - - self.disposition_widget = Disposition(self, f"Subtitle Track {index}", subs=True) - self.set_dis_button() + self.disposition_widget = Disposition( + app=self.app, parent=self, track_name=f"Subtitle Track {index}", track_index=index, audio=False + ) + # self.set_dis_button() self.widgets.disposition.clicked.connect(self.disposition_widget.show) disposition_layout = QtWidgets.QHBoxLayout() @@ -132,7 +128,7 @@ def __init__(self, parent, subtitle, index, enabled=True, first=False, dispositi self.grid.addWidget(self.widgets.track_number, 0, 1) self.grid.addWidget(self.widgets.title, 0, 2) self.grid.setColumnStretch(2, True) - if self.subtitle_type == "text": + if sub_track.subtitle_type == "text": self.grid.addWidget(self.widgets.extract, 0, 3) self.grid.addWidget(self.gif_label, 0, 3) self.gif_label.hide() @@ -148,24 +144,6 @@ def __init__(self, parent, subtitle, index, enabled=True, first=False, dispositi self.updating_burn = False self.extract_completed_signal.connect(self.extraction_complete) - def set_dis_button(self): - output = "" - for disposition, is_set in self.dispositions.items(): - if is_set: - output += f"{t(disposition)}," - if output: - self.widgets.disposition.setText(output.rstrip(",")) - else: - self.widgets.disposition.setText(t("none")) - - @property - def dis_forced(self): - return self.dispositions.get("forced", False) - - @property - def dis_default(self): - return self.dispositions.get("default", False) - def extraction_complete(self): self.grid.addWidget(self.widgets.extract, 0, 3) self.movie.stop() @@ -214,19 +192,17 @@ def set_last(self, last=True): self.widgets.down_button.setDisabled(self.last) def set_outdex(self, outdex): + self.app.fastflix.current_video.subtitle_tracks[self.index].outdex = outdex + sub_track: SubtitleTrack = self.app.fastflix.current_video.subtitle_tracks[self.index] self.outdex = outdex if not self.enabled: self.widgets.track_number.setText("❌") else: - self.widgets.track_number.setText(f"{self.index}:{self.outdex}") - - # @property - # def disposition(self): - # return None + self.widgets.track_number.setText(f"{sub_track.index}:{sub_track.outdex}") @property def enabled(self): - return self.widgets.enable_check.isChecked() + return self.app.fastflix.current_video.subtitle_tracks[self.index].enabled @property def language(self): @@ -238,7 +214,9 @@ def burn_in(self): def update_enable(self): enabled = self.widgets.enable_check.isChecked() - self.widgets.track_number.setText(f"{self.index}:{self.outdex}" if enabled else "❌") + sub_track = self.app.fastflix.current_video.subtitle_tracks[self.index] + sub_track.enabled = enabled + self.widgets.track_number.setText(f"{sub_track.index}:{sub_track.outdex}" if enabled else "❌") self.parent.reorder(update=True) def update_burn_in(self): @@ -251,6 +229,8 @@ def update_burn_in(self): error_message(t("There is an existing burn-in track, only one can be enabled at a time")) if enable and self.parent.main.fast_time: self.parent.main.widgets.fast_time.setCurrentText("exact") + sub_track = self.app.fastflix.current_video.subtitle_tracks[self.index] + sub_track.burn_in = enable self.updating_burn = False self.page_update() @@ -263,7 +243,9 @@ class SubtitleList(FlixList): def __init__(self, parent, app: FastFlixApp): top_layout = QtWidgets.QHBoxLayout() - top_layout.addWidget(QtWidgets.QLabel(t("Subtitle Tracks"))) + label = QtWidgets.QLabel(t("Subtitle Tracks")) + label.setFixedHeight(30) + top_layout.addWidget(label) top_layout.addStretch(1) self.remove_all_button = QtWidgets.QPushButton(t("Unselect All")) @@ -317,9 +299,28 @@ def lang_match(self, track: Union[Subtitle, dict], ignore_first=False): def new_source(self): self.tracks = [] self._first_selected = False + audio_end = len(self.app.fastflix.current_video.audio_tracks) for index, track in enumerate(self.app.fastflix.current_video.streams.subtitle): enabled = self.lang_match(track) - new_item = Subtitle(self, track, index=track.index, first=True if index == 0 else False, enabled=enabled) + self.app.fastflix.current_video.subtitle_tracks.append( + SubtitleTrack( + index=track.index, + outdex=audio_end + index + 1, + dispositions={k: bool(v) for k, v in track.disposition.items()}, + burn_in=False, + language=track.get("tags", {}).get("language", ""), + subtitle_type=subtitle_types.get(track.get("codec_name", "text"), "text"), + enabled=enabled, + ) + ) + + new_item = Subtitle( + app=self.app, + parent=self, + index=index, + first=True if index == 0 else False, + enabled=enabled, + ) self.tracks.append(new_item) if self.tracks: self.tracks[0].set_first() @@ -328,10 +329,18 @@ def new_source(self): if self.app.fastflix.config.opt("subtitle_automatic_burn_in"): first_default, first_forced = None, None for track in self.tracks: - if not first_default and track.dis_default and self.lang_match(track, ignore_first=True): + if ( + not first_default + and self.app.fastflix.current_video.subtitle_tracks[track.index].dispositions.get("default", False) + and self.lang_match(track, ignore_first=True) + ): first_default = track break - if not first_forced and track.dis_forced and self.lang_match(track, ignore_first=True): + if ( + not first_forced + and self.app.fastflix.current_video.subtitle_tracks[track.index].dispositions.get("forced", False) + and self.lang_match(track, ignore_first=True) + ): first_forced = track break if not self.app.fastflix.config.disable_automatic_subtitle_burn_in: @@ -344,37 +353,35 @@ def new_source(self): self.get_settings() def get_settings(self): - tracks = [] - burn_in_count = 0 - for track in self.tracks: - if track.enabled: - tracks.append( - SubtitleTrack( - index=track.index, - outdex=track.outdex, - dispositions=track.dispositions, - language=track.language, - burn_in=track.burn_in, - subtitle_type=track.subtitle_type, - ) - ) - if track.burn_in: - burn_in_count += 1 - if burn_in_count > 1: - raise FastFlixInternalException(t("More than one track selected to burn in")) - clear_list(self.app.fastflix.current_video.video_settings.subtitle_tracks) - self.app.fastflix.current_video.video_settings.subtitle_tracks = tracks + return # TODO remove def reload(self, original_tracks): - enabled_tracks = [x.index for x in original_tracks] - self.new_source() - for track in self.tracks: - enabled = track.index in enabled_tracks - track.widgets.enable_check.setChecked(enabled) - if enabled: - existing_track = [x for x in original_tracks if x.index == track.index][0] - track.dispositions = existing_track.dispositions.copy() - track.set_dis_button() - track.widgets.burn_in.setChecked(existing_track.burn_in) - track.widgets.language.setCurrentText(Lang(existing_track.language).name) + clear_list(self.tracks) + + for i, track in enumerate(self.app.fastflix.current_video.subtitle_tracks): + self.tracks.append( + Subtitle( + app=self.app, + parent=self, + index=i, + first=True if i == 0 else False, + enabled=track.enabled, + ) + ) super()._new_source(self.tracks) + + def move_up(self, widget): + self.app.fastflix.current_video.subtitle_tracks.insert( + widget.index - 1, self.app.fastflix.current_video.subtitle_tracks.pop(widget.index) + ) + index = self.tracks.index(widget) + self.tracks.insert(index - 1, self.tracks.pop(index)) + self.reorder() + + def move_down(self, widget): + self.app.fastflix.current_video.subtitle_tracks.insert( + widget.index + 1, self.app.fastflix.current_video.subtitle_tracks.pop(widget.index) + ) + index = self.tracks.index(widget) + self.tracks.insert(index + 1, self.tracks.pop(index)) + self.reorder() diff --git a/fastflix/widgets/video_options.py b/fastflix/widgets/video_options.py index 938ef73a..e12bc80c 100644 --- a/fastflix/widgets/video_options.py +++ b/fastflix/widgets/video_options.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import copy import logging +from typing import TYPE_CHECKING from PySide6 import QtGui, QtWidgets, QtCore @@ -19,6 +20,9 @@ from fastflix.widgets.panels.status_panel import StatusPanel from fastflix.widgets.panels.subtitle_panel import SubtitleList +if TYPE_CHECKING: + from fastflix.widgets.main import Main + logger = logging.getLogger("fastflix") icons = { @@ -38,7 +42,7 @@ class VideoOptions(QtWidgets.QTabWidget): def __init__(self, parent, app: FastFlixApp, available_audio_encoders): super().__init__(parent) - self.main = parent + self.main: "Main" = parent self.app = app self.reloading = False @@ -151,7 +155,7 @@ def change_conversion(self, conversion): # Page update does a reload which bases itself off the current encoder so we have to do audio formats after if not self.reloading: self.audio.allowed_formats(self._get_audio_formats(encoder)) - self.update_profile() + # self.update_profile() def get_settings(self): if not self.app.fastflix.current_video: @@ -247,15 +251,16 @@ def reload(self): 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 + audio_tracks = copy.deepcopy(self.app.fastflix.current_video.audio_tracks or []) + subtitle_tracks = copy.deepcopy(self.app.fastflix.current_video.subtitle_tracks or []) + attachment_tracks = copy.deepcopy(self.app.fastflix.current_video.attachment_tracks or []) try: if getattr(self.main.current_encoder, "enable_audio", False): self.audio.reload(audio_tracks, self.audio_formats) if getattr(self.main.current_encoder, "enable_subtitles", False): self.subtitles.reload(subtitle_tracks) if getattr(self.main.current_encoder, "enable_attachments", False): - self.attachments.reload_from_queue(streams, settings) + self.attachments.reload_from_queue(streams, attachment_tracks) self.advanced.reset(settings=settings) self.info.reset() except Exception: @@ -264,7 +269,7 @@ def reload(self): self.debug.reset() def clear_tracks(self): - self.current_settings.update_profile() + # self.current_settings.update_profile() self.audio.remove_all() self.subtitles.remove_all() self.attachments.clear_covers() diff --git a/fastflix/widgets/windows/audio_conversion.py b/fastflix/widgets/windows/audio_conversion.py new file mode 100644 index 00000000..e5ffdb0e --- /dev/null +++ b/fastflix/widgets/windows/audio_conversion.py @@ -0,0 +1,192 @@ +# -*- 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.models.fastflix_app import FastFlixApp +from fastflix.models.encode import AudioTrack + +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__ = ["AudioConversion"] + +logger = logging.getLogger("fastflix") + +# audio_disposition_options = [ +# "dub", +# "original", +# "comment", +# "visual_impaired", +# ] +# +# subtitle_disposition_options = [ +# "dub", +# "original", +# "comment", +# "lyrics", +# "karaoke", +# "hearing_impaired", +# ] + +channel_list = { + "mono": 1, + "stereo": 2, + "2.1": 3, + "3.0": 3, + "3.0(back)": 3, + "3.1": 4, + "4.0": 4, + "quad": 4, + "quad(side)": 4, + "5.0": 5, + "5.1": 6, + "6.0": 6, + "6.0(front)": 6, + "hexagonal": 6, + "6.1": 7, + "6.1(front)": 7, + "7.0": 7, + "7.0(front)": 7, + "7.1": 8, + "7.1(wide)": 8, +} + + +class AudioConversion(QtWidgets.QWidget): + def __init__(self, app: FastFlixApp, track_index, encoders): + super().__init__(None) + self.app = app + self.setWindowTitle(f"Audio Conversion for Track {track_index}") + self.setMinimumWidth(400) + self.audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[track_index] + + # Conversion + + self.conversion_codec = QtWidgets.QComboBox() + self.conversion_codec.addItems([t("None")] + list(sorted(encoders))) + + if self.audio_track.conversion_codec: + self.conversion_codec.setCurrentText(self.audio_track.conversion_codec) + self.conversion_codec.currentIndexChanged.connect(self.codec_changed) + + conversion_layout = QtWidgets.QHBoxLayout() + conversion_layout.addWidget(QtWidgets.QLabel(t("Codec"))) + conversion_layout.addWidget(self.conversion_codec, 2) + + # AQ vs Bitrate + + self.aq = QtWidgets.QComboBox() + self.aq.addItems( + [ + f"0 - {t('Near Lossless')}", + "1", + f"2 - {t('High Quality')}", + "3", + f"4 - {t('Medium Quality')}", + "5", + f"6 {t('Low Quality')}", + "7", + "8", + "9", + t("Custom Bitrate"), + ] + ) + self.aq.setMinimumWidth(100) + self.aq.currentIndexChanged.connect(self.set_aq) + self.bitrate = QtWidgets.QLineEdit() + self.bitrate.setFixedWidth(50) + + if self.audio_track.conversion_aq: + self.aq.setCurrentIndex(self.audio_track.conversion_aq) + self.bitrate.setDisabled(True) + elif self.audio_track.conversion_bitrate: + self.aq.setCurrentText(t("Custom Bitrate")) + self.bitrate.setText(self.audio_track.conversion_bitrate) + self.bitrate.setEnabled(True) + + elif self.conversion_codec.currentText() in ["libopus"]: + self.aq.setCurrentIndex(10) + else: + self.aq.setCurrentIndex(3) + + quality_layout = QtWidgets.QHBoxLayout() + quality_layout.addWidget(QtWidgets.QLabel(t("Audio Quality"))) + quality_layout.addWidget(self.aq, 1) + quality_layout.addWidget(QtWidgets.QLabel(t("Bitrate"))) + quality_layout.addWidget(self.bitrate) + quality_layout.addWidget(QtWidgets.QLabel("kb/s")) + + # Downmix + + self.downmix = QtWidgets.QComboBox() + self.downmix.addItems([t("None")] + list(channel_list.keys())) + self.downmix.setCurrentIndex(2) + if self.audio_track.downmix: + self.downmix.setCurrentText(self.audio_track.downmix) + + downmix_layout = QtWidgets.QHBoxLayout() + downmix_layout.addWidget(QtWidgets.QLabel(t("Channel Layout"))) + downmix_layout.addWidget(self.downmix, 2) + + # Yes No + + yes_no_layout = QtWidgets.QHBoxLayout() + cancel = QtWidgets.QPushButton(t("Cancel")) + cancel.clicked.connect(self.close) + yes_no_layout.addWidget(cancel) + save = QtWidgets.QPushButton(t("Save")) + save.clicked.connect(self.save) + yes_no_layout.addWidget(save) + + layout = QtWidgets.QVBoxLayout() + layout.addLayout(conversion_layout) + layout.addLayout(quality_layout) + layout.addLayout(downmix_layout) + layout.addLayout(yes_no_layout) + + self.setLayout(layout) + + def set_aq(self): + index = self.aq.currentIndex() + if index == 10: + self.bitrate.setEnabled(True) + else: + self.bitrate.setDisabled(True) + + def codec_changed(self): + if self.conversion_codec.currentText() in ["libopus"]: + self.aq.setCurrentIndex(10) + self.aq.setDisabled(True) + # self.bitrate.setEnabled(True) + else: + self.aq.setEnabled(True) + # self.bitrate.setDisabled(True) + + def save(self): + if self.conversion_codec.currentIndex() != 0: + self.audio_track.conversion_codec = self.conversion_codec.currentText() + else: + self.audio_track.conversion_codec = None + + if self.aq.currentIndex() != 10: + self.audio_track.conversion_aq = self.aq.currentIndex() + self.audio_track.conversion_bitrate = None + else: + self.audio_track.conversion_bitrate = self.bitrate.text() + self.audio_track.conversion_aq = None + + if self.downmix.currentIndex() != 0: + self.audio_track.downmix = self.downmix.currentText() + else: + self.audio_track.downmix = None + + self.close() diff --git a/fastflix/widgets/windows/disposition.py b/fastflix/widgets/windows/disposition.py index 0a6ff507..9ad0d9e5 100644 --- a/fastflix/widgets/windows/disposition.py +++ b/fastflix/widgets/windows/disposition.py @@ -13,6 +13,7 @@ from fastflix.encoders.common import helpers from fastflix.resources import get_icon from fastflix.language import t +from fastflix.models.fastflix_app import FastFlixApp __all__ = ["Disposition"] @@ -36,12 +37,15 @@ class Disposition(QtWidgets.QWidget): - def __init__(self, parent, track_name, subs=False): + def __init__(self, app: FastFlixApp, parent, track_name, track_index, audio=True): super().__init__(None) self.parent = parent + self.app = app self.track_name = track_name - self.subs = subs - self.dispositions = parent.dispositions + self.track_index = track_index + self.audio = audio + + self.setMinimumWidth(400) self.forced = QtWidgets.QCheckBox(t("Forced")) @@ -67,13 +71,13 @@ def __init__(self, parent, track_name, subs=False): group.addButton(none_extra) layout.addWidget(none_extra) - if subs: - for dis in subtitle_disposition_options: + if audio: + for dis in audio_disposition_options: self.widgets[dis] = QtWidgets.QRadioButton(t(dis)) group.addButton(self.widgets[dis]) layout.addWidget(self.widgets[dis]) else: - for dis in audio_disposition_options: + for dis in subtitle_disposition_options: self.widgets[dis] = QtWidgets.QRadioButton(t(dis)) group.addButton(self.widgets[dis]) layout.addWidget(self.widgets[dis]) @@ -85,23 +89,31 @@ def __init__(self, parent, track_name, subs=False): self.setLayout(layout) def set_dispositions(self): - self.parent.dispositions["forced"] = self.forced.isChecked() - self.parent.dispositions["default"] = self.default.isChecked() + if self.audio: + track = self.app.fastflix.current_video.audio_tracks[self.track_index] + else: + track = self.app.fastflix.current_video.subtitle_tracks[self.track_index] + + track.dispositions["forced"] = self.forced.isChecked() + track.dispositions["default"] = self.default.isChecked() for dis in self.widgets: - self.parent.dispositions[dis] = self.widgets[dis].isChecked() - self.parent.set_dis_button() + track.dispositions[dis] = self.widgets[dis].isChecked() self.parent.page_update() self.hide() def show(self): - self.forced.setChecked(self.parent.dispositions["forced"]) - self.default.setChecked(self.parent.dispositions["default"]) + if self.audio: + dispositions = self.app.fastflix.current_video.audio_tracks[self.track_index].dispositions + else: + dispositions = self.app.fastflix.current_video.subtitle_tracks[self.track_index].dispositions for dis in self.widgets: - self.widgets[dis].setChecked(self.parent.dispositions.get(dis, False)) + self.widgets[dis].setChecked(dispositions.get(dis, False)) super().show() def close(self) -> bool: del self.parent - del self.subs - del self.dispositions + del self.app + del self.track_name + del self.track_index + del self.audio return super().close() diff --git a/pyproject.toml b/pyproject.toml index 93defac4..a95df362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "coloredlogs>=15.0,<16.0", "iso639-lang==0.0.9", "mistune>=2.0,<3.0", + "packaging>=23.2", "pathvalidate>=2.4,<3.0", "psutil>=5.9,<6.0", "pydantic>=1.9,<2.0", diff --git a/tests/test_version_check.py b/tests/test_version_check.py index 0ad54442..fdb4223b 100644 --- a/tests/test_version_check.py +++ b/tests/test_version_check.py @@ -1,18 +1,17 @@ # -*- coding: utf-8 -*- from subprocess import run, PIPE import re -from distutils.version import StrictVersion +from packaging import version import requests -from box import Box def test_version(): with open("fastflix/version.py") as version_file: - code_version = StrictVersion(re.search(r"__version__ *= *['\"](.+)['\"]", version_file.read()).group(1)) + code_version = version.parse(re.search(r"__version__ *= *['\"](.+)['\"]", version_file.read()).group(1)) url = "https://api.github.com/repos/cdgriffith/FastFlix/releases/latest" data = requests.get(url).json() assert ( - StrictVersion(data["tag_name"]) < code_version - ), f"Last Release Version {StrictVersion(data['tag_name'])} vs Code Version {code_version}" + version.parse(data["tag_name"]) < code_version + ), f"Last Release Version {version.parse(data['tag_name'])} vs Code Version {code_version}" diff --git a/velocemente/__init__.py b/velocemente/__init__.py new file mode 100644 index 00000000..e69de29b From 9984a29b2672a9bba44135b2367a407b94462be3 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 16 Apr 2024 22:31:21 -0500 Subject: [PATCH 07/15] Add content warning --- .github/ISSUE_TEMPLATE/bug_report.md | 8 ++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 9 +++++++++ fastflix/encoders/common/audio.py | 4 +++- fastflix/ff_queue.py | 4 ++-- fastflix/widgets/background_tasks.py | 3 +++ fastflix/widgets/main.py | 7 +++++-- fastflix/widgets/panels/audio_panel.py | 5 ++++- fastflix/widgets/panels/subtitle_panel.py | 5 ++++- 8 files changed, 38 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8d70979e..4f939f34 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,6 +7,14 @@ assignees: '' --- + + + + + + + + **FastFlix Version:** **Operating System:** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bed7fa1e..0532410a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,6 +7,15 @@ assignees: '' --- + + + + + + + + + Please use Discussions for new Features! They can be voted upon, and that way I can work in the most demanded features first. Don't forget to first search for the feature you want, as well as upvote others you would like to see! diff --git a/fastflix/encoders/common/audio.py b/fastflix/encoders/common/audio.py index 207bb12d..b84421a9 100644 --- a/fastflix/encoders/common/audio.py +++ b/fastflix/encoders/common/audio.py @@ -13,6 +13,7 @@ "quad(side)": 4, "5.0": 5, "5.1": 6, + "5.1(side)": 6, "6.0": 6, "6.0(front)": 6, "hexagonal": 6, @@ -42,8 +43,9 @@ def build_audio(audio_tracks, audio_file_index=0): if not track.conversion_codec or track.conversion_codec == "none": command_list.append(f"-c:{track.outdex} copy") elif track.conversion_codec: + cl = track.downmix if "downmix" in track and track.downmix else track.raw_info.channel_layout downmix = ( - f"-ac:{track.outdex} {channel_list[track.downmix]} -filter:{track.outdex} aformat=channel_layouts={track.downmix}" + f"-ac:{track.outdex} {channel_list[cl]} -filter:{track.outdex} aformat=channel_layouts={cl}" if track.downmix else "" ) diff --git a/fastflix/ff_queue.py b/fastflix/ff_queue.py index 8ee9de73..18ab59a4 100644 --- a/fastflix/ff_queue.py +++ b/fastflix/ff_queue.py @@ -35,8 +35,8 @@ def get_queue(queue_file: Path) -> list[Video]: video["video_settings"]["output_path"] = Path(video["video_settings"]["output_path"]) encoder_settings = video["video_settings"]["video_encoder_settings"] ves = [x(**encoder_settings) for x in setting_types.values() if x().name == encoder_settings["name"]][0] - # TODO breaks - audio = [AudioTrack(**x) for x in video["audio_tracks"]] - # TODO breaks subtitles = [SubtitleTrack(**x) for x in video["subtitle_tracks"]] + audio = [AudioTrack(**x) for x in video["audio_tracks"]] + subtitles = [SubtitleTrack(**x) for x in video["subtitle_tracks"]] attachments = [] for x in video["attachment_tracks"]: try: diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py index 5acabfa9..85c9911d 100644 --- a/fastflix/widgets/background_tasks.py +++ b/fastflix/widgets/background_tasks.py @@ -32,6 +32,9 @@ def run(self): "Please use FFmpeg 4.3+ built against the latest zimg libraries. " "Static builds available at https://ffmpeg.org/download.html " ) + if "OpenCL mapping not usable" in result.stdout.decode(encoding="utf-8", errors="ignore"): + self.main.thread_logging_signal.emit("ERROR trying to use OpenCL for thumbnail generation") + self.main.thumbnail_complete.emit(2) else: self.main.thread_logging_signal.emit(f"ERROR:{t('Could not generate thumbnail')}: {result.stdout}") diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index 5675aa43..25731a10 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1680,10 +1680,13 @@ def thread_logger(text): logger.warning(text) @reusables.log_exception("fastflix", show_traceback=False) - def thumbnail_generated(self, success=False): - if not success or not self.thumb_file.exists(): + def thumbnail_generated(self, status=0): + if status == 0 or not status or not self.thumb_file.exists(): self.widgets.preview.setText(t("Error Updating Thumbnail")) return + if status == 2: + self.generate_thumbnail() + return pixmap = QtGui.QPixmap(str(self.thumb_file)) pixmap = pixmap.scaled(420, 260, QtCore.Qt.KeepAspectRatio) self.widgets.preview.setPixmap(pixmap) diff --git a/fastflix/widgets/panels/audio_panel.py b/fastflix/widgets/panels/audio_panel.py index 097f0ff6..ae75a567 100644 --- a/fastflix/widgets/panels/audio_panel.py +++ b/fastflix/widgets/panels/audio_panel.py @@ -208,7 +208,10 @@ def page_update(self): @property def enabled(self): - return self.app.fastflix.current_video.audio_tracks[self.index].enabled + try: + return self.app.fastflix.current_video.audio_tracks[self.index].enabled + except IndexError: + return False @property def language(self) -> str: diff --git a/fastflix/widgets/panels/subtitle_panel.py b/fastflix/widgets/panels/subtitle_panel.py index deb79fb2..f774e22c 100644 --- a/fastflix/widgets/panels/subtitle_panel.py +++ b/fastflix/widgets/panels/subtitle_panel.py @@ -202,7 +202,10 @@ def set_outdex(self, outdex): @property def enabled(self): - return self.app.fastflix.current_video.subtitle_tracks[self.index].enabled + try: + return self.app.fastflix.current_video.subtitle_tracks[self.index].enabled + except IndexError: + return False @property def language(self): From 4e2e444025fd8f2f991150863ed0353c6aacfbd1 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 17 Apr 2024 11:43:21 -0500 Subject: [PATCH 08/15] figure out windows platform --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 48ff4010..db164192 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,6 +41,7 @@ jobs: python -m pip install --upgrade pip setuptools pip install . pip install .[develop] + python -c "import platform;print(f'PLATFORM PLATFORM {platform.platform()}')" - name: Run tests env: From fca27d43a8f026debfa008f18968430bfeb0b051 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Wed, 17 Apr 2024 11:45:29 -0500 Subject: [PATCH 09/15] figure out windows platform --- .github/workflows/build.yaml | 1 + .github/workflows/test.yaml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f48cb765..96cb3d93 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -91,6 +91,7 @@ jobs: python -m pip install --upgrade pip setuptools --ignore-installed pip install . pip install .[develop] + python -c "import platform;print(f'PLATFORM PLATFORM {platform.platform()}')" - name: Grab iso-639 lists diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index db164192..48ff4010 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,7 +41,6 @@ jobs: python -m pip install --upgrade pip setuptools pip install . pip install .[develop] - python -c "import platform;print(f'PLATFORM PLATFORM {platform.platform()}')" - name: Run tests env: From 2465b363a40c841393f6b376575d65e276fb42c8 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 28 May 2024 21:40:35 -0500 Subject: [PATCH 10/15] figure out windows platform version --- .github/workflows/build.yaml | 1 - fastflix/entry.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 96cb3d93..f48cb765 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -91,7 +91,6 @@ jobs: python -m pip install --upgrade pip setuptools --ignore-installed pip install . pip install .[develop] - python -c "import platform;print(f'PLATFORM PLATFORM {platform.platform()}')" - name: Grab iso-639 lists diff --git a/fastflix/entry.py b/fastflix/entry.py index d89d8dfb..12247893 100644 --- a/fastflix/entry.py +++ b/fastflix/entry.py @@ -122,8 +122,15 @@ def main(portable_mode=False): import platform try: - win_ver = int(platform.platform().lower().split("-")[1]) - except Exception: + windows_version_string = platform.platform().lower().split("-")[1] + if "server" in windows_version_string: + # Windows-2022Server-10.0.20348-SP0 + server_version = int(windows_version_string[:4]) + win_ver = 0 if server_version < 2016 else 10 + else: + win_ver = int(windows_version_string) + except Exception as error: + print(f"COULD NOT DETERMINE WINDOWS VERSION FROM: {platform.platform()} - {error}") win_ver = 0 if win_ver < 10: input( From 8aef267b62bdd5c8145aa7ca4e0446367951172a Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Tue, 28 May 2024 22:33:48 -0500 Subject: [PATCH 11/15] Audio queue fixes and button highlighting --- fastflix/ff_queue.py | 6 ------ fastflix/widgets/panels/audio_panel.py | 15 ++++++++++++++- fastflix/widgets/windows/audio_conversion.py | 19 ++++++------------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/fastflix/ff_queue.py b/fastflix/ff_queue.py index 18ab59a4..88914736 100644 --- a/fastflix/ff_queue.py +++ b/fastflix/ff_queue.py @@ -50,17 +50,11 @@ def get_queue(queue_file: Path) -> list[Video]: crop = None if video["video_settings"]["crop"]: crop = Crop(**video["video_settings"]["crop"]) - del video["audio_tracks"] - del video["subtitle_tracks"] - del video["attachment_tracks"] del video["video_settings"]["video_encoder_settings"] del video["status"] del video["video_settings"]["crop"] vs = VideoSettings( **video["video_settings"], - audio_tracks=audio, - subtitle_tracks=subtitles, - attachment_tracks=attachments, crop=crop, ) vs.video_encoder_settings = ves # No idea why this has to be called after, otherwise reset to x265 diff --git a/fastflix/widgets/panels/audio_panel.py b/fastflix/widgets/panels/audio_panel.py index ae75a567..a052697f 100644 --- a/fastflix/widgets/panels/audio_panel.py +++ b/fastflix/widgets/panels/audio_panel.py @@ -166,7 +166,10 @@ def show_conversions(self): pass self.conversion_box = AudioConversion( - self.app, track_index=self.index, encoders=self.app.fastflix.audio_encoders + self.app, + track_index=self.index, + encoders=self.app.fastflix.audio_encoders, + audio_track_update=self.page_update, ) self.conversion_box.show() @@ -204,6 +207,7 @@ def update_enable(self): def page_update(self): if not self.loading: + self.check_conversion_button() return self.parent.main.page_update(build_thumbnail=False) @property @@ -279,6 +283,15 @@ def update_track(self, conversion=None, bitrate=None, downmix=None): audio_track.downmix = downmix self.page_update() + def check_conversion_button(self): + audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[self.index] + if audio_track.conversion_codec: + self.widgets.conversion.setStyleSheet("border-color: #0055ff") + self.widgets.conversion.setText(t("Conversion") + f": {audio_track.conversion_codec}") + else: + self.widgets.conversion.setStyleSheet("") + self.widgets.conversion.setText(t("Conversion")) + class AudioList(FlixList): def __init__(self, parent, app: FastFlixApp): diff --git a/fastflix/widgets/windows/audio_conversion.py b/fastflix/widgets/windows/audio_conversion.py index e5ffdb0e..b2a7869d 100644 --- a/fastflix/widgets/windows/audio_conversion.py +++ b/fastflix/widgets/windows/audio_conversion.py @@ -1,20 +1,12 @@ # -*- 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 PySide6 import QtWidgets from fastflix.models.fastflix_app import FastFlixApp from fastflix.models.encode import AudioTrack -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__ = ["AudioConversion"] @@ -62,9 +54,10 @@ class AudioConversion(QtWidgets.QWidget): - def __init__(self, app: FastFlixApp, track_index, encoders): + def __init__(self, app: FastFlixApp, track_index, encoders, audio_track_update): super().__init__(None) self.app = app + self.audio_track_update = audio_track_update self.setWindowTitle(f"Audio Conversion for Track {track_index}") self.setMinimumWidth(400) self.audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[track_index] @@ -175,7 +168,7 @@ def save(self): if self.conversion_codec.currentIndex() != 0: self.audio_track.conversion_codec = self.conversion_codec.currentText() else: - self.audio_track.conversion_codec = None + self.audio_track.conversion_codec = "" if self.aq.currentIndex() != 10: self.audio_track.conversion_aq = self.aq.currentIndex() @@ -188,5 +181,5 @@ def save(self): self.audio_track.downmix = self.downmix.currentText() else: self.audio_track.downmix = None - + self.audio_track_update() self.close() From 26abedb5d2609d348ccccd52fb31a47e158d5272 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sun, 16 Jun 2024 10:09:19 -0500 Subject: [PATCH 12/15] Fixing header bar --- FastFlix.nsi | 2 +- fastflix/widgets/container.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FastFlix.nsi b/FastFlix.nsi index ad5c0c1d..02fff751 100644 --- a/FastFlix.nsi +++ b/FastFlix.nsi @@ -48,7 +48,7 @@ InstallDirRegKey HKLM "Software\FastFlix" "Install_Dir" Function .onInit ReadRegStr $0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\FastFlix" "UninstallString" ${If} $0 != "" - Messagebox MB_OK|MB_ICONINFORMATION "You will now be prompted to first uninstall the previous version of FastFlix" + Messagebox MB_OK|MB_ICONINFORMATION "You will now be prompted to first uninstall the previous version of FastFlix. Please ensure it is not currently running!" ExecWait '$0 _?=$INSTDIR' ${EndIf} FunctionEnd diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py index 9198701a..160c495f 100644 --- a/fastflix/widgets/container.py +++ b/fastflix/widgets/container.py @@ -163,7 +163,7 @@ def si(self, widget): def init_menu(self): menubar = self.menuBar() menubar.setNativeMenuBar(False) - menubar.setFixedWidth(260) + menubar.setFixedWidth(360) menubar.setStyleSheet("font-size: 14px") file_menu = menubar.addMenu(t("File")) From 1eeae72ad53ed1391579c7c2321690de4a9b06f0 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sun, 16 Jun 2024 12:37:08 -0500 Subject: [PATCH 13/15] Fix pyinstaller hidden imports --- FastFlix_Nix_OneFile.spec | 5 +++++ FastFlix_Windows_Installer.spec | 5 +++++ FastFlix_Windows_OneFile.spec | 5 +++++ pyproject.toml | 2 +- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/FastFlix_Nix_OneFile.spec b/FastFlix_Nix_OneFile.spec index 7bbbbd3e..fb2141b8 100644 --- a/FastFlix_Nix_OneFile.spec +++ b/FastFlix_Nix_OneFile.spec @@ -19,6 +19,11 @@ with open("pyproject.toml") as f: if package not in ("pyinstaller"): all_imports.append(package) +all_imports.remove("iso639-lang") +all_imports.remove("python-box") +all_imports.append("box") +all_imports.append("iso639") + a = Analysis(['fastflix/__main__.py'], binaries=[], datas=[('iso-639-3.tab', 'iso639'), ('iso-639-3.json', 'iso639'), ('CHANGES', 'fastflix/.'), ('docs/build-licenses.txt', 'docs')] + all_fastflix_files, diff --git a/FastFlix_Windows_Installer.spec b/FastFlix_Windows_Installer.spec index 0eaca049..5a1fa36c 100644 --- a/FastFlix_Windows_Installer.spec +++ b/FastFlix_Windows_Installer.spec @@ -19,6 +19,11 @@ with open("pyproject.toml") as f: if package not in ("pyinstaller"): all_imports.append(package) +all_imports.remove("iso639-lang") +all_imports.remove("python-box") +all_imports.append("box") +all_imports.append("iso639") + a = Analysis(['fastflix\\__main__.py'], binaries=[], datas=[('iso-639-3.tab', 'iso639'), ('iso-639-3.json', 'iso639'), ('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files, diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index 92a42bf9..6841db66 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -16,6 +16,11 @@ for root, dirs, files in os.walk('fastflix'): all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys', 'typing_extensions', 'box'] +all_imports.remove("iso639-lang") +all_imports.remove("python-box") +all_imports.append("box") +all_imports.append("iso639") + with open("pyproject.toml") as f: for line in toml.load(f)["project"]["dependencies"]: package = line.split("[")[0].split("=")[0].split(">")[0].split("<")[0].replace('"', '').replace("'",'').rstrip("~").strip() diff --git a/pyproject.toml b/pyproject.toml index a95df362..c92c2539 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ develop = [ "wheel>=0.38.4", "typing_extensions>=4.4", - "pyinstaller>=5.7", + "pyinstaller>=6.8", "pytest>=7.3", "types-requests>=2.28", "types-setuptools>=65.7", From 8eac73b29e32c71b42ba34c099549c6fd19765ab Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sun, 16 Jun 2024 12:46:12 -0500 Subject: [PATCH 14/15] Fix pyinstaller hidden imports for onefile --- FastFlix_Windows_OneFile.spec | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index 6841db66..1569b0d6 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -15,6 +15,11 @@ for root, dirs, files in os.walk('fastflix'): all_fastflix_files.append((os.path.join(root,file), root)) all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys', 'typing_extensions', 'box'] +with open("pyproject.toml") as f: + for line in toml.load(f)["project"]["dependencies"]: + package = line.split("[")[0].split("=")[0].split(">")[0].split("<")[0].replace('"', '').replace("'",'').rstrip("~").strip() + if package not in ("pyinstaller"): + all_imports.append(package) all_imports.remove("iso639-lang") all_imports.remove("python-box") From 67119e749aa64bbf2e2e3e1d1a69f1936e37f5c0 Mon Sep 17 00:00:00 2001 From: Chris Griffith Date: Sun, 16 Jun 2024 12:56:36 -0500 Subject: [PATCH 15/15] Fix pyinstaller hidden imports for onefile --- FastFlix_Windows_OneFile.spec | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec index 1569b0d6..89c61a7c 100644 --- a/FastFlix_Windows_OneFile.spec +++ b/FastFlix_Windows_OneFile.spec @@ -15,6 +15,7 @@ for root, dirs, files in os.walk('fastflix'): all_fastflix_files.append((os.path.join(root,file), root)) all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys', 'typing_extensions', 'box'] + with open("pyproject.toml") as f: for line in toml.load(f)["project"]["dependencies"]: package = line.split("[")[0].split("=")[0].split(">")[0].split("<")[0].replace('"', '').replace("'",'').rstrip("~").strip() @@ -26,12 +27,6 @@ all_imports.remove("python-box") all_imports.append("box") all_imports.append("iso639") -with open("pyproject.toml") as f: - for line in toml.load(f)["project"]["dependencies"]: - package = line.split("[")[0].split("=")[0].split(">")[0].split("<")[0].replace('"', '').replace("'",'').rstrip("~").strip() - if package not in ("pyinstaller"): - all_imports.append(package) - portable_file = "fastflix\\portable.py" with open(portable_file, "w") as portable: portable.write(" ")