diff --git a/CHANGES b/CHANGES index b20934fd..a37a379a 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,8 @@ * 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 #582 (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) diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml index 5c87270d..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 diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py index e1ada880..9db349f4 100644 --- a/fastflix/encoders/common/helpers.py +++ b/fastflix/encoders/common/helpers.py @@ -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 9e7e9f38..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): 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..bdacaed1 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() 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..6c749804 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,37 @@ 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]) + _, qscale = self.get_mode_settings() + try: + settings.qscale = float(qscale) + except ValueError: + logger.warning("Invalid Qscale, using default 75") + settings.qscale = 75 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 77c5a1db..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 @@ -126,6 +126,13 @@ 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: str = "HEVC (NVEncC)" @@ -160,6 +167,13 @@ 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: str = "AV1 (NVEncC)" @@ -194,6 +208,13 @@ 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: str = "HEVC (QSVEncC)" @@ -220,6 +241,13 @@ 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: str = "AV1 (QSVEncC)" @@ -246,6 +274,13 @@ 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: str = "AVC (QSVEncC)" @@ -271,6 +306,13 @@ 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: str = "AVC (NVEncC)" @@ -303,6 +345,13 @@ 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: str = "HEVC (VCEEncC)" @@ -338,6 +387,13 @@ 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: str = "AV1 (VCEEncC)" @@ -373,6 +429,13 @@ class VCEEncCAV1Settings(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 VCEEncCAVCSettings(EncoderSettings): name: str = "AVC (VCEEncC)" @@ -407,6 +470,13 @@ 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: str = "AV1 (rav1e)" @@ -495,19 +565,44 @@ class AOMAV1Settings(EncoderSettings): class WebPSettings(EncoderSettings): name: str = "WebP" - lossless: str = "0" + 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: str = "GIF" - fps: int = 15 + 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: str = "Copy" diff --git a/fastflix/models/profiles.py b/fastflix/models/profiles.py index 7a8fa281..ea8e75fe 100644 --- a/fastflix/models/profiles.py +++ b/fastflix/models/profiles.py @@ -61,21 +61,21 @@ class AudioMatch(BaseModel): bitrate: Optional[str] = None downmix: Optional[Union[str, int]] = None - @field_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) - @field_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) - @field_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"} @@ -87,7 +87,7 @@ def downmix_as_string(cls, v): return None return v - @field_validator("bitrate") + @field_validator("bitrate", mode="before") @classmethod def bitrate_k_end(cls, v): if v and not v.endswith("k"): diff --git a/fastflix/models/video.py b/fastflix/models/video.py index 559d7b47..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,9 +106,9 @@ 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 + brightness: Optional[str] = None + contrast: Optional[str] = None + saturation: Optional[str] = None video_encoder_settings: Optional[ Union[ x265Settings, @@ -145,6 +145,27 @@ class VideoSettings(BaseModel): # 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 diff --git a/fastflix/version.py b/fastflix/version.py index 04096d1c..f52a5951 100644 --- a/fastflix/version.py +++ b/fastflix/version.py @@ -1,4 +1,4 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -__version__ = "5.8.0" +__version__ = "5.8.0b0" __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 13c5e455..e60b7ee6 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1625,7 +1625,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" diff --git a/fastflix/widgets/panels/advanced_panel.py b/fastflix/widgets/panels/advanced_panel.py index 571f7a7e..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)) 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/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/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}
")