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