diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 9179b6dd..622e07c7 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -9,13 +9,13 @@ jobs:
build-nix:
strategy:
matrix:
- os: [ ubuntu-20.04, ubuntu-22.04, macos-12 ]
+ os: [ ubuntu-20.04, ubuntu-22.04, macos-12, macos-14 ]
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.12"
@@ -23,7 +23,8 @@ jobs:
run: |
mkdir dist
echo "::set-env name=VERSION::$(python scripts/get_version.py)"
- echo "Building branch ${{env.GITHUB_REF}} - version ${{env.VERSION}}"
+ echo "::set-env name=ARCH::$(python scripts/get_arch.py)"
+ echo Building branch ${{ env.GITHUB_REF }} - version ${{ env.VERSION }} - on ${{ env.ARCH }}
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
@@ -57,13 +58,25 @@ jobs:
dist/FastFlix --version
dist/FastFlix --test
- - name: Upload standalone executable artifact
- uses: actions/upload-artifact@v3
+ - name: Archive excutables
+ run: |
+ pushd dist
+ 7z a -mm=Deflate -mfb=258 -mpass=15 FastFlix_${{ env.VERSION }}_${{ matrix.os }}_${{ env.ARCH }}.zip *
+ popd
+
+ - name: Build Mac App
+ if : ${{ startsWith(matrix.os, 'macos') }}
+ run: |
+ python scripts/build_mac_app.py ${{ matrix.os }}
+ pushd dist
+ 7z a -mm=Deflate -mfb=258 -mpass=15 FastFlix_${{ env.VERSION }}_appbundle_${{ matrix.os }}_${{ env.ARCH }}.zip FastFlix.app
+ popd
+
+ - name: Upload executable artifacts
+ uses: actions/upload-artifact@v4
with:
- name: FastFlix_${{ env.VERSION }}_${{ matrix.os }}_x86_64
- path: |
- dist/FastFlix
- dist/LICENSE
+ name: FastFlix_${{ env.VERSION }}_OUTER_DO_NOT_UPLOAD_${{ matrix.os }}_${{ env.ARCH }}
+ path: dist/*.zip
build-windows-2022:
@@ -71,8 +84,8 @@ jobs:
runs-on: windows-2022
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.12"
@@ -145,7 +158,7 @@ jobs:
move docs\build-licenses.txt LICENSE
- name: Upload standalone executable artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: FastFlix_${{ env.VERSION }}_win64
path: |
diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml
index de732a22..70b34cdb 100644
--- a/.github/workflows/pythonpublish.yml
+++ b/.github/workflows/pythonpublish.yml
@@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: '3.12'
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 48ff4010..40c125b3 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -12,22 +12,22 @@ jobs:
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- - uses: actions/setup-python@v3
+ - uses: actions/setup-python@v5
with:
python-version: "3.12"
- - run: pip install black==23.7.0
+ - run: pip install black==24.8.0
- run: python -m black --check .
test:
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- - uses: actions/setup-python@v3
+ - uses: actions/setup-python@v5
with:
python-version: "3.12"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8adf78e1..00f777a1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
+ rev: v4.6.0
hooks:
- id: mixed-line-ending
- id: trailing-whitespace
@@ -12,14 +12,18 @@ repos:
- id: check-byte-order-marker
- id: debug-statements
- id: check-added-large-files
- exclude: tests/media/.+
+ exclude: |
+ (?x)^(
+ tests/media/.+|
+ ^fastflix/data/icon.icns
+ )$
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-toml
- id: detect-private-key
- id: end-of-file-fixer
- repo: https://github.com/psf/black
- rev: 23.7.0
+ rev: 24.8.0
hooks:
- id: black
# - repo: https://github.com/pre-commit/mirrors-mypy
diff --git a/CHANGES b/CHANGES
index 77195c9e..fddb3de9 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,5 +1,23 @@
# Changelog
+## Version 5.8.0
+
+* Adding #283 support for experimental DTS (dca) audio by adding -strict -2 (thanks to Sub7)
+* Adding #354 M1 support (thanks to Nhunz and Anton)
+* Adding #536 Improve Profiles - save advanced options (thanks to CelticTaonga and DCNerds)
+* Adding #568 center app on startup (thanks to Viet-Duc Le)
+* Adding #587 Distribute a mac app bundle (thanks to Ivan Novokhatski)
+* Adding #589 support for pydantic 2.x (thanks to dmo marillat)
+* Adding #592 Add alpha channel for VP9 (thjanks to subof)
+* Fixing #185 audio channels not being set properly and resetting on encoder change (thanks to Tupsi)
+* Fixing #522 add file fails - fixed as of 5.7.0 (thanks to pcl5x2008)
+* Fixing #531 list limitation in readme that FFmpeg must support the software encoders listed (thanks to brunoais)
+* Fixing #567 Profiles for WebP did not work (nor GIF dither) (thanks to jpert)
+* Fixing #582 BT.2020-10 Color transfer not maintained (thanks to Ryushin)
+* Fixing #585 error when trying to return a video from queue that has the video track after audio or subtitiles (thanks to Hankuu)
+* Fixing #586 audio channels being set incorrectly (thanks to Hankuu)
+* Fixing #588 audio and subtitle dispositions were not set from source (thanks to GeZorTenPlotZ)
+
## Version 5.7.4
* Fixing #579 Missing Infos and no Mouse-Over info in Subs-Panel since 5.7 (thanks to GeZorTenPlotZ)
diff --git a/FastFlix_Nix_OneFile.spec b/FastFlix_Nix_OneFile.spec
index fb2141b8..40d1038a 100644
--- a/FastFlix_Nix_OneFile.spec
+++ b/FastFlix_Nix_OneFile.spec
@@ -1,6 +1,8 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_submodules
import toml
+import os
+import platform
block_cipher = None
@@ -37,6 +39,7 @@ a = Analysis(['fastflix/__main__.py'],
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
+
exe = EXE(pyz,
a.scripts,
a.binaries,
@@ -50,4 +53,7 @@ exe = EXE(pyz,
upx=False,
upx_exclude=[],
runtime_tmpdir=None,
- console=True , icon='fastflix/data/icon.ico')
+ target_arch='arm64' if 'arm64' in platform.platform() else 'x86_64',
+ console=True,
+ icon='fastflix/data/icon.ico'
+ )
diff --git a/README.md b/README.md
index 3a1bd272..b70b4bcb 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,8 @@ Check out [the FastFlix github wiki](https://github.com/cdgriffith/FastFlix/wiki
| Covers | ✓ | ✓ | ✓ | ✓ | ✓ | | ✓ |
| bt.2020 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
+If one of the above software encoders is not listed, it is due to your version of FFmpeg not having that encoder compiled in.
+
## Hardware Encoders
These will require the appropriate hardware. Nvidia GPU for NVEnc, Intel GPU/CPU for QSVEnc, and AMD GPU for VCEEnc.
diff --git a/fastflix/application.py b/fastflix/application.py
index ef670394..7757489b 100644
--- a/fastflix/application.py
+++ b/fastflix/application.py
@@ -233,6 +233,8 @@ def app_setup(
container = Container(app)
container.show()
+ container.move(QtGui.QGuiApplication.primaryScreen().availableGeometry().center() - container.rect().center())
+
if not app.fastflix.config.disable_version_check:
latest_fastflix(app=app, show_new_dialog=False)
diff --git a/fastflix/command_runner.py b/fastflix/command_runner.py
index 42668149..9a2f457f 100644
--- a/fastflix/command_runner.py
+++ b/fastflix/command_runner.py
@@ -110,9 +110,10 @@ def change_priority(
logger.exception(f"Could not set process priority to {new_priority}")
def read_output(self):
- with open(self.output_file, "r", encoding="utf-8", errors="ignore") as out_file, open(
- self.error_output_file, "r", encoding="utf-8", errors="ignore"
- ) as err_file:
+ with (
+ open(self.output_file, "r", encoding="utf-8", errors="ignore") as out_file,
+ open(self.error_output_file, "r", encoding="utf-8", errors="ignore") as err_file,
+ ):
while True:
time.sleep(0.01)
if not self.is_alive():
diff --git a/fastflix/data/Info.plist.template b/fastflix/data/Info.plist.template
new file mode 100644
index 00000000..ee7ddce9
--- /dev/null
+++ b/fastflix/data/Info.plist.template
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ FastFlix
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleIconFile
+ icon.icns
+ CFBundleIdentifier
+ com.github.cdgriffith.FastFlix
+ CFBundlePackageType
+ APPL
+ CFBundleSignature
+ PURE
+ CFBundleVersion
+ {version}
+ CFBundleName
+ FastFlix
+ CSResourcesFileMapped
+
+ NSHighResolutionCapable
+
+ NSDisablePersistence
+
+ LSMinimumSystemVersion
+ {mac_version}
+
+
diff --git a/fastflix/data/icon.icns b/fastflix/data/icon.icns
new file mode 100644
index 00000000..b2c14858
Binary files /dev/null and b/fastflix/data/icon.icns differ
diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml
index dd138523..ca305847 100644
--- a/fastflix/data/languages.yaml
+++ b/fastflix/data/languages.yaml
@@ -8696,51 +8696,6 @@ Bitrate Mode:
ukr: Режим бітрейту
kor: 비트레이트 모드
ron: Mod Bitrate
-VCEEncC AV1 Encoder is untested!:
- eng: VCEEncC AV1 Encoder is untested!
- deu: VCEEncC AV1 Encoder ist ungetestet!
- fra: VCEEncC AV1 Encoder n'est pas testé !
- ita: VCEEncC AV1 Encoder non è stato testato!
- spa: El codificador VCEEncC AV1 no ha sido probado.
- chs: VCEEncC AV1编码器未经测试!
- jpn: VCEEncC AV1エンコーダは未検証です!
- rus: VCEEncC AV1 Encoder не тестировался!
- por: O codificador VCEEncC AV1 não foi testado!
- swe: VCEEncC AV1 Encoder är otestad!
- pol: VCEEncC AV1 Encoder nie jest testowany!
- ukr: Кодер VCEEncC AV1 неперевірений!
- kor: VCEEncC AV1 인코더는 테스트되지 않았습니다!
- ron: VCEEncC AV1 Encoder nu a fost testat!
-QSVEncC AV1 Encoder is untested!:
- eng: QSVEncC AV1 Encoder is untested!
- deu: QSVEncC AV1 Encoder ist ungetestet!
- fra: QSVEncC AV1 Encoder n'est pas testé !
- ita: Il codificatore QSVEncC AV1 non è stato testato!
- spa: El codificador QSVEncC AV1 no ha sido probado.
- chs: QSVEncC AV1编码器未经测试!
- jpn: QSVEncC AV1 Encoderは未検証です。
- rus: Кодировщик QSVEncC AV1 не тестировался!
- por: O codificador QSVEncC AV1 não foi testado!
- swe: QSVEncC AV1 Encoder är otestad!
- pol: QSVEncC AV1 Encoder nie jest testowany!
- ukr: Кодер QSVEncC AV1 неперевірений!
- kor: QSVEncC AV1 인코더는 테스트되지 않았습니다!
- ron: QSVEncC AV1 Encoder nu a fost testat!
-NVEncC AV1 Encoder is untested!:
- eng: NVEncC AV1 Encoder is untested!
- deu: NVEncC AV1 Encoder ist ungetestet!
- fra: L'encodeur NVEncC AV1 n'est pas testé !
- ita: Il codificatore NVEncC AV1 non è stato testato!
- spa: El codificador NVEncC AV1 no ha sido probado.
- chs: NVEncC AV1编码器未经测试!
- jpn: NVEncC AV1 Encoderは未検証です。
- rus: NVEncC AV1 Encoder не тестировался!
- por: O codificador NVEncC AV1 não foi testado!
- swe: NVEncC AV1 Encoder är otestad!
- pol: NVEncC AV1 Encoder nie jest testowany!
- ukr: Кодер NVEncC AV1 неперевірений!
- kor: NVEncC AV1 인코더는 테스트되지 않았습니다!
- ron: NVEncC AV1 Encoder nu este testat!
Load Directory:
eng: Load Directory
deu: Verzeichnis laden
@@ -10730,3 +10685,18 @@ Subtitle Type:
ukr: Тип субтитрів
kor: 자막 유형
ron: Tip subtitrare
+Pattern Match:
+ eng: Pattern Match
+ deu: Mustervergleich
+ fra: Correspondance des motifs
+ ita: Corrispondenza dei modelli
+ spa: Coincidencia de patrones
+ jpn: パターン・マッチ
+ rus: Соответствие шаблону
+ por: Correspondência de padrões
+ swe: Mönstermatchning
+ pol: Dopasowanie wzorca
+ chs: 模式匹配
+ ukr: Збіг за зразком
+ kor: 패턴 일치
+ ron: Potrivire model
diff --git a/fastflix/encoders/common/audio.py b/fastflix/encoders/common/audio.py
index 9efc374c..2b48f7e5 100644
--- a/fastflix/encoders/common/audio.py
+++ b/fastflix/encoders/common/audio.py
@@ -1,5 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
+import logging
+
+logger = logging.getLogger("fastflix")
channel_list = {
"mono": 1,
@@ -67,13 +70,12 @@ def build_audio(audio_tracks, audio_file_index=0):
cl = track.downmix if track.downmix and track.downmix != "No Downmix" else track.raw_info.channel_layout
except (AssertionError, KeyError):
cl = "stereo"
+ logger.warning("Could not determine channel layout, defaulting to stereo, please manually specify")
downmix = (
- f"-ac:{track.outdex} {channel_list[cl]} -filter:{track.outdex} aformat=channel_layouts={cl}"
- if track.downmix and track.downmix != "No Downmix"
- else ""
+ f"-ac:{track.outdex} {channel_list[cl]}" if track.downmix and track.downmix != "No Downmix" else ""
)
- channel_layout = f'-filter:{track.outdex} aformat=channel_layouts="{channel_list[cl]}"'
+ channel_layout = f'-filter:{track.outdex} "aformat=channel_layouts={cl}"'
bitrate = ""
if track.conversion_codec not in lossless:
@@ -84,13 +86,13 @@ def build_audio(audio_tracks, audio_file_index=0):
else f"{track.conversion_bitrate}k"
)
- bitrate = f"-b:{track.outdex} {conversion_bitrate} {channel_layout}"
+ bitrate = f"-b:{track.outdex} {conversion_bitrate}"
else:
bitrate = audio_quality_converter(
track.conversion_aq, track.conversion_codec, track.raw_info.get("channels"), track.outdex
)
- command_list.append(f"-c:{track.outdex} {track.conversion_codec} {bitrate} {downmix}")
+ command_list.append(f"-c:{track.outdex} {track.conversion_codec} {bitrate} {downmix} {channel_layout}")
if getattr(track, "dispositions", None):
added = ""
@@ -103,6 +105,6 @@ def build_audio(audio_tracks, audio_file_index=0):
command_list.append(f"-disposition:{track.outdex} 0")
end_command = " ".join(command_list)
- if " truehd " or " opus " in end_command:
+ if " truehd " in end_command or " opus " in end_command or " dca " in end_command:
end_command += " -strict -2 "
return end_command
diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py
index 637fad8e..9db349f4 100644
--- a/fastflix/encoders/common/helpers.py
+++ b/fastflix/encoders/common/helpers.py
@@ -19,7 +19,7 @@
class Command(BaseModel):
command: str
- item = "command"
+ item: str = "command"
name: str = ""
exe: str = None
shell: bool = False
@@ -269,7 +269,7 @@ def generate_all(
filters = None
if not disable_filters:
- filter_details = fastflix.current_video.video_settings.dict().copy()
+ filter_details = fastflix.current_video.video_settings.model_dump().copy()
filter_details.update(filters_extra)
filters = generate_filters(
source=fastflix.current_video.source,
@@ -287,7 +287,7 @@ def generate_all(
cover=attachments,
output_video=fastflix.current_video.video_settings.output_path,
disable_rotate_metadata=encoder == "copy",
- **fastflix.current_video.video_settings.dict(),
+ **fastflix.current_video.video_settings.model_dump(),
)
beginning = generate_ffmpeg_start(
@@ -299,8 +299,8 @@ def generate_all(
enable_opencl=enable_opencl,
ffmpeg_version=fastflix.ffmpeg_version,
start_extra=start_extra,
- **fastflix.current_video.video_settings.dict(),
- **settings.dict(),
+ **fastflix.current_video.video_settings.model_dump(),
+ **settings.model_dump(),
)
return beginning, ending, output_fps
diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py
index 44abd27e..99cbd81c 100644
--- a/fastflix/encoders/common/setting_panel.py
+++ b/fastflix/encoders/common/setting_panel.py
@@ -4,14 +4,14 @@
from pathlib import Path
from box import Box
-from PySide6 import QtGui, QtWidgets, QtCore
+from PySide6 import QtGui, QtWidgets
from fastflix.exceptions import FastFlixInternalException
from fastflix.language import t
from fastflix.models.fastflix_app import FastFlixApp
from fastflix.widgets.background_tasks import ExtractHDR10
from fastflix.resources import group_box_style, get_icon
-from fastflix.shared import clear_list
+
logger = logging.getLogger("fastflix")
@@ -96,7 +96,7 @@ def translate_tip(tooltip):
def determine_default(self, widget_name, opt, items: List, raise_error: bool = False):
if widget_name == "pix_fmt":
items = [x.split(":")[1].strip() for x in items]
- elif widget_name in ("crf", "qp"):
+ elif widget_name in ("crf", "qp", "qscale"):
if not opt:
return 6
opt = str(opt)
@@ -152,6 +152,8 @@ def _add_combo_box(
widget_name, self.app.fastflix.config.encoder_opt(self.profile_name, opt), options
)
self.opts[widget_name] = opt
+ else:
+ logger.warning("No opt provided for widget %s %s", self.__class__.__name__, widget_name)
self.widgets[widget_name].setCurrentIndex(default or 0)
self.widgets[widget_name].setDisabled(not enabled)
new_width = self.widgets[widget_name].minimumSizeHint().width() + 20
@@ -203,6 +205,9 @@ def _add_text_box(
if opt:
default = str(self.app.fastflix.config.encoder_opt(self.profile_name, opt)) or default
self.opts[widget_name] = opt
+ else:
+ logger.warning("No opt provided for widget %s %s", self.__class__.__name__, widget_name)
+
self.widgets[widget_name].setText(default)
self.widgets[widget_name].setDisabled(not enabled)
if tooltip:
@@ -340,6 +345,7 @@ def _add_modes(
add_qp=True,
disable_custom_qp=False,
show_bitrate_passes=False,
+ disable_bitrate=False,
):
self.recommended_bitrates = recommended_bitrates
self.recommended_qps = recommended_qps
@@ -353,54 +359,55 @@ def _add_modes(
bitrate_box_layout = QtWidgets.QHBoxLayout()
self.widgets.mode = QtWidgets.QButtonGroup()
self.widgets.mode.buttonClicked.connect(self.set_mode)
-
- self.bitrate_radio = QtWidgets.QRadioButton("Bitrate")
- self.bitrate_radio.setFixedWidth(80)
- self.widgets.mode.addButton(self.bitrate_radio)
- 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:
- default_bitrate_index = self.determine_default(
- "bitrate", config_opt, recommended_bitrates, raise_error=True
- )
- except FastFlixInternalException:
- custom_bitrate = True
- self.widgets.bitrate.setCurrentText("Custom")
- else:
- self.widgets.bitrate.setCurrentIndex(default_bitrate_index)
- self.widgets.bitrate.currentIndexChanged.connect(lambda: self.mode_update())
- self.widgets.custom_bitrate = QtWidgets.QLineEdit("3000" if not custom_bitrate else config_opt)
- self.widgets.custom_bitrate.setFixedWidth(100)
- self.widgets.custom_bitrate.setEnabled(custom_bitrate)
- self.widgets.custom_bitrate.textChanged.connect(lambda: self.main.build_commands())
- self.widgets.custom_bitrate.setValidator(self.only_int)
- 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(t("Custom") + ":"))
- bitrate_box_layout.addWidget(self.widgets.custom_bitrate)
- bitrate_box_layout.addWidget(QtWidgets.QLabel("k"))
-
qp_help = (
f"{qp_name.upper()} {t('is extremely source dependant')},\n"
f"{t('the resolution-to-')}{qp_name.upper()}{t('are mere suggestions!')}"
)
- self.qp_radio = QtWidgets.QRadioButton(qp_name.upper())
- self.qp_radio.setChecked(True)
- self.qp_radio.setFixedWidth(80)
- self.qp_radio.setToolTip(qp_help)
- self.widgets.mode.addButton(self.qp_radio)
+ config_opt = None
+ if not disable_bitrate:
+ self.bitrate_radio = QtWidgets.QRadioButton("Bitrate")
+ self.bitrate_radio.setFixedWidth(80)
+ self.widgets.mode.addButton(self.bitrate_radio)
+ self.widgets.bitrate = QtWidgets.QComboBox()
+ 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:
+ default_bitrate_index = self.determine_default(
+ "bitrate", config_opt, recommended_bitrates, raise_error=True
+ )
+ except FastFlixInternalException:
+ custom_bitrate = True
+ self.widgets.bitrate.setCurrentText("Custom")
+ else:
+ self.widgets.bitrate.setCurrentIndex(default_bitrate_index)
+ self.widgets.bitrate.currentIndexChanged.connect(lambda: self.mode_update())
+ self.widgets.custom_bitrate = QtWidgets.QLineEdit("3000" if not custom_bitrate else config_opt)
+ self.widgets.custom_bitrate.setValidator(QtGui.QDoubleValidator())
+ self.widgets.custom_bitrate.setFixedWidth(100)
+ self.widgets.custom_bitrate.setEnabled(custom_bitrate)
+ self.widgets.custom_bitrate.textChanged.connect(lambda: self.main.build_commands())
+ self.widgets.custom_bitrate.setValidator(self.only_int)
+ 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(t("Custom") + ":"))
+ bitrate_box_layout.addWidget(self.widgets.custom_bitrate)
+ bitrate_box_layout.addWidget(QtWidgets.QLabel("k"))
+
+ self.qp_radio = QtWidgets.QRadioButton(qp_name.upper())
+ self.qp_radio.setChecked(True)
+ self.qp_radio.setFixedWidth(80)
+ self.qp_radio.setToolTip(qp_help)
+ self.widgets.mode.addButton(self.qp_radio)
self.widgets[qp_name] = QtWidgets.QComboBox()
self.widgets[qp_name].setToolTip(qp_help)
@@ -421,14 +428,16 @@ def _add_modes(
if not disable_custom_qp:
self.widgets[f"custom_{qp_name}"] = QtWidgets.QLineEdit("30" if not custom_qp else str(qp_value))
self.widgets[f"custom_{qp_name}"].setFixedWidth(100)
+ self.widgets[f"custom_{qp_name}"].setValidator(QtGui.QDoubleValidator())
self.widgets[f"custom_{qp_name}"].setEnabled(custom_qp)
self.widgets[f"custom_{qp_name}"].textChanged.connect(lambda: self.main.build_commands())
- if config_opt:
+ if not disable_bitrate and config_opt:
self.mode = "Bitrate"
self.qp_radio.setChecked(False)
self.bitrate_radio.setChecked(True)
- qp_box_layout.addWidget(self.qp_radio)
+ if not disable_bitrate:
+ qp_box_layout.addWidget(self.qp_radio)
qp_box_layout.addWidget(self.widgets[qp_name], 1)
qp_box_layout.addStretch(1)
qp_box_layout.addStretch(1)
@@ -439,11 +448,13 @@ def _add_modes(
qp_box_layout.addWidget(self.widgets[f"custom_{qp_name}"])
qp_box_layout.addWidget(QtWidgets.QLabel(" "))
- bitrate_group_box.setLayout(bitrate_box_layout)
+ if not disable_bitrate:
+ bitrate_group_box.setLayout(bitrate_box_layout)
qp_group_box.setLayout(qp_box_layout)
layout.addWidget(qp_group_box, 0, 0)
- layout.addWidget(bitrate_group_box, 1, 0)
+ if not disable_bitrate:
+ layout.addWidget(bitrate_group_box, 1, 0)
if not add_qp:
qp_group_box.hide()
@@ -550,7 +561,7 @@ def reload(self):
if widget_name in ("x265_params", "svtav1_params", "vvc_params"):
data = ":".join(data)
self.widgets[widget_name].setText(str(data) or "")
- if getattr(self, "qp_radio", None):
+ if getattr(self, "mode", None):
bitrate = getattr(self.app.fastflix.current_video.video_settings.video_encoder_settings, "bitrate", None)
if bitrate:
self.mode = "Bitrate"
@@ -565,8 +576,11 @@ def reload(self):
self.widgets.custom_bitrate.setText(bitrate.rstrip("k"))
else:
self.mode = self.qp_name
- self.qp_radio.setChecked(True)
- self.bitrate_radio.setChecked(False)
+ try:
+ self.qp_radio.setChecked(True)
+ self.bitrate_radio.setChecked(False)
+ except Exception:
+ pass
qp = str(getattr(self.app.fastflix.current_video.video_settings.video_encoder_settings, self.qp_name))
for i, rec in enumerate(self.recommended_qps):
if rec.startswith(qp):
@@ -598,7 +612,11 @@ def get_mode_settings(self) -> Tuple[str, Union[float, int, str]]:
if not custom_value:
logger.error("No value provided for custom QP/CRF value, defaulting to 30")
return "qp", 30
- custom_value = float(self.widgets[f"custom_{self.qp_name}"].text().rstrip("."))
+ try:
+ custom_value = float(self.widgets[f"custom_{self.qp_name}"].text().rstrip("."))
+ except ValueError:
+ logger.error("Custom QP/CRF value is not a number, defaulting to 30")
+ return "qp", 30
if custom_value.is_integer():
custom_value = int(custom_value)
return "qp", custom_value
diff --git a/fastflix/encoders/gif/command_builder.py b/fastflix/encoders/gif/command_builder.py
index 96f5616b..5e0d264f 100644
--- a/fastflix/encoders/gif/command_builder.py
+++ b/fastflix/encoders/gif/command_builder.py
@@ -16,11 +16,11 @@ def build(fastflix: FastFlix):
args += f":max_colors={settings.max_colors}"
palletgen_filters = generate_filters(
- custom_filters=f"palettegen{args}", **fastflix.current_video.video_settings.dict()
+ custom_filters=f"palettegen{args}", **fastflix.current_video.video_settings.model_dump()
)
filters = generate_filters(
- custom_filters=f"fps={settings.fps:.2f}", raw_filters=True, **fastflix.current_video.video_settings.dict()
+ custom_filters=f"fps={settings.fps}", raw_filters=True, **fastflix.current_video.video_settings.model_dump()
)
output_video = clean_file_string(fastflix.current_video.video_settings.output_path)
@@ -41,7 +41,7 @@ def build(fastflix: FastFlix):
f'{beginning} {palletgen_filters} {settings.extra if settings.extra_both_passes else ""} -y "{temp_palette}"'
)
- gif_filters = f"fps={settings.fps:.2f}"
+ gif_filters = f"fps={settings.fps}"
if filters:
gif_filters += f",{filters}"
diff --git a/fastflix/encoders/gif/settings_panel.py b/fastflix/encoders/gif/settings_panel.py
index e4450239..11a03091 100644
--- a/fastflix/encoders/gif/settings_panel.py
+++ b/fastflix/encoders/gif/settings_panel.py
@@ -42,6 +42,7 @@ def init_dither(self):
return self._add_combo_box(
label="Dither",
widget_name="dither",
+ opt="dither",
tooltip=(
"Dither is an intentionally applied form of noise used to randomize quantization error,\n"
"preventing large-scale patterns such as color banding in images."
@@ -77,7 +78,7 @@ def init_statistics_mode(self):
def update_video_encoder_settings(self):
self.app.fastflix.current_video.video_settings.video_encoder_settings = GIFSettings(
- fps=int(self.widgets.fps.currentText()),
+ fps=self.widgets.fps.currentText(),
dither=self.widgets.dither.currentText(),
extra=self.ffmpeg_extras,
pix_fmt="yuv420p", # hack for thumbnails to show properly
@@ -88,5 +89,3 @@ def update_video_encoder_settings(self):
def new_source(self):
super().new_source()
- self.widgets.fps.setCurrentIndex(14)
- self.widgets.dither.setCurrentIndex(0)
diff --git a/fastflix/encoders/nvencc_av1/settings_panel.py b/fastflix/encoders/nvencc_av1/settings_panel.py
index cedda91b..9ce6f85a 100644
--- a/fastflix/encoders/nvencc_av1/settings_panel.py
+++ b/fastflix/encoders/nvencc_av1/settings_panel.py
@@ -147,7 +147,6 @@ def __init__(self, parent, main, app: FastFlixApp):
guide_label.setOpenExternalLinks(True)
grid.addWidget(guide_label, 11, 0, 1, 4)
grid.addWidget(warning_label, 11, 4, 1, 1, alignment=QtCore.Qt.AlignRight)
- grid.addWidget(QtWidgets.QLabel(t("NVEncC AV1 Encoder is untested!")), 11, 5, 1, 1)
self.setLayout(grid)
self.hide()
diff --git a/fastflix/encoders/qsvencc_av1/settings_panel.py b/fastflix/encoders/qsvencc_av1/settings_panel.py
index fcb1772f..e8f31be8 100644
--- a/fastflix/encoders/qsvencc_av1/settings_panel.py
+++ b/fastflix/encoders/qsvencc_av1/settings_panel.py
@@ -153,7 +153,6 @@ def __init__(self, parent, main, app: FastFlixApp):
guide_label.setOpenExternalLinks(True)
grid.addWidget(guide_label, 11, 0, 1, 4)
grid.addWidget(warning_label, 11, 4, 1, 1, alignment=QtCore.Qt.AlignRight)
- grid.addWidget(QtWidgets.QLabel(t("QSVEncC AV1 Encoder is untested!")), 11, 5, 1, 1)
self.setLayout(grid)
self.hide()
diff --git a/fastflix/encoders/vceencc_av1/settings_panel.py b/fastflix/encoders/vceencc_av1/settings_panel.py
index b5930e90..a607c292 100644
--- a/fastflix/encoders/vceencc_av1/settings_panel.py
+++ b/fastflix/encoders/vceencc_av1/settings_panel.py
@@ -134,7 +134,6 @@ def __init__(self, parent, main, app: FastFlixApp):
guide_label.setOpenExternalLinks(True)
grid.addWidget(guide_label, 12, 0, 1, 4)
grid.addWidget(warning_label, 12, 4, 1, 1, alignment=QtCore.Qt.AlignRight)
- grid.addWidget(QtWidgets.QLabel(t("VCEEncC AV1 Encoder is untested!")), 12, 5, 1, 1)
self.setLayout(grid)
self.hide()
@@ -302,9 +301,9 @@ def update_video_encoder_settings(self):
pa_paq=self.widgets.pa_paq.currentText(),
pa_taq=None if self.widgets.pa_taq.currentIndex() == 0 else self.widgets.pa_taq.currentText(),
pa_motion_quality=self.widgets.pa_motion_quality.currentText(),
- output_depth=None
- if self.widgets.output_depth.currentIndex() == 0
- else self.widgets.output_depth.currentText(),
+ output_depth=(
+ None if self.widgets.output_depth.currentIndex() == 0 else self.widgets.output_depth.currentText()
+ ),
)
encode_type, q_value = self.get_mode_settings()
diff --git a/fastflix/encoders/vceencc_avc/settings_panel.py b/fastflix/encoders/vceencc_avc/settings_panel.py
index f2341f7e..dcd8e1d2 100644
--- a/fastflix/encoders/vceencc_avc/settings_panel.py
+++ b/fastflix/encoders/vceencc_avc/settings_panel.py
@@ -301,9 +301,9 @@ def update_video_encoder_settings(self):
pa_paq=self.widgets.pa_paq.currentText(),
pa_taq=None if self.widgets.pa_taq.currentIndex() == 0 else self.widgets.pa_taq.currentText(),
pa_motion_quality=self.widgets.pa_motion_quality.currentText(),
- output_depth=None
- if self.widgets.output_depth.currentIndex() == 0
- else self.widgets.output_depth.currentText(),
+ output_depth=(
+ None if self.widgets.output_depth.currentIndex() == 0 else self.widgets.output_depth.currentText()
+ ),
)
encode_type, q_value = self.get_mode_settings()
diff --git a/fastflix/encoders/vceencc_hevc/settings_panel.py b/fastflix/encoders/vceencc_hevc/settings_panel.py
index cef9dc58..2c2089b8 100644
--- a/fastflix/encoders/vceencc_hevc/settings_panel.py
+++ b/fastflix/encoders/vceencc_hevc/settings_panel.py
@@ -295,9 +295,9 @@ def update_video_encoder_settings(self):
pa_paq=self.widgets.pa_paq.currentText(),
pa_taq=None if self.widgets.pa_taq.currentIndex() == 0 else self.widgets.pa_taq.currentText(),
pa_motion_quality=self.widgets.pa_motion_quality.currentText(),
- output_depth=None
- if self.widgets.output_depth.currentIndex() == 0
- else self.widgets.output_depth.currentText(),
+ output_depth=(
+ None if self.widgets.output_depth.currentIndex() == 0 else self.widgets.output_depth.currentText()
+ ),
)
encode_type, q_value = self.get_mode_settings()
diff --git a/fastflix/encoders/vp9/settings_panel.py b/fastflix/encoders/vp9/settings_panel.py
index 526eac03..0b6210e1 100644
--- a/fastflix/encoders/vp9/settings_panel.py
+++ b/fastflix/encoders/vp9/settings_panel.py
@@ -42,6 +42,7 @@
"8-bit: yuv420p",
"10-bit: yuv420p10le",
"12-bit: yuv420p12le",
+ "8-bit 420 Transparent: yuva420p",
"8-bit 422: yuv422p",
"8-bit 444: yuv444p",
"10-bit 422: yuv422p10le",
@@ -213,9 +214,9 @@ def update_video_encoder_settings(self):
extra=self.ffmpeg_extras,
extra_both_passes=self.widgets.extra_both_passes.isChecked(),
fast_first_pass=self.widgets.fast_first_pass.isChecked(),
- tile_columns=self.widgets.tile_columns.currentText()
- if self.widgets.tile_columns.currentIndex() > 0
- else "-1",
+ tile_columns=(
+ self.widgets.tile_columns.currentText() if self.widgets.tile_columns.currentIndex() > 0 else "-1"
+ ),
tile_rows=self.widgets.tile_rows.currentText() if self.widgets.tile_rows.currentIndex() > 0 else "-1",
)
encode_type, q_value = self.get_mode_settings()
diff --git a/fastflix/encoders/webp/command_builder.py b/fastflix/encoders/webp/command_builder.py
index 9a9b7524..feacebef 100644
--- a/fastflix/encoders/webp/command_builder.py
+++ b/fastflix/encoders/webp/command_builder.py
@@ -11,7 +11,8 @@ def build(fastflix: FastFlix):
return [
Command(
- command=f"{beginning} -lossless {settings.lossless} -compression_level {settings.compression} "
+ command=f"{beginning} -lossless {'1' if settings.lossless.lower() in ('1', 'yes') else '0'} "
+ f"-compression_level {settings.compression} "
f"-qscale {settings.qscale} -preset {settings.preset} {settings.extra} {ending}",
name="WebP",
exe="ffmpeg",
diff --git a/fastflix/encoders/webp/settings_panel.py b/fastflix/encoders/webp/settings_panel.py
index 53205542..7c8ebfd1 100644
--- a/fastflix/encoders/webp/settings_panel.py
+++ b/fastflix/encoders/webp/settings_panel.py
@@ -1,12 +1,16 @@
# -*- coding: utf-8 -*-
from box import Box
from PySide6 import QtWidgets
+import logging
from fastflix.encoders.common.setting_panel import SettingPanel
from fastflix.models.encode import WebPSettings
from fastflix.models.fastflix_app import FastFlixApp
+logger = logging.getLogger("fastflix")
+
+
class WEBP(SettingPanel):
profile_name = "webp"
@@ -14,6 +18,7 @@ def __init__(self, parent, main, app: FastFlixApp):
super().__init__(parent, main, app)
self.main = main
self.app = app
+ self.mode = "qscale"
grid = QtWidgets.QGridLayout()
@@ -31,7 +36,13 @@ def __init__(self, parent, main, app: FastFlixApp):
self.setLayout(grid)
def init_lossless(self):
- return self._add_combo_box(label="lossless", options=["yes", "no"], widget_name="lossless", default=1)
+ return self._add_combo_box(
+ label="lossless",
+ options=["yes", "no"],
+ widget_name="lossless",
+ default="yes",
+ opt="lossless",
+ )
def init_compression(self):
return self._add_combo_box(
@@ -40,6 +51,7 @@ def init_compression(self):
widget_name="compression",
tooltip="For lossy, this is a quality/speed tradeoff.\nFor lossless, this is a size/speed tradeoff.",
default=4,
+ opt="compression",
)
def init_preset(self):
@@ -48,67 +60,32 @@ def init_preset(self):
options=["none", "default", "picture", "photo", "drawing", "icon", "text"],
widget_name="preset",
default=1,
+ opt="preset",
)
def init_modes(self):
- layout = QtWidgets.QGridLayout()
- qscale_group_box = QtWidgets.QGroupBox()
- qscale_group_box.setStyleSheet("QGroupBox{padding-top:5px; margin-top:-18px}")
- qscale_box_layout = QtWidgets.QHBoxLayout()
-
- self.widgets.mode = QtWidgets.QButtonGroup()
- self.widgets.mode.buttonClicked.connect(self.set_mode)
-
- qscale_radio = QtWidgets.QRadioButton("qscale")
- qscale_radio.setChecked(True)
- qscale_radio.setFixedWidth(80)
- self.widgets.mode.addButton(qscale_radio)
-
- self.widgets.qscale = QtWidgets.QComboBox()
- self.widgets.qscale.setFixedWidth(250)
- self.widgets.qscale.addItems([str(x) for x in range(0, 101, 5)] + ["Custom"])
- self.widgets.qscale.setCurrentIndex(15)
- self.widgets.qscale.currentIndexChanged.connect(lambda: self.mode_update())
- self.widgets.custom_qscale = QtWidgets.QLineEdit("75")
- self.widgets.custom_qscale.setFixedWidth(100)
- self.widgets.custom_qscale.setDisabled(True)
- self.widgets.custom_qscale.setValidator(self.only_int)
- self.widgets.custom_qscale.textChanged.connect(lambda: self.main.build_commands())
- qscale_box_layout.addWidget(qscale_radio)
- qscale_box_layout.addWidget(self.widgets.qscale)
- qscale_box_layout.addStretch()
- qscale_box_layout.addWidget(QtWidgets.QLabel("Custom:"))
- qscale_box_layout.addWidget(self.widgets.custom_qscale)
-
- qscale_group_box.setLayout(qscale_box_layout)
-
- layout.addWidget(qscale_group_box, 0, 0)
- return layout
+ return self._add_modes(
+ qp_name="qscale",
+ add_qp=True,
+ disable_bitrate=True,
+ recommended_qps=[str(x) for x in range(0, 101, 5)] + ["Custom"],
+ recommended_bitrates=[],
+ )
def update_video_encoder_settings(self):
- lossless = self.widgets.lossless.currentText()
-
settings = WebPSettings(
- lossless="1" if lossless == "yes" else "0",
+ lossless=self.widgets.lossless.currentText(),
compression=self.widgets.compression.currentText(),
preset=self.widgets.preset.currentText(),
extra=self.ffmpeg_extras,
pix_fmt="yuv420p", # hack for thumbnails to show properly
extra_both_passes=self.widgets.extra_both_passes.isChecked(),
)
- qscale = self.widgets.qscale.currentText()
- if self.widgets.custom_qscale.isEnabled():
- if not self.widgets.custom_qscale.text():
- settings.qscale = 75
- else:
- settings.qscale = int(self.widgets.custom_qscale.text())
- else:
- settings.qscale = int(qscale.split(" ", 1)[0])
+ _, settings.qscale = self.get_mode_settings()
self.app.fastflix.current_video.video_settings.video_encoder_settings = settings
def new_source(self):
super().new_source()
- self.widgets.lossless.setCurrentIndex(0)
def set_mode(self, x):
self.mode = x.text()
diff --git a/fastflix/ff_queue.py b/fastflix/ff_queue.py
index 48127454..47ed08b8 100644
--- a/fastflix/ff_queue.py
+++ b/fastflix/ff_queue.py
@@ -84,7 +84,7 @@ def update_conversion_command(vid, old_path: str, new_path: str):
command["command"] = new_command
for video in queue:
- video = video.dict()
+ video = video.model_dump()
video["source"] = os.fspath(video["source"])
video["work_path"] = os.fspath(video["work_path"])
video["video_settings"]["output_path"] = os.fspath(video["video_settings"]["output_path"])
diff --git a/fastflix/flix.py b/fastflix/flix.py
index 02d79d34..73e0b064 100644
--- a/fastflix/flix.py
+++ b/fastflix/flix.py
@@ -61,9 +61,14 @@
"bt2020_10bit",
"bt2020_12",
"bt2020_12bit",
+ "bt2020-10",
+ "bt2020-10bit",
+ "bt2020-12",
+ "bt2020-12bit",
"smpte2084",
"smpte428",
"smpte428_1",
+ "smpte428-1",
"arib-std-b67",
]
diff --git a/fastflix/models/config.py b/fastflix/models/config.py
index f8a5074b..fbbf045b 100644
--- a/fastflix/models/config.py
+++ b/fastflix/models/config.py
@@ -357,7 +357,7 @@ def check_hw_encoders(self):
self.qsvencc_encoders = []
def save(self):
- items = self.dict()
+ items = self.model_dump()
del items["config_path"]
for k, v in items.items():
if isinstance(v, Path):
diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py
index 095e0ecb..2b104bcb 100644
--- a/fastflix/models/encode.py
+++ b/fastflix/models/encode.py
@@ -3,7 +3,7 @@
from pathlib import Path
from typing import Optional, Union
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, field_validator
from box import Box
@@ -25,6 +25,9 @@ class AudioTrack(BaseModel):
raw_info: Optional[Union[dict, Box]] = None
dispositions: dict = Field(default_factory=dict)
+ class Config:
+ arbitrary_types_allowed = True
+
class SubtitleTrack(BaseModel):
index: int
@@ -38,6 +41,9 @@ class SubtitleTrack(BaseModel):
long_name: str = ""
raw_info: Optional[Union[dict, Box]] = None
+ class Config:
+ arbitrary_types_allowed = True
+
class AttachmentTrack(BaseModel):
outdex: int
@@ -55,7 +61,7 @@ class EncoderSettings(BaseModel):
class x265Settings(EncoderSettings):
- name = "HEVC (x265)" # MUST match encoder main.name
+ name: str = "HEVC (x265)" # MUST match encoder main.name
preset: str = "medium"
intra_encoding: bool = False
profile: str = "default"
@@ -80,7 +86,7 @@ class x265Settings(EncoderSettings):
class VVCSettings(EncoderSettings):
- name = "VVC" # MUST match encoder main.name
+ name: str = "VVC" # MUST match encoder main.name
preset: str = "medium"
qp: Optional[Union[int, float]] = 22
bitrate: Optional[str] = None
@@ -92,7 +98,7 @@ class VVCSettings(EncoderSettings):
class x264Settings(EncoderSettings):
- name = "AVC (x264)"
+ name: str = "AVC (x264)"
preset: str = "medium"
profile: str = "default"
tune: Optional[str] = None
@@ -103,13 +109,13 @@ class x264Settings(EncoderSettings):
class FFmpegNVENCSettings(EncoderSettings):
- name = "HEVC (NVENC)"
+ name: str = "HEVC (NVENC)"
preset: str = "slow"
profile: str = "main"
tune: str = "hq"
pix_fmt: str = "p010le"
bitrate: Optional[str] = "6000k"
- qp: Optional[str] = None
+ qp: Optional[Union[int, float]] = None
cq: int = 0
spatial_aq: int = 0
rc_lookahead: int = 0
@@ -120,13 +126,20 @@ class FFmpegNVENCSettings(EncoderSettings):
b_ref_mode: str = "disabled"
hw_accel: bool = False
+ @field_validator("qp", mode="before")
+ @classmethod
+ def qp_to_int(cls, value):
+ if isinstance(value, str):
+ return int(value)
+ return value
+
class NVEncCSettings(EncoderSettings):
- name = "HEVC (NVEncC)"
+ name: str = "HEVC (NVEncC)"
preset: str = "quality"
profile: str = "auto"
bitrate: Optional[str] = "5000k"
- cqp: Optional[str] = None
+ cqp: Optional[Union[int, float]] = None
aq: str = "off"
aq_strength: int = 0
lookahead: Optional[int] = None
@@ -154,13 +167,20 @@ class NVEncCSettings(EncoderSettings):
decoder: str = "Auto"
copy_hdr10: bool = False
+ @field_validator("cqp", mode="before")
+ @classmethod
+ def cqp_to_int(cls, value):
+ if isinstance(value, str):
+ return int(value)
+ return value
+
class NVEncCAV1Settings(EncoderSettings):
- name = "AV1 (NVEncC)"
+ name: str = "AV1 (NVEncC)"
preset: str = "quality"
profile: str = "auto"
bitrate: Optional[str] = "5000k"
- cqp: Optional[str] = None
+ cqp: Optional[Union[int, float]] = None
aq: str = "off"
aq_strength: int = 0
lookahead: Optional[int] = None
@@ -188,12 +208,19 @@ class NVEncCAV1Settings(EncoderSettings):
decoder: str = "Auto"
copy_hdr10: bool = False
+ @field_validator("cqp", mode="before")
+ @classmethod
+ def cqp_to_int(cls, value):
+ if isinstance(value, str):
+ return int(value)
+ return value
+
class QSVEncCSettings(EncoderSettings):
- name = "HEVC (QSVEncC)"
+ name: str = "HEVC (QSVEncC)"
preset: str = "best"
bitrate: Optional[str] = "5000k"
- cqp: Optional[str] = None
+ cqp: Optional[Union[int, float]] = None
lookahead: Optional[str] = None
level: Optional[str] = None
hdr10plus_metadata: str = ""
@@ -214,12 +241,19 @@ class QSVEncCSettings(EncoderSettings):
adapt_ltr: bool = False
copy_hdr10: bool = False
+ @field_validator("cqp", mode="before")
+ @classmethod
+ def cqp_to_int(cls, value):
+ if isinstance(value, str):
+ return int(value)
+ return value
+
class QSVEncCAV1Settings(EncoderSettings):
- name = "AV1 (QSVEncC)"
+ name: str = "AV1 (QSVEncC)"
preset: str = "best"
bitrate: Optional[str] = "5000k"
- cqp: Optional[str] = None
+ cqp: Optional[Union[int, float]] = None
lookahead: Optional[str] = None
level: Optional[str] = None
hdr10plus_metadata: str = ""
@@ -240,13 +274,20 @@ class QSVEncCAV1Settings(EncoderSettings):
adapt_ltr: bool = False
copy_hdr10: bool = False
+ @field_validator("cqp", mode="before")
+ @classmethod
+ def cqp_to_int(cls, value):
+ if isinstance(value, str):
+ return int(value)
+ return value
+
class QSVEncCH264Settings(EncoderSettings):
- name = "AVC (QSVEncC)"
+ name: str = "AVC (QSVEncC)"
preset: str = "best"
profile: str = "auto"
bitrate: Optional[str] = "5000k"
- cqp: Optional[str] = None
+ cqp: Optional[Union[int, float]] = None
lookahead: Optional[str] = None
level: Optional[str] = None
min_q_i: Optional[str] = None
@@ -265,13 +306,20 @@ class QSVEncCH264Settings(EncoderSettings):
adapt_cqm: bool = False
adapt_ltr: bool = False
+ @field_validator("cqp", mode="before")
+ @classmethod
+ def cqp_to_int(cls, value):
+ if isinstance(value, str):
+ return int(value)
+ return value
+
class NVEncCAVCSettings(EncoderSettings):
- name = "AVC (NVEncC)"
+ name: str = "AVC (NVEncC)"
preset: str = "quality"
profile: str = "auto"
bitrate: Optional[str] = "5000k"
- cqp: Optional[str] = None
+ cqp: Optional[Union[int, float]] = None
aq: str = "off"
aq_strength: int = 0
lookahead: Optional[int] = None
@@ -297,12 +345,19 @@ class NVEncCAVCSettings(EncoderSettings):
device: int = 0
decoder: str = "Auto"
+ @field_validator("cqp", mode="before")
+ @classmethod
+ def cqp_to_int(cls, value):
+ if isinstance(value, str):
+ return int(value)
+ return value
+
class VCEEncCSettings(EncoderSettings):
- name = "HEVC (VCEEncC)"
+ name: str = "HEVC (VCEEncC)"
preset: str = "slow"
bitrate: Optional[str] = "5000k"
- cqp: Optional[str] = None
+ cqp: Optional[Union[int, float]] = None
tier: str = "high"
level: Optional[str] = None
hdr10plus_metadata: str = ""
@@ -332,12 +387,19 @@ class VCEEncCSettings(EncoderSettings):
output_depth: str | None = None
copy_hdr10: bool = False
+ @field_validator("cqp", mode="before")
+ @classmethod
+ def cqp_to_int(cls, value):
+ if isinstance(value, str):
+ return int(value)
+ return value
+
class VCEEncCAV1Settings(EncoderSettings):
- name = "AV1 (VCEEncC)"
+ name: str = "AV1 (VCEEncC)"
preset: str = "slower"
bitrate: Optional[str] = "5000k"
- cqp: Optional[str] = None
+ cqp: Optional[Union[int, float]] = None
level: Optional[str] = None
hdr10plus_metadata: str = ""
mv_precision: str = "q-pel"
@@ -364,16 +426,23 @@ class VCEEncCAV1Settings(EncoderSettings):
pa_paq: str | None = None
pa_taq: int | None = None
pa_motion_quality: str | None = None
- output_depth: str | None
+ output_depth: str | None = None
copy_hdr10: bool = False
+ @field_validator("cqp", mode="before")
+ @classmethod
+ def cqp_to_int(cls, value):
+ if isinstance(value, str):
+ return int(value)
+ return value
+
class VCEEncCAVCSettings(EncoderSettings):
- name = "AVC (VCEEncC)"
+ name: str = "AVC (VCEEncC)"
preset: str = "slow"
profile: str = "Auto"
bitrate: Optional[str] = "5000k"
- cqp: Optional[str] = None
+ cqp: Optional[Union[int, float]] = None
tier: str = "high"
level: Optional[str] = None
hdr10plus_metadata: str = ""
@@ -401,9 +470,16 @@ class VCEEncCAVCSettings(EncoderSettings):
pa_motion_quality: str | None = None
output_depth: str | None = None
+ @field_validator("cqp", mode="before")
+ @classmethod
+ def cqp_to_int(cls, value):
+ if isinstance(value, str):
+ return int(value)
+ return value
+
class rav1eSettings(EncoderSettings):
- name = "AV1 (rav1e)"
+ name: str = "AV1 (rav1e)"
speed: str = "-1"
tile_columns: str = "-1"
tile_rows: str = "-1"
@@ -414,7 +490,7 @@ class rav1eSettings(EncoderSettings):
class SVTAV1Settings(EncoderSettings):
- name = "AV1 (SVT AV1)"
+ name: str = "AV1 (SVT AV1)"
tile_columns: str = "0"
tile_rows: str = "0"
scene_detection: bool = False
@@ -427,7 +503,7 @@ class SVTAV1Settings(EncoderSettings):
class SVTAVIFSettings(EncoderSettings):
- name = "AVIF (SVT AV1)"
+ name: str = "AVIF (SVT AV1)"
single_pass: bool = True
speed: str = "7" # Renamed preset in svtav1 encoder
qp: Optional[Union[int, float]] = 24
@@ -437,7 +513,7 @@ class SVTAVIFSettings(EncoderSettings):
class VP9Settings(EncoderSettings):
- name = "VP9"
+ name: str = "VP9"
profile: int = 2
quality: str = "good"
speed: str = "0"
@@ -451,7 +527,7 @@ class VP9Settings(EncoderSettings):
class HEVCVideoToolboxSettings(EncoderSettings):
- name = "HEVC (Video Toolbox)"
+ name: str = "HEVC (Video Toolbox)"
profile: int = 0
allow_sw: bool = False
require_sw: bool = False
@@ -464,7 +540,7 @@ class HEVCVideoToolboxSettings(EncoderSettings):
class H264VideoToolboxSettings(EncoderSettings):
- name = "H264 (Video Toolbox)"
+ name: str = "H264 (Video Toolbox)"
profile: int = 0
allow_sw: bool = False
require_sw: bool = False
@@ -477,7 +553,7 @@ class H264VideoToolboxSettings(EncoderSettings):
class AOMAV1Settings(EncoderSettings):
- name = "AV1 (AOM)"
+ name: str = "AV1 (AOM)"
tile_columns: str = "0"
tile_rows: str = "0"
usage: str = "good"
@@ -488,27 +564,52 @@ class AOMAV1Settings(EncoderSettings):
class WebPSettings(EncoderSettings):
- name = "WebP"
- lossless: str = "0"
+ name: str = "WebP"
+ lossless: str = "no"
compression: str = "3"
preset: str = "none"
- qscale: Union[int, float] = 15
+ qscale: Union[int, float] = 75
+
+ @field_validator("lossless", mode="before")
+ @classmethod
+ def losslessq_new_value(cls, value):
+ if value == "0":
+ return "no"
+ if value == "1":
+ return "yes"
+ return value
+
+ @field_validator("qscale", mode="before")
+ @classmethod
+ def qscale_new_value(cls, value):
+ if isinstance(value, str):
+ return int(value)
+ return value
class GIFSettings(EncoderSettings):
- name = "GIF"
- fps: int = 15
+ name: str = "GIF"
+ fps: str = "15"
dither: str = "sierra2_4a"
max_colors: str = "256"
stats_mode: str = "full"
+ @field_validator("fps", mode="before")
+ @classmethod
+ def fps_field_validate(cls, value):
+ if isinstance(value, (int, float)):
+ return str(value)
+ if not value.isdigit():
+ raise ValueError("FPS must be a while number")
+ return value
+
class CopySettings(EncoderSettings):
- name = "Copy"
+ name: str = "Copy"
class VAAPIH264Settings(EncoderSettings):
- name = "VAAPI H264" # must be same as encoder name in main
+ name: str = "VAAPI H264" # must be same as encoder name in main
vaapi_device: str = "/dev/dri/renderD128"
low_power: bool = False
@@ -524,7 +625,7 @@ class VAAPIH264Settings(EncoderSettings):
class VAAPIHEVCSettings(EncoderSettings):
- name = "VAAPI HEVC"
+ name: str = "VAAPI HEVC"
vaapi_device: str = "/dev/dri/renderD128"
low_power: bool = False
@@ -540,7 +641,7 @@ class VAAPIHEVCSettings(EncoderSettings):
class VAAPIVP9Settings(EncoderSettings):
- name = "VAAPI VP9"
+ name: str = "VAAPI VP9"
vaapi_device: str = "/dev/dri/renderD128"
low_power: bool = False
@@ -553,7 +654,7 @@ class VAAPIVP9Settings(EncoderSettings):
class VAAPIMPEG2Settings(EncoderSettings):
- name = "VAAPI MPEG2"
+ name: str = "VAAPI MPEG2"
vaapi_device: str = "/dev/dri/renderD128"
low_power: bool = False
diff --git a/fastflix/models/fastflix.py b/fastflix/models/fastflix.py
index 8eb4a95a..f6843052 100644
--- a/fastflix/models/fastflix.py
+++ b/fastflix/models/fastflix.py
@@ -32,8 +32,8 @@ class FastFlix(BaseModel):
currently_encoding: bool = False
conversion_paused: bool = False
conversion_list: list[Video] = Field(default_factory=list)
- current_video_encode_index = 0
- current_command_encode_index = 0
+ current_video_encode_index: int = 0
+ current_command_encode_index: int = 0
# State
shutting_down: bool = False
diff --git a/fastflix/models/profiles.py b/fastflix/models/profiles.py
index 5c1f2ad8..ea8e75fe 100644
--- a/fastflix/models/profiles.py
+++ b/fastflix/models/profiles.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
from typing import Optional, Union
-from pydantic import BaseModel, Field, validator
+from pydantic import field_validator, BaseModel, Field
from enum import Enum
from fastflix.models.encode import (
@@ -61,19 +61,22 @@ class AudioMatch(BaseModel):
bitrate: Optional[str] = None
downmix: Optional[Union[str, int]] = None
- @validator("match_type")
+ @field_validator("match_type", mode="before")
+ @classmethod
def match_type_must_be_enum(cls, v):
if isinstance(v, list):
return MatchType(v[0])
return MatchType(v)
- @validator("match_item")
+ @field_validator("match_item", mode="before")
+ @classmethod
def match_item_must_be_enum(cls, v):
if isinstance(v, list):
return MatchType(v[0])
return MatchItem(v)
- @validator("downmix")
+ @field_validator("downmix", mode="before")
+ @classmethod
def downmix_as_string(cls, v):
fixed = {1: "monoo", 2: "stereo", 3: "2.1", 4: "3.1", 5: "5.0", 6: "5.1", 7: "6.1", 8: "7.1"}
if isinstance(v, str) and v.isnumeric():
@@ -84,7 +87,8 @@ def downmix_as_string(cls, v):
return None
return v
- @validator("bitrate")
+ @field_validator("bitrate", mode="before")
+ @classmethod
def bitrate_k_end(cls, v):
if v and not v.endswith("k"):
return f"{v}k"
diff --git a/fastflix/models/video.py b/fastflix/models/video.py
index 37ced71c..728f57a2 100644
--- a/fastflix/models/video.py
+++ b/fastflix/models/video.py
@@ -4,7 +4,7 @@
from typing import List, Optional, Union, Tuple
from box import Box
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, field_validator
from fastflix.models.encode import (
AOMAV1Settings,
@@ -106,43 +106,66 @@ class VideoSettings(BaseModel):
vsync: Optional[str] = None
maxrate: Optional[int] = None
bufsize: Optional[int] = None
- brightness: Optional[float] = None
- contrast: Optional[float] = None
- saturation: Optional[float] = None
- video_encoder_settings: Union[
- x265Settings,
- x264Settings,
- rav1eSettings,
- SVTAV1Settings,
- AOMAV1Settings,
- VP9Settings,
- GIFSettings,
- WebPSettings,
- CopySettings,
- FFmpegNVENCSettings,
- QSVEncCSettings,
- QSVEncCAV1Settings,
- QSVEncCH264Settings,
- NVEncCSettings,
- NVEncCAVCSettings,
- NVEncCAV1Settings,
- VCEEncCSettings,
- VCEEncCAVCSettings,
- VCEEncCAV1Settings,
- HEVCVideoToolboxSettings,
- H264VideoToolboxSettings,
- SVTAVIFSettings,
- VVCSettings,
- VAAPIH264Settings,
- VAAPIHEVCSettings,
- VAAPIVP9Settings,
- VAAPIMPEG2Settings,
+ brightness: Optional[str] = None
+ contrast: Optional[str] = None
+ saturation: Optional[str] = None
+ video_encoder_settings: Optional[
+ Union[
+ x265Settings,
+ x264Settings,
+ rav1eSettings,
+ SVTAV1Settings,
+ AOMAV1Settings,
+ VP9Settings,
+ GIFSettings,
+ WebPSettings,
+ CopySettings,
+ FFmpegNVENCSettings,
+ QSVEncCSettings,
+ QSVEncCAV1Settings,
+ QSVEncCH264Settings,
+ NVEncCSettings,
+ NVEncCAVCSettings,
+ NVEncCAV1Settings,
+ VCEEncCSettings,
+ VCEEncCAVCSettings,
+ VCEEncCAV1Settings,
+ HEVCVideoToolboxSettings,
+ H264VideoToolboxSettings,
+ SVTAVIFSettings,
+ VVCSettings,
+ VAAPIH264Settings,
+ VAAPIHEVCSettings,
+ 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)
conversion_commands: List = Field(default_factory=list)
+ @field_validator("brightness", mode="before")
+ @classmethod
+ def brightness_to_str(cls, value):
+ if isinstance(value, (int, float)):
+ return str(value)
+ return value
+
+ @field_validator("contrast", mode="before")
+ @classmethod
+ def contrast_to_str(cls, value):
+ if isinstance(value, (int, float)):
+ return float(value)
+ return value
+
+ @field_validator("saturation", mode="before")
+ @classmethod
+ def saturation_to_str(cls, value):
+ if isinstance(value, (int, float)):
+ return float(value)
+ return value
+
class Status(BaseModel):
success: bool = False
@@ -282,3 +305,6 @@ def scale(self):
return f"{self.video_settings.resolution_custom}:-8"
else:
return f"-8:{self.video_settings.resolution_custom}"
+
+ class Config:
+ arbitrary_types_allowed = True
diff --git a/fastflix/version.py b/fastflix/version.py
index d61958af..04096d1c 100644
--- a/fastflix/version.py
+++ b/fastflix/version.py
@@ -1,4 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-__version__ = "5.7.4"
+__version__ = "5.8.0"
__author__ = "Chris Griffith"
diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py
index 160c495f..271a0f8b 100644
--- a/fastflix/widgets/container.py
+++ b/fastflix/widgets/container.py
@@ -421,7 +421,7 @@ def profile_widget(self, settings):
title = QtWidgets.QLabel(t("Encoder Settings"))
# title.setFont(QtGui.QFont(self.app.font().family(), 9, weight=70))
layout.addWidget(title)
- for k, v in settings.dict().items():
+ for k, v in settings.model_dump().items():
item_1 = QtWidgets.QLabel(" ".join(str(k).split("_")).title())
item_2 = QtWidgets.QLabel(str(v))
item_2.setMaximumWidth(150)
@@ -440,7 +440,7 @@ def __init__(self, profile_name, profile):
profile_title = QtWidgets.QLabel(f"{t('Profile_window')}: {profile_name}")
# profile_title.setFont(QtGui.QFont(self.app.font().family(), 10, weight=70))
main_section.addWidget(profile_title)
- for k, v in profile.dict().items():
+ for k, v in profile.model_dump().items():
if k == "advanced_options":
continue
if k.lower().startswith("audio") or k.lower() == "profile_version":
@@ -472,7 +472,7 @@ def __init__(self, profile_name, profile):
advanced_section = QtWidgets.QVBoxLayout(self)
advanced_section.addWidget(QtWidgets.QLabel(t("Advanced Options")))
- for k, v in profile.advanced_options.dict().items():
+ for k, v in profile.advanced_options.model_dump().items():
if k.endswith("_index"):
continue
item_1 = QtWidgets.QLabel(k)
diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py
index 42dde7c4..16f72c11 100644
--- a/fastflix/widgets/main.py
+++ b/fastflix/widgets/main.py
@@ -16,7 +16,7 @@
import importlib.resources
import reusables
from box import Box
-from pydantic import BaseModel, Field
+from pydantic import ConfigDict, BaseModel, Field
from PySide6 import QtCore, QtGui, QtWidgets
from fastflix.encoders.common import helpers
@@ -71,12 +71,15 @@
t("Custom (w:h)"): {"method": "custom"},
"4320 LE": {"method": "long edge", "pixels": 4320},
"2160 LE": {"method": "long edge", "pixels": 2160},
+ "1920 LE": {"method": "long edge", "pixels": 1920},
"1440 LE": {"method": "long edge", "pixels": 1440},
+ "1280 LE": {"method": "long edge", "pixels": 1280},
"1080 LE": {"method": "long edge", "pixels": 1080},
"720 LE": {"method": "long edge", "pixels": 720},
"480 LE": {"method": "long edge", "pixels": 480},
"4320 H": {"method": "height", "pixels": 4320},
"2160 H": {"method": "height", "pixels": 2160},
+ "1920 H": {"method": "height", "pixels": 1920},
"1440 H": {"method": "height", "pixels": 1440},
"1080 H": {"method": "height", "pixels": 1080},
"720 H": {"method": "height", "pixels": 720},
@@ -96,17 +99,13 @@ class CropWidgets(BaseModel):
bottom: QtWidgets.QLineEdit = None
left: QtWidgets.QLineEdit = None
right: QtWidgets.QLineEdit = None
-
- class Config:
- arbitrary_types_allowed = True
+ model_config = ConfigDict(arbitrary_types_allowed=True)
class ScaleWidgets(BaseModel):
width: QtWidgets.QLineEdit = None
height: QtWidgets.QLineEdit = None
-
- class Config:
- arbitrary_types_allowed = True
+ model_config = ConfigDict(arbitrary_types_allowed=True)
class MainWidgets(BaseModel):
@@ -134,9 +133,7 @@ class MainWidgets(BaseModel):
output_directory_combo: QtWidgets.QComboBox = None
output_type_combo: QtWidgets.QComboBox = Field(default_factory=QtWidgets.QComboBox)
output_directory_select: QtWidgets.QPushButton = None
-
- class Config:
- arbitrary_types_allowed = True
+ model_config = ConfigDict(arbitrary_types_allowed=True)
def items(self):
for key in dir(self):
@@ -1427,11 +1424,15 @@ def reload_video_from_queue(self, video: Video):
]
self.widgets.video_track.clear()
self.widgets.video_track.addItems(text_video_tracks)
- selected_track = 0
- for track in self.app.fastflix.current_video.streams.video:
- if track.index == self.app.fastflix.current_video.video_settings.selected_track:
- selected_track = track.index
- self.widgets.video_track.setCurrentIndex(selected_track)
+ for i, track in enumerate(text_video_tracks):
+ if int(track.split(":")[0]) == self.app.fastflix.current_video.video_settings.selected_track:
+ self.widgets.video_track.setCurrentIndex(i)
+ break
+ else:
+ logger.warning(
+ f"Could not find selected track {self.app.fastflix.current_video.video_settings.selected_track} "
+ f"in {text_video_tracks}"
+ )
end_time = self.app.fastflix.current_video.video_settings.end_time or video.duration
if self.app.fastflix.current_video.video_settings.crop:
@@ -1474,7 +1475,7 @@ def reload_video_from_queue(self, video: Video):
self.app.fastflix.current_video.status = Status()
self.loading_video = False
- self.page_update()
+ self.page_update(build_thumbnail=True, force_build_thumbnail=True)
@reusables.log_exception("fastflix", show_traceback=False)
def update_video_info(self, hide_progress=False):
@@ -1627,7 +1628,7 @@ def generate_thumbnail(self):
if not self.input_video or self.loading_video:
return
- settings = self.app.fastflix.current_video.video_settings.dict()
+ settings = self.app.fastflix.current_video.video_settings.model_dump()
if (
self.app.fastflix.current_video.video_settings.video_encoder_settings.pix_fmt == "yuv420p10le"
@@ -1699,7 +1700,7 @@ def resolution_method(self):
def resolution_custom(self):
res = resolutions[self.widgets.resolution_drop_down.currentText()]
if "pixels" in res:
- return res["pixels"]
+ return str(res["pixels"])
if self.widgets.resolution_custom.text().strip():
return self.widgets.resolution_custom.text()
@@ -1792,7 +1793,7 @@ def video_track_update(self):
self.loading_video = False
self.page_update(build_thumbnail=True)
- def page_update(self, build_thumbnail=True):
+ def page_update(self, build_thumbnail=True, force_build_thumbnail=False):
while self.page_updating:
time.sleep(0.1)
self.page_updating = True
@@ -1809,7 +1810,7 @@ def page_update(self, build_thumbnail=True):
f"{int(self.remove_hdr)}:{self.preview_place}:{self.widgets.rotate.currentIndex()}:"
f"{self.widgets.flip.currentIndex()}"
)
- if new_hash == self.last_thumb_hash:
+ if new_hash == self.last_thumb_hash and not force_build_thumbnail:
return
self.last_thumb_hash = new_hash
self.generate_thumbnail()
diff --git a/fastflix/widgets/panels/advanced_panel.py b/fastflix/widgets/panels/advanced_panel.py
index 826f1737..ffe67e05 100644
--- a/fastflix/widgets/panels/advanced_panel.py
+++ b/fastflix/widgets/panels/advanced_panel.py
@@ -228,14 +228,17 @@ def init_video_speed(self):
def init_eq(self):
self.last_row += 1
self.brightness_widget = QtWidgets.QLineEdit()
+ self.brightness_widget.setValidator(QtGui.QDoubleValidator())
self.brightness_widget.setToolTip("Default is: 0")
self.brightness_widget.textChanged.connect(lambda: self.page_update(build_thumbnail=True))
self.contrast_widget = QtWidgets.QLineEdit()
+ self.contrast_widget.setValidator(QtGui.QDoubleValidator())
self.contrast_widget.setToolTip("Default is: 1")
self.contrast_widget.textChanged.connect(lambda: self.page_update(build_thumbnail=True))
self.saturation_widget = QtWidgets.QLineEdit()
+ self.saturation_widget.setValidator(QtGui.QDoubleValidator())
self.saturation_widget.setToolTip("Default is: 1")
self.saturation_widget.textChanged.connect(lambda: self.page_update(build_thumbnail=True))
@@ -398,9 +401,23 @@ def update_settings(self):
self.app.fastflix.current_video.video_settings.tone_map = self.tone_map_widget.currentText()
self.app.fastflix.current_video.video_settings.vsync = non(self.vsync_widget.currentText())
- self.app.fastflix.current_video.video_settings.brightness = self.brightness_widget.text() or None
- self.app.fastflix.current_video.video_settings.saturation = self.saturation_widget.text() or None
- self.app.fastflix.current_video.video_settings.contrast = self.contrast_widget.text() or None
+ try:
+ if self.brightness_widget.text().strip() != "":
+ self.app.fastflix.current_video.video_settings.brightness = str(float(self.brightness_widget.text()))
+ except ValueError:
+ logger.warning("Invalid brightness value")
+
+ try:
+ if self.saturation_widget.text().strip() != "":
+ self.app.fastflix.current_video.video_settings.saturation = str(float(self.saturation_widget.text()))
+ except ValueError:
+ logger.warning("Invalid saturation value")
+
+ try:
+ if self.contrast_widget.text().strip() != "":
+ self.app.fastflix.current_video.video_settings.contrast = str(float(self.contrast_widget.text()))
+ except ValueError:
+ logger.warning("Invalid contrast value")
# self.app.fastflix.current_video.video_settings.first_pass_filters = self.first_filters.text() or None
# self.app.fastflix.current_video.video_settings.second_filters = self.second_filters.text() or None
@@ -454,15 +471,36 @@ def get_settings(self):
maxrate = int(self.maxrate_widget.text())
bufsize = int(self.bufsize_widget.text())
+ contrast = None
+ if self.contrast_widget.text().strip() != "":
+ try:
+ contrast = str(float(self.contrast_widget.text()))
+ except ValueError:
+ logger.warning("Invalid contrast value")
+
+ saturation = None
+ if self.saturation_widget.text().strip() != "":
+ try:
+ saturation = str(float(self.saturation_widget.text()))
+ except ValueError:
+ logger.warning("Invalid saturation value")
+
+ brightness = None
+ if self.brightness_widget.text().strip() != "":
+ try:
+ brightness = str(float(self.brightness_widget.text()))
+ except ValueError:
+ logger.warning("Invalid brightness value")
+
return AdvancedOptions(
video_speed=video_speeds[self.video_speed_widget.currentText()],
deblock=non(self.deblock_widget.currentText()),
deblock_size=int(self.deblock_size_widget.currentText()),
tone_map=self.tone_map_widget.currentText(),
vsync=non(self.vsync_widget.currentText()),
- brightness=(self.brightness_widget.text() or None),
- saturation=(self.saturation_widget.text() or None),
- contrast=(self.contrast_widget.text() or None),
+ brightness=brightness,
+ saturation=saturation,
+ contrast=contrast,
maxrate=maxrate,
bufsize=bufsize,
source_fps=(None if self.incoming_same_as_source.isChecked() else self.incoming_fps_widget.text()),
@@ -679,23 +717,78 @@ def reset(self, settings: VideoSettings = None):
def new_source(self):
self.reset()
- if self.app.fastflix.current_video.color_primaries in ffmpeg_valid_color_primaries:
+ advanced_options: AdvancedOptions = self.app.fastflix.config.opt("advanced_options")
+
+ if color_primaries := advanced_options.color_primaries:
+ self.color_primaries_widget.setCurrentText(color_primaries)
+ elif self.app.fastflix.current_video.color_primaries in ffmpeg_valid_color_primaries:
self.color_primaries_widget.setCurrentIndex(
ffmpeg_valid_color_primaries.index(self.app.fastflix.current_video.color_primaries) + 1
)
else:
self.color_primaries_widget.setCurrentIndex(0)
- if self.app.fastflix.current_video.color_transfer in ffmpeg_valid_color_transfers:
+ if color_transfer := advanced_options.color_transfer:
+ self.color_transfer_widget.setCurrentText(color_transfer)
+ elif self.app.fastflix.current_video.color_transfer in ffmpeg_valid_color_transfers:
self.color_transfer_widget.setCurrentIndex(
ffmpeg_valid_color_transfers.index(self.app.fastflix.current_video.color_transfer) + 1
)
else:
self.color_transfer_widget.setCurrentIndex(0)
- if self.app.fastflix.current_video.color_space in ffmpeg_valid_color_space:
+ if color_space := advanced_options.color_space:
+ self.color_space_widget.setCurrentText(color_space)
+ elif self.app.fastflix.current_video.color_space in ffmpeg_valid_color_space:
self.color_space_widget.setCurrentIndex(
ffmpeg_valid_color_space.index(self.app.fastflix.current_video.color_space) + 1
)
else:
self.color_space_widget.setCurrentIndex(0)
+
+ if video_speed := advanced_options.video_speed:
+ self.video_speed_widget.setCurrentText(get_key(video_speeds, video_speed))
+
+ if deblock := advanced_options.deblock:
+ self.deblock_widget.setCurrentText(deblock)
+
+ if deblock_size := advanced_options.deblock_size:
+ self.deblock_size_widget.setCurrentText(str(deblock_size))
+
+ if tone_map := advanced_options.tone_map:
+ self.tone_map_widget.setCurrentText(tone_map)
+
+ if vsync := advanced_options.vsync:
+ self.vsync_widget.setCurrentText(vsync)
+
+ if brightness := advanced_options.brightness:
+ self.brightness_widget.setText(brightness)
+
+ if saturation := advanced_options.saturation:
+ self.saturation_widget.setText(saturation)
+
+ if contrast := advanced_options.contrast:
+ self.contrast_widget.setText(contrast)
+
+ if maxrate := advanced_options.maxrate:
+ self.maxrate_widget.setText(str(maxrate))
+
+ if bufsize := advanced_options.bufsize:
+ self.bufsize_widget.setText(str(bufsize))
+
+ if source_fps := advanced_options.source_fps:
+ self.incoming_fps_widget.setText(source_fps)
+ self.incoming_same_as_source.setChecked(False)
+ else:
+ self.incoming_same_as_source.setChecked(True)
+
+ if output_fps := advanced_options.output_fps:
+ self.outgoing_fps_widget.setText(output_fps)
+ self.outgoing_same_as_source.setChecked(False)
+ else:
+ self.outgoing_same_as_source.setChecked(True)
+
+ if denoise_type_index := advanced_options.denoise_type_index:
+ self.denoise_type_widget.setCurrentIndex(denoise_type_index)
+ if denoise_strength_index := advanced_options.denoise_strength_index:
+ self.denoise_strength_widget.setCurrentIndex(denoise_strength_index)
diff --git a/fastflix/widgets/panels/audio_panel.py b/fastflix/widgets/panels/audio_panel.py
index ee08e360..43422405 100644
--- a/fastflix/widgets/panels/audio_panel.py
+++ b/fastflix/widgets/panels/audio_panel.py
@@ -152,6 +152,7 @@ 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.check_dis_button()
self.conversion_box = None
self.loading = False
@@ -202,6 +203,7 @@ def page_update(self):
self.app.fastflix.current_video.audio_tracks[self.index].language = self.language
if not self.loading:
self.check_conversion_button()
+ self.check_dis_button()
return self.parent.main.page_update(build_thumbnail=False)
@property
@@ -286,6 +288,13 @@ def check_conversion_button(self):
self.widgets.conversion.setStyleSheet("")
self.widgets.conversion.setText(t("Conversion"))
+ def check_dis_button(self):
+ audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[self.index]
+ if any(audio_track.dispositions.values()):
+ self.widgets.disposition.setStyleSheet("border-color: #0055ff")
+ else:
+ self.widgets.disposition.setStyleSheet("")
+
class AudioList(FlixList):
def __init__(self, parent, app: FastFlixApp):
diff --git a/fastflix/widgets/panels/debug_panel.py b/fastflix/widgets/panels/debug_panel.py
index 1761d17f..7bc57786 100644
--- a/fastflix/widgets/panels/debug_panel.py
+++ b/fastflix/widgets/panels/debug_panel.py
@@ -21,13 +21,13 @@ def __init__(self, parent, app: FastFlixApp):
if not DEVMODE:
self.hide()
return
- self.addTab(self.get_textbox(Box(self.app.fastflix.config.dict())), "Config")
+ self.addTab(self.get_textbox(Box(self.app.fastflix.config.model_dump())), "Config")
self.addTab(self.get_textbox(Box(self.get_ffmpeg_details())), "FFmpeg Details")
self.addTab(self.get_textbox(BoxList(self.app.fastflix.conversion_list)), "Queue")
self.addTab(self.get_textbox(Box(self.app.fastflix.encoders)), "Encoders")
self.addTab(self.get_textbox(BoxList(self.app.fastflix.audio_encoders)), "Audio Encoders")
if self.app.fastflix.current_video:
- self.cv = self.get_textbox(Box(self.app.fastflix.current_video.dict()))
+ self.cv = self.get_textbox(Box(self.app.fastflix.current_video.model_dump()))
self.addTab(self.cv, "Current Video")
def get_textbox(self, obj: Union["Box", "BoxList"]) -> "QtWidgets.QTextBrowser":
@@ -56,5 +56,5 @@ def reset(self):
self.removeTab(self.count() - 1)
self.cv.close()
del self.cv
- self.cv = self.get_textbox(Box(self.app.fastflix.current_video.dict()))
+ self.cv = self.get_textbox(Box(self.app.fastflix.current_video.model_dump()))
self.addTab(self.cv, "Current Video")
diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py
index 64eb4e85..138113c2 100644
--- a/fastflix/widgets/panels/queue_panel.py
+++ b/fastflix/widgets/panels/queue_panel.py
@@ -86,7 +86,7 @@ def __init__(self, parent, video: Video, index, first=False):
)
title.setFixedWidth(300)
- settings = Box(copy.deepcopy(video.video_settings.dict()))
+ settings = Box(copy.deepcopy(video.video_settings.model_dump()))
# settings.output_path = str(settings.output_path)
# for i, o in enumerate(video.attachment_tracks):
# if o.file_path:
diff --git a/fastflix/widgets/panels/subtitle_panel.py b/fastflix/widgets/panels/subtitle_panel.py
index e0e0f8c8..cdd14627 100644
--- a/fastflix/widgets/panels/subtitle_panel.py
+++ b/fastflix/widgets/panels/subtitle_panel.py
@@ -143,6 +143,7 @@ def __init__(self, app, parent, index, enabled=True, first=False):
self.grid.addWidget(self.widgets.enable_check, 0, 8)
self.setLayout(self.grid)
+ self.check_dis_button()
self.loading = False
self.updating_burn = False
self.extract_completed_signal.connect(self.extraction_complete)
@@ -242,8 +243,16 @@ def update_burn_in(self):
def page_update(self):
if not self.loading:
+ self.check_dis_button()
return self.parent.main.page_update(build_thumbnail=False)
+ def check_dis_button(self):
+ track: SubtitleTrack = self.app.fastflix.current_video.subtitle_tracks[self.index]
+ if any(track.dispositions.values()):
+ self.widgets.disposition.setStyleSheet("border-color: #0055ff")
+ else:
+ self.widgets.disposition.setStyleSheet("")
+
class SubtitleList(FlixList):
def __init__(self, parent, app: FastFlixApp):
diff --git a/fastflix/widgets/windows/audio_conversion.py b/fastflix/widgets/windows/audio_conversion.py
index 0bc9c099..45db840a 100644
--- a/fastflix/widgets/windows/audio_conversion.py
+++ b/fastflix/widgets/windows/audio_conversion.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import logging
-from PySide6 import QtWidgets
+from PySide6 import QtWidgets, QtGui
from fastflix.models.fastflix_app import FastFlixApp
from fastflix.models.encode import AudioTrack
@@ -109,6 +109,7 @@ def __init__(self, app: FastFlixApp, track_index, encoders, audio_track_update):
self.aq.currentIndexChanged.connect(self.set_aq)
self.bitrate = QtWidgets.QLineEdit()
self.bitrate.setFixedWidth(50)
+ self.bitrate.setValidator(QtGui.QDoubleValidator())
if self.audio_track.conversion_aq:
self.aq.setCurrentIndex(self.audio_track.conversion_aq)
diff --git a/fastflix/widgets/windows/disposition.py b/fastflix/widgets/windows/disposition.py
index 9ad0d9e5..fd0632e4 100644
--- a/fastflix/widgets/windows/disposition.py
+++ b/fastflix/widgets/windows/disposition.py
@@ -45,12 +45,16 @@ def __init__(self, app: FastFlixApp, parent, track_name, track_index, audio=True
self.track_index = track_index
self.audio = audio
- self.setMinimumWidth(400)
+ self.setMinimumWidth(200)
self.forced = QtWidgets.QCheckBox(t("Forced"))
self.default = QtWidgets.QCheckBox(t("Default"))
+ track = self.get_track()
+ self.forced.setChecked(track.dispositions.get("forced", False))
+ self.default.setChecked(track.dispositions.get("default", False))
+
layout = QtWidgets.QVBoxLayout()
layout.addWidget(QtWidgets.QLabel(track_name))
layout.addWidget(self.default)
@@ -71,16 +75,16 @@ def __init__(self, app: FastFlixApp, parent, track_name, track_index, audio=True
group.addButton(none_extra)
layout.addWidget(none_extra)
- 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 subtitle_disposition_options:
- self.widgets[dis] = QtWidgets.QRadioButton(t(dis))
- group.addButton(self.widgets[dis])
- layout.addWidget(self.widgets[dis])
+ for dis in audio_disposition_options if audio else subtitle_disposition_options:
+ self.widgets[dis] = QtWidgets.QRadioButton(t(dis))
+ group.addButton(self.widgets[dis])
+ layout.addWidget(self.widgets[dis])
+
+ for track_dis, is_set in track.dispositions.items():
+ if is_set and track_dis in self.widgets.keys():
+ self.widgets[track_dis].setChecked(True)
+
+ self.parent.page_update()
self.set_button = QtWidgets.QPushButton(t("Set"))
self.set_button.clicked.connect(self.set_dispositions)
@@ -88,11 +92,13 @@ def __init__(self, app: FastFlixApp, parent, track_name, track_index, audio=True
self.setLayout(layout)
- def set_dispositions(self):
+ def get_track(self):
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]
+ return self.app.fastflix.current_video.audio_tracks[self.track_index]
+ return self.app.fastflix.current_video.subtitle_tracks[self.track_index]
+
+ def set_dispositions(self):
+ track = self.get_track()
track.dispositions["forced"] = self.forced.isChecked()
track.dispositions["default"] = self.default.isChecked()
diff --git a/fastflix/widgets/windows/large_preview.py b/fastflix/widgets/windows/large_preview.py
index b51a3552..8f4e3ca6 100644
--- a/fastflix/widgets/windows/large_preview.py
+++ b/fastflix/widgets/windows/large_preview.py
@@ -57,7 +57,7 @@ def keyPressEvent(self, a0: QtGui.QKeyEvent) -> None:
super(LargePreview, self).keyPressEvent(a0)
def generate_image(self):
- settings = self.main.app.fastflix.current_video.video_settings.dict()
+ settings = self.main.app.fastflix.current_video.video_settings.model_dump()
if not self.main.app.fastflix.current_video.video_settings.video_encoder_settings:
return
diff --git a/fastflix/widgets/windows/profile_window.py b/fastflix/widgets/windows/profile_window.py
index a42c672a..e31e8fa7 100644
--- a/fastflix/widgets/windows/profile_window.py
+++ b/fastflix/widgets/windows/profile_window.py
@@ -332,7 +332,7 @@ def __init__(self, advanced_settings):
def text_update(self, advanced_settings):
ignored = ("color_primaries", "color_transfer", "color_space", "denoise_type_index", "denoise_strength_index")
- settings = "\n".join(f"{k:<30} {v}" for k, v in advanced_settings.dict().items() if k not in ignored)
+ settings = "\n".join(f"{k:<30} {v}" for k, v in advanced_settings.model_dump().items() if k not in ignored)
self.label.setText(f"
{settings}
")
@@ -371,7 +371,7 @@ def __init__(self, app, main):
self.setLayout(layout)
def update_settings(self):
- settings = "\n".join(f"{k:<30} {v}" for k, v in self.main.encoder.dict().items())
+ settings = "\n".join(f"{k:<30} {v}" for k, v in self.main.encoder.model_dump().items())
self.label.setText(f"{settings}
")
diff --git a/pyproject.toml b/pyproject.toml
index c92c2539..00577405 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,7 +33,7 @@ dependencies = [
"packaging>=23.2",
"pathvalidate>=2.4,<3.0",
"psutil>=5.9,<6.0",
- "pydantic>=1.9,<2.0",
+ "pydantic>=2.0,<3.0",
"pyside6>=6.4.2",
"python-box[all]>=6.0,<7.0",
"requests>=2.28,<3.0",
diff --git a/scripts/build_mac_app.py b/scripts/build_mac_app.py
new file mode 100644
index 00000000..a3b04eff
--- /dev/null
+++ b/scripts/build_mac_app.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+from pathlib import Path
+import sys
+import shutil
+from subprocess import check_output
+import platform
+import reusables
+
+from fastflix.version import __version__
+
+arch = "arm64" if "arm64" in platform.platform() else "x86_64"
+
+here = Path(__file__).parent
+plist_template = here.parent / "fastflix" / "data" / "Info.plist.template"
+
+build_folder = Path(here.parent / "dist" / "FastFlix.app")
+build_folder.mkdir(exist_ok=True)
+
+content_folder = build_folder / "Contents"
+content_folder.mkdir(exist_ok=True)
+
+macos_folder = content_folder / "MacOS"
+macos_folder.mkdir(exist_ok=True)
+
+resources_folder = content_folder / "Resources"
+resources_folder.mkdir(exist_ok=True)
+
+try:
+ mac_version = f"{sys.argv[1].split("-")[1]}.0"
+ assert mac_version in ("12.0", "13.0", "14.0", "15.0")
+except Exception:
+ print(f"Did not get expected input, received: {sys.argv}")
+ sys.exit(1)
+
+with open(plist_template) as in_file, open(content_folder / "Info.plist", "w") as out_file:
+ template = in_file.read().format(version=__version__, mac_version=mac_version)
+ out_file.write(template)
+
+shutil.copy(here.parent / "fastflix" / "data" / "icon.icns", resources_folder / "icon.icns")
+
+shutil.move(here.parent / "dist" / "FastFlix", macos_folder / "FastFlix")
+shutil.move(here.parent / "dist" / "LICENSE", macos_folder / "LICENSE")
diff --git a/scripts/get_arch.py b/scripts/get_arch.py
new file mode 100644
index 00000000..c88ca854
--- /dev/null
+++ b/scripts/get_arch.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import sys
+import platform
+
+
+def write_and_exit(msg):
+ sys.stdout.write(msg)
+ sys.stdout.flush()
+ sys.exit(0)
+
+
+write_and_exit("arm64" if "arm64" in platform.platform() else "x86_64")
diff --git a/velocemente/__init__.py b/velocemente/__init__.py
deleted file mode 100644
index e69de29b..00000000