diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 8d70979e..4f939f34 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -7,6 +7,14 @@ assignees: ''
---
+
+
+
+
+
+
+
+
**FastFlix Version:**
**Operating System:**
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index bed7fa1e..0532410a 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -7,6 +7,15 @@ assignees: ''
---
+
+
+
+
+
+
+
+
+
Please use Discussions for new Features! They can be voted upon, and that way I can work in the most demanded features first.
Don't forget to first search for the feature you want, as well as upvote others you would like to see!
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index e1cc29b5..f48cb765 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -17,7 +17,7 @@ jobs:
- uses: actions/setup-python@v4
with:
- python-version: "3.11"
+ python-version: "3.12"
- name: Gather build version (*nix)
run: |
@@ -74,7 +74,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
- python-version: "3.11"
+ python-version: "3.12"
- name: Gather build version
shell: powershell
diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml
index efad57e2..de732a22 100644
--- a/.github/workflows/pythonpublish.yml
+++ b/.github/workflows/pythonpublish.yml
@@ -18,7 +18,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
- python-version: '3.11'
+ python-version: '3.12'
- name: Install Dependencies
run: |
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 6052c1ef..48ff4010 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -16,7 +16,7 @@ jobs:
- uses: actions/setup-python@v3
with:
- python-version: "3.11"
+ python-version: "3.12"
- run: pip install black==23.7.0
- run: python -m black --check .
@@ -29,7 +29,7 @@ jobs:
- uses: actions/setup-python@v3
with:
- python-version: "3.11"
+ python-version: "3.12"
- name: Install PySide6 requirements
run: |
diff --git a/CHANGES b/CHANGES
index ade0d083..14a9d1de 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,5 +1,13 @@
# Changelog
+## Version 5.7.0
+
+* Adding new audio encoding panel
+* Adding support for audio quality targeting instead of bitrate
+* Fixing that audio and subtitles would be reset on change of encoder
+* Fixing #543 systems with more than one opencl device would break thumbnails and some encodings (thanks to swadomlic)
+* Fixing #505 (maybe) trying new methods to clean file paths for subtitles (thanks to Maddie Davis)
+
## Version 5.6.0
* Adding Passes option for bitrate mode in x265 and x264 (thanks to Chriss)
diff --git a/FastFlix.nsi b/FastFlix.nsi
index ad5c0c1d..02fff751 100644
--- a/FastFlix.nsi
+++ b/FastFlix.nsi
@@ -48,7 +48,7 @@ InstallDirRegKey HKLM "Software\FastFlix" "Install_Dir"
Function .onInit
ReadRegStr $0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\FastFlix" "UninstallString"
${If} $0 != ""
- Messagebox MB_OK|MB_ICONINFORMATION "You will now be prompted to first uninstall the previous version of FastFlix"
+ Messagebox MB_OK|MB_ICONINFORMATION "You will now be prompted to first uninstall the previous version of FastFlix. Please ensure it is not currently running!"
ExecWait '$0 _?=$INSTDIR'
${EndIf}
FunctionEnd
diff --git a/FastFlix_Nix_OneFile.spec b/FastFlix_Nix_OneFile.spec
index 7bbbbd3e..fb2141b8 100644
--- a/FastFlix_Nix_OneFile.spec
+++ b/FastFlix_Nix_OneFile.spec
@@ -19,6 +19,11 @@ with open("pyproject.toml") as f:
if package not in ("pyinstaller"):
all_imports.append(package)
+all_imports.remove("iso639-lang")
+all_imports.remove("python-box")
+all_imports.append("box")
+all_imports.append("iso639")
+
a = Analysis(['fastflix/__main__.py'],
binaries=[],
datas=[('iso-639-3.tab', 'iso639'), ('iso-639-3.json', 'iso639'), ('CHANGES', 'fastflix/.'), ('docs/build-licenses.txt', 'docs')] + all_fastflix_files,
diff --git a/FastFlix_Windows_Installer.spec b/FastFlix_Windows_Installer.spec
index 0eaca049..5a1fa36c 100644
--- a/FastFlix_Windows_Installer.spec
+++ b/FastFlix_Windows_Installer.spec
@@ -19,6 +19,11 @@ with open("pyproject.toml") as f:
if package not in ("pyinstaller"):
all_imports.append(package)
+all_imports.remove("iso639-lang")
+all_imports.remove("python-box")
+all_imports.append("box")
+all_imports.append("iso639")
+
a = Analysis(['fastflix\\__main__.py'],
binaries=[],
datas=[('iso-639-3.tab', 'iso639'), ('iso-639-3.json', 'iso639'), ('CHANGES', 'fastflix\\.'), ('docs\\build-licenses.txt', 'docs')] + all_fastflix_files,
diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec
index 92a42bf9..89c61a7c 100644
--- a/FastFlix_Windows_OneFile.spec
+++ b/FastFlix_Windows_OneFile.spec
@@ -22,6 +22,11 @@ with open("pyproject.toml") as f:
if package not in ("pyinstaller"):
all_imports.append(package)
+all_imports.remove("iso639-lang")
+all_imports.remove("python-box")
+all_imports.append("box")
+all_imports.append("iso639")
+
portable_file = "fastflix\\portable.py"
with open(portable_file, "w") as portable:
portable.write(" ")
diff --git a/LICENSE b/LICENSE
index 9e804e90..5f8d3b14 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License
-Copyright (c) 2019-2023 Chris Griffith
+Copyright (c) 2019-2024 Chris Griffith
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index bebeb99c..3a1bd272 100644
--- a/README.md
+++ b/README.md
@@ -140,7 +140,7 @@ Special thanks to [leonardyan](https://github.com/leonardyan) for numerous Chine
# License
-Copyright (C) 2019-2023 Chris Griffith
+Copyright (C) 2019-2024 Chris Griffith
The code itself is licensed under the MIT which you can read in the `LICENSE` file.
Read more about the release licensing in the [docs](docs/README.md) folder.
diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml
index 675f91cf..0a8ce704 100644
--- a/fastflix/data/languages.yaml
+++ b/fastflix/data/languages.yaml
@@ -10595,3 +10595,123 @@ Custom:
ukr: Нестандартний
kor: 사용자 지정
ron: Personalizat
+Codec:
+ eng: Codec
+ deu: Codec
+ fra: Codec
+ ita: Codec
+ spa: Códec
+ jpn: コーデック
+ rus: Кодек
+ por: Codec
+ swe: Kodning
+ pol: Kodek
+ chs: 编解码器
+ ukr: Кодек
+ kor: 코덱
+ ron: Codec
+Near Lossless:
+ eng: Near Lossless
+ deu: Fast verlustfrei
+ fra: Presque sans perte
+ ita: Quasi senza perdita
+ spa: Casi sin pérdidas
+ jpn: ニア・ロスレス
+ rus: Почти без потерь
+ por: Quase sem perdas
+ swe: Nära förlustfri
+ pol: Near Lossless
+ chs: 近乎无损
+ ukr: Майже без втрат
+ kor: 거의 무손실
+ ron: Aproape fără pierderi
+High Quality:
+ eng: High Quality
+ deu: Hohe Qualität
+ fra: Haute qualité
+ ita: Alta qualità
+ spa: Alta calidad
+ jpn: 高品質
+ rus: Высокое качество
+ por: Alta qualidade
+ swe: Hög kvalitet
+ pol: Wysoka jakość
+ chs: 高质量
+ ukr: Висока якість
+ kor: 고품질
+ ron: De înaltă calitate
+Medium Quality:
+ eng: Medium Quality
+ deu: Mittlere Qualität
+ fra: Qualité moyenne
+ ita: Qualità media
+ spa: Calidad media
+ jpn: ミディアム・クオリティ
+ rus: Среднее качество
+ por: Qualidade média
+ swe: Medelhög kvalitet
+ pol: Średnia jakość
+ chs: 中等质量
+ ukr: Середня якість
+ kor: 중간 품질
+ ron: Calitate medie
+Low Quality:
+ eng: Low Quality
+ deu: Geringe Qualität
+ fra: Faible qualité
+ ita: Bassa qualità
+ spa: Baja calidad
+ jpn: 低品質
+ rus: Низкое качество
+ por: Baixa qualidade
+ swe: Låg kvalitet
+ pol: Niska jakość
+ chs: 低质量
+ ukr: Низька якість
+ kor: 낮은 품질
+ ron: Calitate scăzută
+Custom Bitrate:
+ eng: Custom Bitrate
+ deu: Benutzerdefinierte Bitrate
+ fra: Bitrate personnalisé
+ ita: Bitrate personalizzato
+ spa: Velocidad de bits personalizada
+ jpn: カスタム・ビットレート
+ rus: Пользовательский битрейт
+ por: Taxa de bits personalizada
+ swe: Anpassad bitrate
+ pol: Niestandardowa szybkość transmisji
+ chs: 自定义比特率
+ ukr: Користувацький бітрейт
+ kor: 사용자 지정 비트레이트
+ ron: Bitrate personalizat
+Audio Quality:
+ eng: Audio Quality
+ deu: Audio-Qualität
+ fra: Qualité audio
+ ita: Qualità audio
+ spa: Calidad de audio
+ jpn: オーディオ品質
+ rus: Качество звука
+ por: Qualidade áudio
+ swe: Ljudkvalitet
+ pol: Jakość dźwięku
+ chs: 音频质量
+ ukr: Якість звуку
+ kor: 오디오 품질
+ ron: Calitatea audio
+Channel Layout:
+ eng: Channel Layout
+ deu: Kanal-Layout
+ fra: Disposition des canaux
+ ita: Layout del canale
+ spa: Disposición de los canales
+ jpn: チャンネルレイアウト
+ rus: Расположение каналов
+ por: Disposição dos canais
+ swe: Kanalens layout
+ pol: Układ kanałów
+ chs: 通道布局
+ ukr: Розташування каналів
+ kor: 채널 레이아웃
+ ron: Canal Layout
diff --git a/fastflix/encoders/common/audio.py b/fastflix/encoders/common/audio.py
index 4b924f3d..b84421a9 100644
--- a/fastflix/encoders/common/audio.py
+++ b/fastflix/encoders/common/audio.py
@@ -13,6 +13,7 @@
"quad(side)": 4,
"5.0": 5,
"5.1": 6,
+ "5.1(side)": 6,
"6.0": 6,
"6.0(front)": 6,
"hexagonal": 6,
@@ -30,6 +31,8 @@
def build_audio(audio_tracks, audio_file_index=0):
command_list = []
for track in audio_tracks:
+ if not track.enabled:
+ continue
command_list.append(
f"-map {audio_file_index}:{track.index} "
f'-metadata:s:{track.outdex} title="{track.title}" '
@@ -40,14 +43,16 @@ def build_audio(audio_tracks, audio_file_index=0):
if not track.conversion_codec or track.conversion_codec == "none":
command_list.append(f"-c:{track.outdex} copy")
elif track.conversion_codec:
+ cl = track.downmix if "downmix" in track and track.downmix else track.raw_info.channel_layout
downmix = (
- f"-ac:{track.outdex} {channel_list[track.downmix]} -filter:{track.outdex} aformat=channel_layouts={track.downmix}"
+ f"-ac:{track.outdex} {channel_list[cl]} -filter:{track.outdex} aformat=channel_layouts={cl}"
if track.downmix
else ""
)
bitrate = ""
if track.conversion_codec not in lossless:
- bitrate = f"-b:{track.outdex} {track.conversion_bitrate} "
+ channel_layout = f'-filter:{track.outdex} aformat=channel_layouts="{track.raw_info.channel_layout}"'
+ bitrate = f"-b:{track.outdex} {track.conversion_bitrate} {channel_layout}"
command_list.append(f"-c:{track.outdex} {track.conversion_codec} {bitrate} {downmix}")
if getattr(track, "dispositions", None):
diff --git a/fastflix/encoders/common/encc_helpers.py b/fastflix/encoders/common/encc_helpers.py
index 8efcfbbf..05f1f84b 100644
--- a/fastflix/encoders/common/encc_helpers.py
+++ b/fastflix/encoders/common/encc_helpers.py
@@ -86,6 +86,8 @@ def build_audio(audio_tracks: list[AudioTrack], audio_streams):
stream_ids = get_stream_pos(audio_streams)
for track in sorted(audio_tracks, key=lambda x: x.outdex):
+ if not track.enabled:
+ continue
if track.index in track_ids:
logger.warning("*EncC does not support copy and duplicate of audio tracks!")
track_ids.add(track.index)
@@ -98,7 +100,10 @@ def build_audio(audio_tracks: list[AudioTrack], audio_streams):
downmix = f"--audio-stream {audio_id}?:{track.downmix}" if track.downmix else ""
bitrate = ""
if track.conversion_codec not in lossless:
- bitrate = f"--audio-bitrate {audio_id}?{track.conversion_bitrate.rstrip('k')} "
+ if track.conversion_bitrate:
+ bitrate = f"--audio-bitrate {audio_id}?{track.conversion_bitrate} "
+ else:
+ bitrate = f"--audio-quality {audio_id}?{track.conversion_aq} "
command_list.append(
f"{downmix} --audio-codec {audio_id}?{track.conversion_codec} {bitrate} "
f"--audio-metadata {audio_id}?clear"
@@ -130,6 +135,8 @@ def build_subtitle(subtitle_tracks: list[SubtitleTrack], subtitle_streams, video
scale = ",scale=2.0" if video_height > 1800 else ""
for track in sorted(subtitle_tracks, key=lambda x: x.outdex):
+ if not track.enabled:
+ continue
sub_id = stream_ids[track.index]
if track.burn_in:
command_list.append(f"--vpp-subburn track={sub_id}{scale}")
diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py
index 5abe48ed..637fad8e 100644
--- a/fastflix/encoders/common/helpers.py
+++ b/fastflix/encoders/common/helpers.py
@@ -77,7 +77,7 @@ def generate_ffmpeg_start(
[
f'"{ffmpeg}"',
start_extra,
- ("-init_hw_device opencl=ocl -filter_hw_device ocl " if enable_opencl and remove_hdr else ""),
+ ("-init_hw_device opencl:0.0=ocl -filter_hw_device ocl " if enable_opencl and remove_hdr else ""),
"-y",
time_one,
incoming_fps,
@@ -250,18 +250,18 @@ def generate_all(
) -> Tuple[str, str, str]:
settings = fastflix.current_video.video_settings.video_encoder_settings
- audio = build_audio(fastflix.current_video.video_settings.audio_tracks) if audio else ""
+ audio = build_audio(fastflix.current_video.audio_tracks) if audio else ""
subtitles, burn_in_track, burn_in_type = "", None, None
if subs:
- subtitles, burn_in_track, burn_in_type = build_subtitle(fastflix.current_video.video_settings.subtitle_tracks)
+ subtitles, burn_in_track, burn_in_type = build_subtitle(fastflix.current_video.subtitle_tracks)
if burn_in_type == "text":
for i, x in enumerate(fastflix.current_video.streams["subtitle"]):
if x["index"] == burn_in_track:
burn_in_track = i
break
- attachments = build_attachments(fastflix.current_video.video_settings.attachment_tracks)
+ attachments = build_attachments(fastflix.current_video.attachment_tracks)
enable_opencl = fastflix.opencl_support
if "enable_opencl" in filters_extra:
diff --git a/fastflix/encoders/common/subtitles.py b/fastflix/encoders/common/subtitles.py
index 6726b063..1103f545 100644
--- a/fastflix/encoders/common/subtitles.py
+++ b/fastflix/encoders/common/subtitles.py
@@ -14,6 +14,8 @@ def build_subtitle(
burn_in_type = None
subs_enabled = False
for track in subtitle_tracks:
+ if not track.enabled:
+ continue
if track.burn_in:
burn_in_track = track.index
burn_in_type = track.subtitle_type
diff --git a/fastflix/encoders/copy/command_builder.py b/fastflix/encoders/copy/command_builder.py
index b7379653..54d25212 100644
--- a/fastflix/encoders/copy/command_builder.py
+++ b/fastflix/encoders/copy/command_builder.py
@@ -16,6 +16,8 @@ def build(fastflix: FastFlix):
rotation = abs(int(fastflix.current_video.current_video_stream.side_data_list[0].rotation))
rot = ""
+ # if fastflix.current_video.video_settings.rotate != 0:
+ # rot = f"-display_rotation:s:v {rotation + (fastflix.current_video.video_settings.rotate * 90)}"
if fastflix.current_video.video_settings.output_path.name.lower().endswith("mp4"):
rot = f"-metadata:s:v rotate={rotation + (fastflix.current_video.video_settings.rotate * 90)}"
diff --git a/fastflix/encoders/nvencc_av1/command_builder.py b/fastflix/encoders/nvencc_av1/command_builder.py
index 522a537a..011054b6 100644
--- a/fastflix/encoders/nvencc_av1/command_builder.py
+++ b/fastflix/encoders/nvencc_av1/command_builder.py
@@ -162,8 +162,8 @@ def build(fastflix: FastFlix):
(f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""),
remove_hdr,
"--psnr --ssim" if settings.metrics else "",
- build_audio(video.video_settings.audio_tracks, video.streams.audio),
- build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height),
+ build_audio(video.audio_tracks, video.streams.audio),
+ build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height),
settings.extra,
"-o",
f'"{clean_file_string(video.video_settings.output_path)}"',
diff --git a/fastflix/encoders/nvencc_avc/command_builder.py b/fastflix/encoders/nvencc_avc/command_builder.py
index 80d04a27..2ccf23f6 100644
--- a/fastflix/encoders/nvencc_avc/command_builder.py
+++ b/fastflix/encoders/nvencc_avc/command_builder.py
@@ -131,8 +131,8 @@ def build(fastflix: FastFlix):
(f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""),
remove_hdr,
"--psnr --ssim" if settings.metrics else "",
- build_audio(video.video_settings.audio_tracks, video.streams.audio),
- build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height),
+ build_audio(video.audio_tracks, video.streams.audio),
+ build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height),
settings.extra,
"-o",
f'"{clean_file_string(video.video_settings.output_path)}"',
diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py
index 1484dc68..c72c96f8 100644
--- a/fastflix/encoders/nvencc_hevc/command_builder.py
+++ b/fastflix/encoders/nvencc_hevc/command_builder.py
@@ -162,8 +162,8 @@ def build(fastflix: FastFlix):
(f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""),
remove_hdr,
"--psnr --ssim" if settings.metrics else "",
- build_audio(video.video_settings.audio_tracks, video.streams.audio),
- build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height),
+ build_audio(video.audio_tracks, video.streams.audio),
+ build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height),
settings.extra,
"-o",
f'"{clean_file_string(video.video_settings.output_path)}"',
diff --git a/fastflix/encoders/qsvencc_av1/command_builder.py b/fastflix/encoders/qsvencc_av1/command_builder.py
index 019afe23..45a8b60a 100644
--- a/fastflix/encoders/qsvencc_av1/command_builder.py
+++ b/fastflix/encoders/qsvencc_av1/command_builder.py
@@ -143,8 +143,8 @@ def build(fastflix: FastFlix):
("--adapt-ltr" if settings.adapt_ltr else ""),
("--adapt-cqm" if settings.adapt_cqm else ""),
"--psnr --ssim" if settings.metrics else "",
- build_audio(video.video_settings.audio_tracks, video.streams.audio),
- build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height),
+ build_audio(video.audio_tracks, video.streams.audio),
+ build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height),
settings.extra,
"-o",
f'"{clean_file_string(video.video_settings.output_path)}"',
diff --git a/fastflix/encoders/qsvencc_avc/command_builder.py b/fastflix/encoders/qsvencc_avc/command_builder.py
index 2d54642f..0b9ac31a 100644
--- a/fastflix/encoders/qsvencc_avc/command_builder.py
+++ b/fastflix/encoders/qsvencc_avc/command_builder.py
@@ -124,8 +124,8 @@ def build(fastflix: FastFlix):
("--adapt-ltr" if settings.adapt_ltr else ""),
("--adapt-cqm" if settings.adapt_cqm else ""),
"--psnr --ssim" if settings.metrics else "",
- build_audio(video.video_settings.audio_tracks, video.streams.audio),
- build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height),
+ build_audio(video.audio_tracks, video.streams.audio),
+ build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height),
settings.extra,
"-o",
f'"{clean_file_string(video.video_settings.output_path)}"',
diff --git a/fastflix/encoders/qsvencc_hevc/command_builder.py b/fastflix/encoders/qsvencc_hevc/command_builder.py
index dfe65639..c1ef50ed 100644
--- a/fastflix/encoders/qsvencc_hevc/command_builder.py
+++ b/fastflix/encoders/qsvencc_hevc/command_builder.py
@@ -143,8 +143,8 @@ def build(fastflix: FastFlix):
("--adapt-ltr" if settings.adapt_ltr else ""),
("--adapt-cqm" if settings.adapt_cqm else ""),
"--psnr --ssim" if settings.metrics else "",
- build_audio(video.video_settings.audio_tracks, video.streams.audio),
- build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height),
+ build_audio(video.audio_tracks, video.streams.audio),
+ build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height),
settings.extra,
"-o",
f'"{clean_file_string(video.video_settings.output_path)}"',
diff --git a/fastflix/encoders/vceencc_av1/command_builder.py b/fastflix/encoders/vceencc_av1/command_builder.py
index 4e6ff4fb..84391ffc 100644
--- a/fastflix/encoders/vceencc_av1/command_builder.py
+++ b/fastflix/encoders/vceencc_av1/command_builder.py
@@ -141,8 +141,8 @@ def build(fastflix: FastFlix):
(f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""),
remove_hdr,
"--psnr --ssim" if settings.metrics else "",
- build_audio(video.video_settings.audio_tracks, video.streams.audio),
- build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height),
+ build_audio(video.audio_tracks, video.streams.audio),
+ build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height),
settings.extra,
"-o",
f'"{clean_file_string(video.video_settings.output_path)}"',
diff --git a/fastflix/encoders/vceencc_avc/command_builder.py b/fastflix/encoders/vceencc_avc/command_builder.py
index 4c5f17c3..8382056d 100644
--- a/fastflix/encoders/vceencc_avc/command_builder.py
+++ b/fastflix/encoders/vceencc_avc/command_builder.py
@@ -126,8 +126,8 @@ def build(fastflix: FastFlix):
(f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""),
remove_hdr,
"--psnr --ssim" if settings.metrics else "",
- build_audio(video.video_settings.audio_tracks, video.streams.audio),
- build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height),
+ build_audio(video.audio_tracks, video.streams.audio),
+ build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height),
settings.extra,
"-o",
f'"{clean_file_string(video.video_settings.output_path)}"',
diff --git a/fastflix/encoders/vceencc_hevc/command_builder.py b/fastflix/encoders/vceencc_hevc/command_builder.py
index d3606fc0..bda09b85 100644
--- a/fastflix/encoders/vceencc_hevc/command_builder.py
+++ b/fastflix/encoders/vceencc_hevc/command_builder.py
@@ -143,8 +143,8 @@ def build(fastflix: FastFlix):
(f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""),
remove_hdr,
"--psnr --ssim" if settings.metrics else "",
- build_audio(video.video_settings.audio_tracks, video.streams.audio),
- build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle, video_height=video.height),
+ build_audio(video.audio_tracks, video.streams.audio),
+ build_subtitle(video.subtitle_tracks, video.streams.subtitle, video_height=video.height),
settings.extra,
"-o",
f'"{clean_file_string(video.video_settings.output_path)}"',
diff --git a/fastflix/entry.py b/fastflix/entry.py
index d89d8dfb..12247893 100644
--- a/fastflix/entry.py
+++ b/fastflix/entry.py
@@ -122,8 +122,15 @@ def main(portable_mode=False):
import platform
try:
- win_ver = int(platform.platform().lower().split("-")[1])
- except Exception:
+ windows_version_string = platform.platform().lower().split("-")[1]
+ if "server" in windows_version_string:
+ # Windows-2022Server-10.0.20348-SP0
+ server_version = int(windows_version_string[:4])
+ win_ver = 0 if server_version < 2016 else 10
+ else:
+ win_ver = int(windows_version_string)
+ except Exception as error:
+ print(f"COULD NOT DETERMINE WINDOWS VERSION FROM: {platform.platform()} - {error}")
win_ver = 0
if win_ver < 10:
input(
diff --git a/fastflix/ff_queue.py b/fastflix/ff_queue.py
index f6cd7cde..88914736 100644
--- a/fastflix/ff_queue.py
+++ b/fastflix/ff_queue.py
@@ -35,10 +35,10 @@ def get_queue(queue_file: Path) -> list[Video]:
video["video_settings"]["output_path"] = Path(video["video_settings"]["output_path"])
encoder_settings = video["video_settings"]["video_encoder_settings"]
ves = [x(**encoder_settings) for x in setting_types.values() if x().name == encoder_settings["name"]][0]
- audio = [AudioTrack(**x) for x in video["video_settings"]["audio_tracks"]]
- subtitles = [SubtitleTrack(**x) for x in video["video_settings"]["subtitle_tracks"]]
+ audio = [AudioTrack(**x) for x in video["audio_tracks"]]
+ subtitles = [SubtitleTrack(**x) for x in video["subtitle_tracks"]]
attachments = []
- for x in video["video_settings"]["attachment_tracks"]:
+ for x in video["attachment_tracks"]:
try:
attachment_path = x.pop("file_path")
except KeyError:
@@ -50,17 +50,11 @@ def get_queue(queue_file: Path) -> list[Video]:
crop = None
if video["video_settings"]["crop"]:
crop = Crop(**video["video_settings"]["crop"])
- del video["video_settings"]["audio_tracks"]
- del video["video_settings"]["subtitle_tracks"]
- del video["video_settings"]["attachment_tracks"]
del video["video_settings"]["video_encoder_settings"]
del video["status"]
del video["video_settings"]["crop"]
vs = VideoSettings(
**video["video_settings"],
- audio_tracks=audio,
- subtitle_tracks=subtitles,
- attachment_tracks=attachments,
crop=crop,
)
vs.video_encoder_settings = ves # No idea why this has to be called after, otherwise reset to x265
@@ -108,7 +102,7 @@ def update_conversion_command(vid, old_path: str, new_path: str):
str(new_metadata_file),
)
video["video_settings"]["video_encoder_settings"]["hdr10plus_metadata"] = str(new_metadata_file)
- for track in video["video_settings"]["attachment_tracks"]:
+ for track in video["attachment_tracks"]:
if track.get("file_path"):
if not Path(track["file_path"]).exists():
logger.exception("Could not save cover to queue recovery location, removing cover")
diff --git a/fastflix/flix.py b/fastflix/flix.py
index 2b12b875..d8956981 100644
--- a/fastflix/flix.py
+++ b/fastflix/flix.py
@@ -5,7 +5,7 @@
from pathlib import Path
from subprocess import PIPE, CompletedProcess, Popen, TimeoutExpired, run, check_output
from typing import List, Tuple, Union
-from distutils.version import LooseVersion
+from packaging import version
import shlex
import reusables
@@ -23,6 +23,8 @@
logger = logging.getLogger("fastflix")
+HDR10_parser_version = None
+
ffmpeg_valid_color_primaries = [
"bt709",
"bt470m",
@@ -273,6 +275,8 @@ def parse(app: FastFlixApp, **_):
def extract_attachments(app: FastFlixApp, **_):
+ if app.fastflix.config.disable_cover_extraction:
+ return
for track in app.fastflix.current_video.streams.attachment:
filename = track.get("tags", {}).get("filename", "")
if filename.rsplit(".", 1)[0] in ("cover", "small_cover", "cover_land", "small_cover_land"):
@@ -329,7 +333,7 @@ def generate_thumbnail_command(
# Hardware acceleration with OpenCL
if enable_opencl:
- command += ["-init_hw_device", "opencl=ocl", "-filter_hw_device", "ocl"]
+ command += ["-init_hw_device", "opencl:0.0=ocl", "-filter_hw_device", "ocl"]
command += shlex.split(filters)
@@ -374,6 +378,9 @@ def get_auto_crop(
)
width, height, x_crop, y_crop = None, None, None, None
+ if not output.stderr:
+ return 0, 0, 0, 0
+
for line in output.stderr.splitlines():
if line.startswith("[Parsed_cropdetect"):
w, h, x, y = [int(x) for x in line.rsplit("=")[1].split(":")]
@@ -427,6 +434,10 @@ def detect_interlaced(app: FastFlixApp, config: Config, source: Path, **_):
logger.exception("Error while running the interlace detection command")
return
+ if not output.stderr:
+ logger.warning("Could not extract interlaced information")
+ return
+
for line in output.stderr.splitlines():
if "Single frame detection" in line:
try:
@@ -460,7 +471,7 @@ def ffmpeg_audio_encoders(app, config: Config) -> List:
def ffmpeg_opencl_support(app, config: Config) -> bool:
- cmd = execute([f"{config.ffmpeg}", "-hide_banner", "-log_level", "error", "-init_hw_device", "opencl", "-h"])
+ cmd = execute([f"{config.ffmpeg}", "-hide_banner", "-log_level", "error", "-init_hw_device", "opencl:0.0", "-h"])
app.fastflix.opencl_support = cmd.returncode == 0
return app.fastflix.opencl_support
@@ -558,16 +569,25 @@ def parse_hdr_details(app: FastFlixApp, **_):
)
+def get_hdr10_parser_version(config: Config) -> version:
+ global HDR10_parser_version
+ if HDR10_parser_version:
+ return HDR10_parser_version
+ HDR10_parser_version_output = check_output([str(config.hdr10plus_parser), "--version"], encoding="utf-8")
+
+ _, version_string = HDR10_parser_version_output.rsplit(sep=" ", maxsplit=1)
+ HDR10_parser_version = version.parse(version_string)
+ logger.debug(f"Using HDR10 parser version {str(HDR10_parser_version).strip()}")
+ return HDR10_parser_version
+
+
def detect_hdr10_plus(app: FastFlixApp, config: Config, **_):
if not config.hdr10plus_parser or not config.hdr10plus_parser.exists():
return
hdr10plus_streams = []
- hdr10_parser_version_output = check_output([str(config.hdr10plus_parser), "--version"], encoding="utf-8")
- _, version_string = hdr10_parser_version_output.rsplit(sep=" ", maxsplit=1)
- hdr10_parser_version = LooseVersion(version_string)
- logger.debug(f"Using HDR10 parser version {str(hdr10_parser_version).strip()}")
+ parser_version = get_hdr10_parser_version(config)
for stream in app.fastflix.current_video.streams.video:
logger.debug(f"Checking for hdr10+ in stream {stream.index}")
@@ -595,7 +615,7 @@ def detect_hdr10_plus(app: FastFlixApp, config: Config, **_):
)
hdr10_parser_command = [str(config.hdr10plus_parser), "--verify", "-"]
- if hdr10_parser_version >= LooseVersion("1.0.0"):
+ if parser_version >= version.parse("1.0.0"):
hdr10_parser_command.insert(-1, "extract")
process_two = Popen(
diff --git a/fastflix/models/config.py b/fastflix/models/config.py
index a4993ccf..f8a5074b 100644
--- a/fastflix/models/config.py
+++ b/fastflix/models/config.py
@@ -3,11 +3,10 @@
import logging
import os
import shutil
-from distutils.version import StrictVersion
+from packaging import version
from pathlib import Path
from typing import Literal
import json
-import sys
from appdirs import user_data_dir
from box import Box, BoxError
@@ -163,6 +162,8 @@ class Config(BaseModel):
sticky_tabs: bool = False
disable_complete_message: bool = False
+ disable_cover_extraction: bool = False
+
def encoder_opt(self, profile_name, profile_option_name):
encoder_settings = getattr(self.profiles[self.selected_profile], profile_name)
if encoder_settings:
@@ -266,7 +267,7 @@ def load(self, portable_mode=False):
if "version" not in data:
raise ConfigError(f"Corrupt config file. Please fix or remove {self.config_path}")
- if StrictVersion(__version__) < StrictVersion(data.version):
+ if version.parse(__version__) < version.parse(data.version):
logger.warning(
f"This FastFlix version ({__version__}) is older "
f"than the one that generated the config file ({data.version}), "
@@ -315,7 +316,7 @@ def load(self, portable_mode=False):
# 5.2.0 remove ext
self.output_name_format = self.output_name_format.replace(".{ext}", "").replace("{ext}", "")
- # if StrictVersion(__version__) > StrictVersion(data.version):
+ # if version.parse(__version__) > version.parse(data.version):
# logger.info(f"Clearing possible old config values from fastflix {data.verion}")
# self.vceencc_encoders = []
# self.nvencc_encoders = []
diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py
index 42901662..4714e572 100644
--- a/fastflix/models/encode.py
+++ b/fastflix/models/encode.py
@@ -14,7 +14,8 @@ class AudioTrack(BaseModel):
downmix: Optional[str] = None
title: str = ""
language: str = ""
- conversion_bitrate: str = ""
+ conversion_aq: Optional[int] = None
+ conversion_bitrate: Optional[str] = None
conversion_codec: str = ""
profile: Optional[str] = None
enabled: bool = True
@@ -33,6 +34,8 @@ class SubtitleTrack(BaseModel):
language: str = ""
subtitle_type: str = ""
dispositions: dict = Field(default_factory=dict)
+ enabled: bool = True
+ long_name: str = ""
class AttachmentTrack(BaseModel):
diff --git a/fastflix/models/video.py b/fastflix/models/video.py
index 5c7fddbf..37ced71c 100644
--- a/fastflix/models/video.py
+++ b/fastflix/models/video.py
@@ -138,9 +138,9 @@ class VideoSettings(BaseModel):
VAAPIVP9Settings,
VAAPIMPEG2Settings,
] = None
- audio_tracks: list[AudioTrack] = Field(default_factory=list)
- subtitle_tracks: list[SubtitleTrack] = Field(default_factory=list)
- attachment_tracks: list[AttachmentTrack] = Field(default_factory=list)
+ # audio_tracks: list[AudioTrack] = Field(default_factory=list)
+ # subtitle_tracks: list[SubtitleTrack] = Field(default_factory=list)
+ # attachment_tracks: list[AttachmentTrack] = Field(default_factory=list)
conversion_commands: List = Field(default_factory=list)
@@ -181,6 +181,10 @@ class Video(BaseModel):
hdr10_plus: list[int] = Field(default_factory=list)
video_settings: VideoSettings = Field(default_factory=VideoSettings)
+ audio_tracks: list[AudioTrack] = Field(default_factory=list)
+ subtitle_tracks: list[SubtitleTrack] = Field(default_factory=list)
+ attachment_tracks: list[AttachmentTrack] = Field(default_factory=list)
+
status: Status = Field(default_factory=Status)
uuid: str = Field(default_factory=lambda: str(uuid.uuid4()))
diff --git a/fastflix/shared.py b/fastflix/shared.py
index d1861364..b9b61906 100644
--- a/fastflix/shared.py
+++ b/fastflix/shared.py
@@ -316,7 +316,21 @@ def clean_file_string(source):
def quoted_path(source):
- return str(source).strip().replace("\\", "\\\\").replace(":", "\\:").replace("'", "'\\\\\\''")
+ cleaned_string = (
+ str(source)
+ .strip()
+ .replace("\\", "\\\\")
+ .replace(":", "\\:")
+ .replace("'", "'\\\\\\''")
+ .replace("\r\n", "")
+ .replace("\n", "")
+ .replace("\r", "")
+ )
+ if " " in cleaned_string[0:4]:
+ logger.warning(f"Unexpected space at start of quoted path, attempting to fix: {cleaned_string}")
+ cleaned_string = cleaned_string[0:4].replace(" ", "") + cleaned_string[4:]
+ logger.warning(f"New path set to: {cleaned_string}")
+ return cleaned_string
def sanitize(source):
diff --git a/fastflix/version.py b/fastflix/version.py
index b2c943c0..08393cc6 100644
--- a/fastflix/version.py
+++ b/fastflix/version.py
@@ -1,4 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-__version__ = "5.6.0"
+__version__ = "5.7.0"
__author__ = "Chris Griffith"
diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py
index 24f11467..85c9911d 100644
--- a/fastflix/widgets/background_tasks.py
+++ b/fastflix/widgets/background_tasks.py
@@ -3,7 +3,7 @@
import logging
from pathlib import Path
from subprocess import PIPE, STDOUT, Popen, run, check_output
-from distutils.version import LooseVersion
+from packaging import version
from PySide6 import QtCore
@@ -32,6 +32,9 @@ def run(self):
"Please use FFmpeg 4.3+ built against the latest zimg libraries. "
"Static builds available at https://ffmpeg.org/download.html "
)
+ if "OpenCL mapping not usable" in result.stdout.decode(encoding="utf-8", errors="ignore"):
+ self.main.thread_logging_signal.emit("ERROR trying to use OpenCL for thumbnail generation")
+ self.main.thumbnail_complete.emit(2)
else:
self.main.thread_logging_signal.emit(f"ERROR:{t('Could not generate thumbnail')}: {result.stdout}")
@@ -114,7 +117,7 @@ def run(self):
[str(self.app.fastflix.config.hdr10plus_parser), "--version"], encoding="utf-8"
)
_, version_string = hdr10_parser_version_output.rsplit(sep=" ", maxsplit=1)
- hdr10_parser_version = LooseVersion(version_string)
+ hdr10_parser_version = version.parse(version_string)
self.main.thread_logging_signal.emit(f"Using HDR10 parser version {str(hdr10_parser_version).strip()}")
ffmpeg_command = [
@@ -134,7 +137,7 @@ def run(self):
]
hdr10_parser_command = [str(self.app.fastflix.config.hdr10plus_parser), "-o", clean_file_string(output), "-"]
- if hdr10_parser_version >= LooseVersion("1.0.0"):
+ if hdr10_parser_version >= version.parse("1.0.0"):
hdr10_parser_command.insert(1, "extract")
self.main.thread_logging_signal.emit(
diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py
index 915f34d7..160c495f 100644
--- a/fastflix/widgets/container.py
+++ b/fastflix/widgets/container.py
@@ -30,6 +30,8 @@
from fastflix.widgets.windows.concat import ConcatWindow
from fastflix.widgets.windows.multiple_files import MultipleFilesWindow
+# from fastflix.widgets.windows.hdr10plus_inject import HDR10PlusInjectWindow
+
logger = logging.getLogger("fastflix")
@@ -161,7 +163,7 @@ def si(self, widget):
def init_menu(self):
menubar = self.menuBar()
menubar.setNativeMenuBar(False)
- menubar.setFixedWidth(260)
+ menubar.setFixedWidth(360)
menubar.setStyleSheet("font-size: 14px")
file_menu = menubar.addMenu(t("File"))
@@ -213,6 +215,12 @@ def init_menu(self):
concat_action.triggered.connect(self.show_concat)
tools_menu.addAction(concat_action)
+ # hdr10p_inject_action = QAction(
+ # QtGui.QIcon(get_icon("onyx-queue", self.app.fastflix.config.theme)), t("HDR10+ Inject"), self
+ # )
+ # hdr10p_inject_action.triggered.connect(self.show_hdr10p_inject)
+ # tools_menu.addAction(hdr10p_inject_action)
+
wiki_action = QAction(self.si(QtWidgets.QStyle.SP_FileDialogInfoView), t("FastFlix Wiki"), self)
wiki_action.triggered.connect(self.show_wiki)
@@ -266,6 +274,10 @@ def show_concat(self):
self.concat = ConcatWindow(app=self.app, main=self.main)
self.concat.show()
+ # def show_hdr10p_inject(self):
+ # self.hdr10p_inject = HDR10PlusInjectWindow(app=self.app, main=self.main)
+ # self.hdr10p_inject.show()
+
def show_about(self):
self.about = About(app=self.app)
self.about.show()
diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py
index 44a3683d..25731a10 100644
--- a/fastflix/widgets/main.py
+++ b/fastflix/widgets/main.py
@@ -1558,6 +1558,7 @@ def update_video_info(self, hide_progress=False):
self.widgets.deinterlace.setChecked(self.app.fastflix.current_video.video_settings.deinterlace)
+ logger.info("Updating video info")
self.video_options.new_source()
self.enable_all()
# self.widgets.convert_button.setDisabled(False)
@@ -1679,10 +1680,13 @@ def thread_logger(text):
logger.warning(text)
@reusables.log_exception("fastflix", show_traceback=False)
- def thumbnail_generated(self, success=False):
- if not success or not self.thumb_file.exists():
+ def thumbnail_generated(self, status=0):
+ if status == 0 or not status or not self.thumb_file.exists():
self.widgets.preview.setText(t("Error Updating Thumbnail"))
return
+ if status == 2:
+ self.generate_thumbnail()
+ return
pixmap = QtGui.QPixmap(str(self.thumb_file))
pixmap = pixmap.scaled(420, 260, QtCore.Qt.KeepAspectRatio)
self.widgets.preview.setPixmap(pixmap)
@@ -2140,12 +2144,3 @@ def run(self):
return
self.main.status_update_signal.emit(status)
self.app.processEvents()
- # if status[0] == "complete":
- # logger.debug("GUI received status queue complete")
- # self.main.completed.emit(0)
- # elif status[0] == "error":
- # logger.debug("GUI received status queue errored")
- # self.main.completed.emit(1)
- # elif status[0] == "cancelled":
- # logger.debug("GUI received status queue errored")
- # self.main.cancelled.emit("|".join(status[1:]))
diff --git a/fastflix/widgets/panels/abstract_list.py b/fastflix/widgets/panels/abstract_list.py
index 72477233..00da12d2 100644
--- a/fastflix/widgets/panels/abstract_list.py
+++ b/fastflix/widgets/panels/abstract_list.py
@@ -80,9 +80,9 @@ def reorder(self, update=True, height=66):
if (
self.app.fastflix.current_video
and self.app.fastflix.current_video.video_settings
- and isinstance(self.app.fastflix.current_video.video_settings.audio_tracks, list)
+ and isinstance(self.app.fastflix.current_video.audio_tracks, list)
):
- start = len(self.app.fastflix.current_video.video_settings.audio_tracks) + 1
+ start = len([x for x in self.app.fastflix.current_video.audio_tracks if x.enabled]) + 1
for index, widget in enumerate(self.tracks, start):
self.inner_layout.addWidget(widget)
diff --git a/fastflix/widgets/panels/audio_panel.py b/fastflix/widgets/panels/audio_panel.py
index 0e79fb36..a052697f 100644
--- a/fastflix/widgets/panels/audio_panel.py
+++ b/fastflix/widgets/panels/audio_panel.py
@@ -17,6 +17,7 @@
from fastflix.shared import no_border, error_message, yes_no_message, clear_list
from fastflix.widgets.panels.abstract_list import FlixList
from fastflix.audio_processing import apply_audio_filters
+from fastflix.widgets.windows.audio_conversion import AudioConversion
from fastflix.widgets.windows.disposition import Disposition
language_list = sorted((k for k, v in Lang._data["name"].items() if v["pt2B"] and v["pt1"]), key=lambda x: x.lower())
@@ -42,66 +43,36 @@
class Audio(QtWidgets.QTabWidget):
def __init__(
self,
+ app,
parent,
- audio,
index,
- codec,
- available_audio_encoders,
- title="",
- language="",
- profile="",
- outdex=None,
- enabled=True,
- original=False,
- first=False,
- last=False,
- codecs=(),
- channels=2,
- all_info=None,
- disable_dup=False,
- dispositions=None,
+ disabled_dup=False,
):
self.loading = True
super(Audio, self).__init__(parent)
+ self.app = app
self.setObjectName("Audio")
- self.parent = parent
- self.audio = audio
- self.setFixedHeight(60)
- self.original = original
- self.outdex = index if self.original else outdex
- self.first = first
- self.track_name = title
- self.profile = profile
- self.last = last
+ self.parent: "AudioList" = parent
self.index = index
- self.codec = codec
- self.codecs = codecs
- self.channels = channels
- self.available_audio_encoders = available_audio_encoders
- self.all_info = all_info
+ self.first = False
+ self.last = False
+ self.setFixedHeight(60)
+ audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[index]
self.widgets = Box(
- track_number=QtWidgets.QLabel(f"{index}:{self.outdex}" if enabled else "❌"),
- title=QtWidgets.QLineEdit(title),
- audio_info=QtWidgets.QLabel(audio),
- up_button=QtWidgets.QPushButton(
- QtGui.QIcon(get_icon("up-arrow", self.parent.app.fastflix.config.theme)), ""
- ),
- down_button=QtWidgets.QPushButton(
- QtGui.QIcon(get_icon("down-arrow", self.parent.app.fastflix.config.theme)), ""
- ),
+ track_number=QtWidgets.QLabel(f"{audio_track.index}:{audio_track.outdex}" if audio_track.enabled else "❌"),
+ title=QtWidgets.QLineEdit(audio_track.title),
+ audio_info=QtWidgets.QLabel(audio_track.friendly_info),
+ up_button=QtWidgets.QPushButton(QtGui.QIcon(get_icon("up-arrow", self.app.fastflix.config.theme)), ""),
+ down_button=QtWidgets.QPushButton(QtGui.QIcon(get_icon("down-arrow", self.app.fastflix.config.theme)), ""),
enable_check=QtWidgets.QCheckBox(t("Enabled")),
- dup_button=QtWidgets.QPushButton(
- QtGui.QIcon(get_icon("onyx-copy", self.parent.app.fastflix.config.theme)), ""
- ),
- delete_button=QtWidgets.QPushButton(
- QtGui.QIcon(get_icon("black-x", self.parent.app.fastflix.config.theme)), ""
- ),
+ dup_button=QtWidgets.QPushButton(QtGui.QIcon(get_icon("onyx-copy", self.app.fastflix.config.theme)), ""),
+ delete_button=QtWidgets.QPushButton(QtGui.QIcon(get_icon("black-x", self.app.fastflix.config.theme)), ""),
language=QtWidgets.QComboBox(),
- downmix=QtWidgets.QComboBox(),
convert_to=None,
convert_bitrate=None,
disposition=QtWidgets.QPushButton(),
+ conversion=QtWidgets.QPushButton(t("Conversion")),
)
self.widgets.up_button.setStyleSheet(no_border)
@@ -109,14 +80,13 @@ def __init__(
self.widgets.dup_button.setStyleSheet(no_border)
self.widgets.delete_button.setStyleSheet(no_border)
- if all_info:
- self.widgets.audio_info.setToolTip(all_info.to_yaml())
+ self.widgets.audio_info.setToolTip(audio_track.raw_info.to_yaml())
self.widgets.language.addItems(["No Language Set"] + language_list)
self.widgets.language.setMaximumWidth(150)
- if language:
+ if audio_track.language:
try:
- lang = Lang(language).name
+ lang = Lang(audio_track.language).name
except InvalidLanguageValue:
pass
else:
@@ -128,18 +98,13 @@ def __init__(
self.widgets.title.textChanged.connect(self.page_update)
# self.widgets.audio_info.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
self.widgets.audio_info.setFixedWidth(350)
- self.widgets.downmix.addItems([t("No Downmix")] + [k for k, v in channel_list.items() if v <= channels])
- self.widgets.downmix.currentIndexChanged.connect(self.update_downmix)
- self.widgets.downmix.setCurrentIndex(0)
- self.widgets.downmix.setDisabled(True)
- self.widgets.downmix.hide()
- self.widgets.enable_check.setChecked(enabled)
+ self.widgets.enable_check.setChecked(audio_track.enabled)
self.widgets.enable_check.toggled.connect(self.update_enable)
self.widgets.dup_button.clicked.connect(lambda: self.dup_me())
self.widgets.dup_button.setFixedWidth(20)
- if disable_dup:
+ if disabled_dup:
self.widgets.dup_button.hide()
self.widgets.dup_button.setDisabled(True)
@@ -148,15 +113,16 @@ def __init__(
self.widgets.track_number.setFixedWidth(20)
- self.dispositions = dispositions or {k: False for k in disposition_options}
-
- self.disposition_widget = Disposition(self, f"Audio Track {index}", subs=True)
- self.set_dis_button()
+ self.disposition_widget = Disposition(
+ app=app, parent=self, track_name=f"Audio Track {index}", track_index=index, audio=True
+ )
self.widgets.disposition.clicked.connect(self.disposition_widget.show)
+ self.widgets.conversion.clicked.connect(self.show_conversions)
+
disposition_layout = QtWidgets.QHBoxLayout()
- disposition_layout.addWidget(QtWidgets.QLabel(t("Dispositions")))
disposition_layout.addWidget(self.widgets.disposition)
+ self.widgets.disposition.setText(t("Dispositions"))
label = QtWidgets.QLabel(f"{t('Title')}: ")
self.widgets.title.setFixedWidth(150)
@@ -172,13 +138,12 @@ def __init__(
grid.addWidget(self.widgets.audio_info, 0, 2)
grid.addLayout(title_layout, 0, 3)
grid.addLayout(disposition_layout, 0, 4)
- grid.addLayout(self.init_conversion(), 0, 5)
- grid.addWidget(self.widgets.downmix, 0, 6)
- grid.addWidget(self.widgets.language, 0, 7)
+ grid.addWidget(self.widgets.conversion, 0, 5)
+ grid.addWidget(self.widgets.language, 0, 6)
- right_button_start_index = 8
+ right_button_start_index = 7
- if not original:
+ if not audio_track.original:
spacer = QtWidgets.QLabel()
spacer.setFixedWidth(63)
grid.addWidget(spacer, 0, right_button_start_index)
@@ -187,8 +152,27 @@ def __init__(
grid.addWidget(self.widgets.enable_check, 0, right_button_start_index)
grid.addWidget(self.widgets.dup_button, 0, right_button_start_index + 1)
self.setLayout(grid)
+ self.conversion_box = None
self.loading = False
+ def show_conversions(self):
+ try:
+ self.conversion_box.close()
+ except Exception:
+ pass
+ try:
+ del self.conversion_box
+ except Exception:
+ pass
+
+ self.conversion_box = AudioConversion(
+ self.app,
+ track_index=self.index,
+ encoders=self.app.fastflix.audio_encoders,
+ audio_track_update=self.page_update,
+ )
+ self.conversion_box.show()
+
def init_move_buttons(self):
layout = QtWidgets.QVBoxLayout()
layout.setSpacing(0)
@@ -205,41 +189,6 @@ def init_move_buttons(self):
layout.addWidget(self.widgets.down_button)
return layout
- def init_conversion(self):
- layout = QtWidgets.QHBoxLayout()
- self.widgets.convert_to = QtWidgets.QComboBox()
-
- self.update_codecs(self.codecs)
-
- self.widgets.convert_bitrate = QtWidgets.QComboBox()
- self.widgets.convert_bitrate.setFixedWidth(70)
- self.widgets.convert_bitrate.addItems(self.get_conversion_bitrates())
- self.widgets.convert_bitrate.setCurrentIndex(3)
- self.widgets.convert_bitrate.setDisabled(True)
- self.widgets.bitrate_label = QtWidgets.QLabel(f"{t('Bitrate')}: ")
- self.widgets.convert_bitrate.hide()
- self.widgets.bitrate_label.hide()
-
- self.widgets.convert_bitrate.currentIndexChanged.connect(lambda: self.page_update())
- self.widgets.convert_to.currentIndexChanged.connect(self.update_conversion)
- layout.addWidget(QtWidgets.QLabel(f"{t('Conversion')}: "))
- layout.addWidget(self.widgets.convert_to)
-
- layout.addWidget(self.widgets.bitrate_label)
- layout.addWidget(self.widgets.convert_bitrate)
-
- return layout
-
- def set_dis_button(self):
- output = ""
- for disposition, is_set in self.dispositions.items():
- if is_set:
- output += f"{t(disposition)},"
- if output:
- self.widgets.disposition.setText(output.rstrip(","))
- else:
- self.widgets.disposition.setText(t("none"))
-
def get_conversion_bitrates(self, channels=None):
if not channels:
channels = self.channels or 2
@@ -250,85 +199,23 @@ def get_conversion_bitrates(self, channels=None):
def update_enable(self):
enabled = self.widgets.enable_check.isChecked()
- self.widgets.track_number.setText(f"{self.index}:{self.outdex}" if enabled else "❌")
+ audio_track = self.app.fastflix.current_video.audio_tracks[self.index]
+ audio_track.enabled = enabled
+ self.widgets.track_number.setText(f"{audio_track.index}:{audio_track.outdex}" if enabled else "❌")
self.parent.reorder(update=True)
-
- def update_downmix(self):
- channels = (
- channel_list[self.widgets.downmix.currentText()]
- if self.widgets.downmix.currentIndex() > 0
- else self.channels
- )
- if self.conversion["codec"] not in lossless:
- self.widgets.convert_bitrate.clear()
- self.widgets.convert_bitrate.addItems(self.get_conversion_bitrates(channels))
- self.widgets.convert_bitrate.setCurrentIndex(3)
- self.page_update()
-
- def update_conversion(self):
- if self.widgets.convert_to.currentIndex() == 0:
- self.widgets.downmix.setDisabled(True)
- self.widgets.convert_bitrate.setDisabled(True)
- self.widgets.convert_bitrate.hide()
- self.widgets.bitrate_label.hide()
- self.widgets.downmix.hide()
- else:
- self.widgets.downmix.setDisabled(False)
- self.widgets.convert_bitrate.show()
- self.widgets.bitrate_label.show()
- self.widgets.downmix.show()
- if self.conversion["codec"] in lossless:
- self.widgets.convert_bitrate.setDisabled(True)
- self.widgets.convert_bitrate.addItem("lossless")
- self.widgets.convert_bitrate.setCurrentText("lossless")
- else:
- self.widgets.convert_bitrate.setDisabled(False)
- self.widgets.convert_bitrate.clear()
- channels = (
- channel_list[self.widgets.downmix.currentText()]
- if self.widgets.downmix.currentIndex() > 0
- else self.channels
- )
- self.widgets.convert_bitrate.addItems(self.get_conversion_bitrates(channels))
- self.widgets.convert_bitrate.setCurrentIndex(3)
- self.page_update()
+ self.parent.parent.subtitles.reorder()
def page_update(self):
if not self.loading:
+ self.check_conversion_button()
return self.parent.main.page_update(build_thumbnail=False)
- def update_codecs(self, codec_list):
- self.loading = True
- current = self.widgets.convert_to.currentText()
- self.widgets.convert_to.clear()
- # passthrough_available = False
- # if self.codec in codec_list:
- passthrough_available = True
- self.widgets.convert_to.addItem(t("none"))
- self.widgets.convert_to.addItems(sorted(set(self.available_audio_encoders) & set(codec_list)))
- if current in codec_list:
- index = codec_list.index(current)
- if passthrough_available:
- index += 1
- self.widgets.convert_to.setCurrentIndex(index)
- self.widgets.convert_to.setCurrentIndex(0) # Will either go to 'copy' or first listed
- if self.widgets.convert_bitrate:
- self.widgets.convert_bitrate.setDisabled(True)
- self.loading = False
-
@property
def enabled(self):
- return self.widgets.enable_check.isChecked()
-
- @property
- def conversion(self):
- if self.widgets.convert_to.currentIndex() == 0:
- return {"codec": "", "bitrate": ""}
- return {"codec": self.widgets.convert_to.currentText(), "bitrate": self.widgets.convert_bitrate.currentText()}
-
- @property
- def downmix(self) -> Optional[str]:
- return self.widgets.downmix.currentText() if self.widgets.downmix.currentIndex() > 0 else None
+ try:
+ return self.app.fastflix.current_video.audio_tracks[self.index].enabled
+ except IndexError:
+ return False
@property
def language(self) -> str:
@@ -349,45 +236,69 @@ def set_last(self, last=True):
self.widgets.down_button.setDisabled(self.last)
def dup_me(self):
- new = Audio(
+ # Add new track to the conversion list
+ new_track = self.app.fastflix.current_video.audio_tracks[self.index].copy()
+ new_track.outdex = len(self.app.fastflix.current_video.audio_tracks) + 1
+ new_track.original = False
+ self.app.fastflix.current_video.audio_tracks.append(new_track)
+
+ # Add new track to GUI
+ new_item = Audio(
parent=self.parent,
- audio=self.audio,
- index=self.index,
- language=self.language,
- outdex=len(self.parent.tracks) + 1,
- codec=self.codec,
- available_audio_encoders=self.available_audio_encoders,
- enabled=True,
- original=False,
- codecs=self.codecs,
- channels=self.channels,
- dispositions=self.dispositions,
+ app=self.app,
+ index=len(self.app.fastflix.current_video.audio_tracks) - 1,
+ disabled_dup=(
+ "nvencc" in self.parent.main.convert_to.lower()
+ or "vcenc" in self.parent.main.convert_to.lower()
+ or "qsvenc" in self.parent.main.convert_to.lower()
+ ),
)
-
- self.parent.tracks.append(new)
+ self.parent.tracks.append(new_item)
self.parent.reorder()
def del_me(self):
self.parent.remove_track(self)
+ del self.app.fastflix.current_video.audio_tracks[self.index]
def set_outdex(self, outdex):
+ self.app.fastflix.current_video.audio_tracks[self.index].outdex = outdex
+ audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[self.index]
self.outdex = outdex
- if not self.enabled:
+ if not audio_track.enabled:
self.widgets.track_number.setText("❌")
else:
- self.widgets.track_number.setText(f"{self.index}:{self.outdex}")
+ self.widgets.track_number.setText(f"{audio_track.index}:{audio_track.outdex}")
def close(self) -> bool:
- del self.dispositions
del self.widgets
return super().close()
+ def update_track(self, conversion=None, bitrate=None, downmix=None):
+ audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[self.index]
+ if conversion:
+ audio_track.conversion_codec = conversion
+ if bitrate:
+ audio_track.conversion_bitrate = bitrate
+ if downmix:
+ audio_track.downmix = downmix
+ self.page_update()
+
+ def check_conversion_button(self):
+ audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[self.index]
+ if audio_track.conversion_codec:
+ self.widgets.conversion.setStyleSheet("border-color: #0055ff")
+ self.widgets.conversion.setText(t("Conversion") + f": {audio_track.conversion_codec}")
+ else:
+ self.widgets.conversion.setStyleSheet("")
+ self.widgets.conversion.setText(t("Conversion"))
+
class AudioList(FlixList):
def __init__(self, parent, app: FastFlixApp):
super(AudioList, self).__init__(app, parent, "Audio Tracks", "audio")
self.available_audio_encoders = app.fastflix.audio_encoders
self.app = app
+ self.parent = parent
self._first_selected = False
def _get_track_info(self, track):
@@ -413,33 +324,35 @@ def disable_all(self):
def new_source(self, codecs):
clear_list(self.tracks, close=True)
+ self.app.fastflix.current_video.audio_tracks = []
self.tracks: list[Audio] = []
self._first_selected = False
- disable_dup = (
- "nvencc" in self.main.convert_to.lower()
- or "vcenc" in self.main.convert_to.lower()
- or "qsvenc" in self.main.convert_to.lower()
- )
- for i, x in enumerate(self.app.fastflix.current_video.streams.audio, start=1):
+ for i, x in enumerate(self.app.fastflix.current_video.streams.audio):
track_info, tags = self._get_track_info(x)
+ self.app.fastflix.current_video.audio_tracks.append(
+ AudioTrack(
+ index=x.index,
+ outdex=i + 1,
+ title=tags.get("title", ""),
+ language=tags.get("language", ""),
+ profile=x.get("profile"),
+ channels=x.channels,
+ enabled=True,
+ original=True,
+ raw_info=x,
+ friendly_info=track_info,
+ dispositions={k: bool(v) for k, v in x.disposition.items()},
+ )
+ )
new_item = Audio(
- self,
- track_info,
- title=tags.get("title"),
- language=tags.get("language"),
- profile=x.get("profile"),
- original=True,
- first=True if i == 0 else False,
- index=x.index,
- outdex=i,
- codec=x.codec_name,
- codecs=codecs,
- channels=x.channels,
- available_audio_encoders=self.available_audio_encoders,
- enabled=True,
- all_info=x,
- disable_dup=disable_dup,
- dispositions={k: bool(v) for k, v in x.disposition.items()},
+ parent=self,
+ app=self.app,
+ index=i,
+ disabled_dup=(
+ "nvencc" in self.main.convert_to.lower()
+ or "vcenc" in self.main.convert_to.lower()
+ or "qsvenc" in self.main.convert_to.lower()
+ ),
)
self.tracks.append(new_item)
@@ -447,7 +360,7 @@ def new_source(self, codecs):
self.tracks[0].set_first()
self.tracks[-1].set_last()
super()._new_source(self.tracks)
- self.update_audio_settings()
+ # self.update_audio_settings()
def allowed_formats(self, allowed_formats=None):
disable_dups = (
@@ -457,8 +370,9 @@ def allowed_formats(self, allowed_formats=None):
)
tracks_need_removed = False
for track in self.tracks:
+ audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[track.index]
track.widgets.dup_button.setDisabled(disable_dups)
- if not track.original:
+ if not audio_track.original:
if disable_dups:
tracks_need_removed = True
else:
@@ -470,8 +384,8 @@ def allowed_formats(self, allowed_formats=None):
error_message(t("This encoder does not support duplicating audio tracks, please remove copied tracks!"))
if not allowed_formats:
return
- for track in self.tracks:
- track.update_codecs(allowed_formats or set())
+ # for track in self.tracks:
+ # track.update_codecs(allowed_formats or set())
def apply_profile_settings(
self,
@@ -488,64 +402,60 @@ def apply_profile_settings(
clear_list(self.tracks)
- def update_track(new_track, downmix=None, conversion=None, bitrate=None):
- if conversion:
- new_track.widgets.convert_to.setCurrentText(conversion)
- # Downmix must come first
- if downmix:
- new_track.widgets.downmix.setCurrentText(downmix)
- if conversion in lossless:
- new_track.widgets.convert_bitrate.setDisabled(True)
- new_track.widgets.convert_bitrate.addItem("lossless")
- new_track.widgets.convert_bitrate.setCurrentText("lossless")
- else:
- if bitrate not in [
- new_track.widgets.convert_bitrate.itemText(i)
- for i in range(new_track.widgets.convert_bitrate.count())
- ]:
- new_track.widgets.convert_bitrate.addItem(bitrate)
- new_track.widgets.convert_bitrate.setCurrentText(bitrate)
- new_track.widgets.convert_bitrate.setDisabled(False)
-
def gen_track(
parent, audio_track, outdex, og=False, enabled=True, downmix=None, conversion=None, bitrate=None
) -> Audio:
track_info, tags = self._get_track_info(audio_track)
- new_track = Audio(
- parent,
- track_info,
- title=tags.get("title"),
- language=tags.get("language"),
- profile=audio_track.get("profile"),
- original=og,
- index=audio_track.index,
- outdex=outdex,
- codec=audio_track.codec_name,
- codecs=audio_formats,
- channels=audio_track.channels,
- available_audio_encoders=self.available_audio_encoders,
- enabled=enabled,
- all_info=audio_track,
- disable_dup=og_only,
+ self.app.fastflix.current_video.audio_tracks.append(
+ AudioTrack(
+ index=audio_track.index,
+ outdex=outdex,
+ title=tags.get("title", ""),
+ language=tags.get("language", ""),
+ profile=audio_track.get("profile"),
+ channels=audio_track.channels,
+ enabled=enabled,
+ original=og,
+ raw_info=audio_track,
+ friendly_info=track_info,
+ downmix=downmix,
+ conversion_codec=conversion,
+ conversion_bitrate=bitrate,
+ dispositions={k: bool(v) for k, v in audio_track.disposition.items()},
+ )
)
+ new_item = Audio(
+ parent=parent,
+ app=self.app,
+ index=i,
+ disabled_dup=(
+ "nvencc" in self.main.convert_to.lower()
+ or "vcenc" in self.main.convert_to.lower()
+ or "qsvenc" in self.main.convert_to.lower()
+ ),
+ )
+ self.tracks.append(new_item)
- update_track(new_track=new_track, downmix=downmix, conversion=conversion, bitrate=bitrate)
+ return new_item
- return new_track
+ self.new_source(audio_formats)
- # First populate all original tracks and disable them
- for i, track in enumerate(original_tracks, start=1):
- self.tracks.append(gen_track(self, track, outdex=i, og=True, enabled=False))
+ # # First populate all original tracks and disable them
+ # for i, track in enumerate(original_tracks, start=1):
+ # self.tracks.append(gen_track(self, track, outdex=i, og=True, enabled=False))
tracks = apply_audio_filters(profile.audio_filters, original_tracks=original_tracks)
+ for track in self.tracks:
+ track.widgets.enable_check.setChecked(False)
+
if profile.audio_filters is not False and self.tracks and not tracks:
enable = yes_no_message(
t("No audio tracks matched for this profile, enable first track?"), title="No Audio Match"
)
if enable:
self.tracks[0].widgets.enable_check.setChecked(True)
- return super()._new_source(self.tracks)
+ return
# Apply first set of conversions to the original audio tracks
current_id = -1
@@ -555,8 +465,7 @@ def gen_track(
if track[0].index > current_id:
current_id = track[0].index
self.tracks[track[0].index - 1].widgets.enable_check.setChecked(True)
- update_track(
- self.tracks[track[0].index - 1],
+ self.tracks[track[0].index - 1].update_track(
downmix=track[1].downmix,
conversion=track[1].conversion,
bitrate=track[1].bitrate,
@@ -585,111 +494,40 @@ def gen_track(
super()._new_source(self.tracks)
def update_audio_settings(self):
- tracks = []
- for track in self.tracks:
- if track.enabled:
- tracks.append(
- AudioTrack(
- index=track.index,
- outdex=track.outdex,
- conversion_bitrate=track.conversion["bitrate"],
- conversion_codec=track.conversion["codec"],
- codec=track.codec,
- downmix=track.downmix,
- title=track.title,
- language=track.language,
- profile=track.profile,
- channels=track.channels,
- enabled=track.enabled,
- original=track.original,
- raw_info=track.all_info,
- friendly_info=track.audio,
- dispositions=track.dispositions,
- )
- )
- clear_list(self.app.fastflix.current_video.video_settings.audio_tracks)
- self.app.fastflix.current_video.video_settings.audio_tracks = tracks
+ return # TODO remove
def reload(self, original_tracks: list[AudioTrack], audio_formats):
+ clear_list(self.tracks)
disable_dups = (
"nvencc" in self.main.convert_to.lower()
or "vcenc" in self.main.convert_to.lower()
or "qsvenc" in self.main.convert_to.lower()
)
- repopulated_tracks = set()
- for track in original_tracks:
- if track.original:
- repopulated_tracks.add(track.index)
-
- new_track = Audio(
- parent=self,
- audio=track.friendly_info,
- all_info=Box(track.raw_info) if track.raw_info else None,
- title=track.title,
- language=track.language,
- profile=track.profile,
- original=track.original,
- index=track.index,
- outdex=track.outdex,
- codec=track.codec,
- codecs=audio_formats,
- channels=track.channels,
- available_audio_encoders=self.available_audio_encoders,
- enabled=True,
- disable_dup=disable_dups,
- dispositions=track.dispositions,
+ for i, track in enumerate(self.app.fastflix.current_video.audio_tracks):
+ self.tracks.append(
+ Audio(
+ app=self.app,
+ parent=self,
+ index=i,
+ disabled_dup=disable_dups,
+ )
)
- new_track.widgets.downmix.setCurrentText(track.downmix)
- new_track.widgets.convert_to.setCurrentText(track.conversion_codec)
- if track.conversion_codec in lossless:
- new_track.widgets.convert_bitrate.setDisabled(True)
- new_track.widgets.convert_bitrate.addItem("lossless")
- new_track.widgets.convert_bitrate.setCurrentText("lossless")
- else:
- if track.conversion_bitrate not in [
- new_track.widgets.convert_bitrate.itemText(i)
- for i in range(new_track.widgets.convert_bitrate.count())
- ]:
- new_track.widgets.convert_bitrate.addItem(track.conversion_bitrate)
- new_track.widgets.convert_bitrate.setCurrentText(track.conversion_bitrate)
- new_track.widgets.title.setText(track.title)
-
- if track.language:
- new_track.widgets.language.setCurrentText(Lang(track.language).name)
- else:
- new_track.widgets.language.setCurrentIndex(0)
-
- self.tracks.append(new_track)
+ super()._new_source(self.tracks)
- for i, x in enumerate(self.app.fastflix.current_video.streams.audio, start=1):
- if x.index in repopulated_tracks:
- continue
- track_info, tags = self._get_track_info(x)
- new_item = Audio(
- self,
- track_info,
- title=tags.get("title"),
- language=tags.get("language"),
- profile=x.get("profile"),
- original=True,
- index=x.index,
- outdex=i,
- codec=x.codec_name,
- codecs=audio_formats,
- channels=x.channels,
- available_audio_encoders=self.available_audio_encoders,
- enabled=False,
- all_info=x,
- disable_dup=disable_dups,
- )
- for idx, tk in enumerate(self.tracks):
- if tk.index > new_item.index:
- print(f"Inserting at {idx}")
- self.tracks.insert(idx, new_item)
- break
- else:
- self.tracks.append(new_item)
+ def move_up(self, widget):
+ self.app.fastflix.current_video.audio_tracks.insert(
+ widget.index - 1, self.app.fastflix.current_video.audio_tracks.pop(widget.index)
+ )
+ index = self.tracks.index(widget)
+ self.tracks.insert(index - 1, self.tracks.pop(index))
+ self.reorder()
- super()._new_source(self.tracks)
+ def move_down(self, widget):
+ self.app.fastflix.current_video.audio_tracks.insert(
+ widget.index + 1, self.app.fastflix.current_video.audio_tracks.pop(widget.index)
+ )
+ index = self.tracks.index(widget)
+ self.tracks.insert(index + 1, self.tracks.pop(index))
+ self.reorder()
diff --git a/fastflix/widgets/panels/cover_panel.py b/fastflix/widgets/panels/cover_panel.py
index 519b7892..cb61e64a 100644
--- a/fastflix/widgets/panels/cover_panel.py
+++ b/fastflix/widgets/panels/cover_panel.py
@@ -212,8 +212,8 @@ def update_cover_settings(self):
return
start_outdex = (
1 # Video Track
- + len(self.app.fastflix.current_video.video_settings.audio_tracks)
- + len(self.app.fastflix.current_video.video_settings.subtitle_tracks)
+ + len(self.app.fastflix.current_video.audio_tracks)
+ + len(self.app.fastflix.current_video.subtitle_tracks)
)
attachments: list[AttachmentTrack] = []
@@ -230,7 +230,7 @@ def update_cover_settings(self):
)
)
start_outdex += 1
- self.app.fastflix.current_video.video_settings.attachment_tracks = attachments
+ self.app.fastflix.current_video.attachment_tracks = attachments
def cover_passthrough_check(self):
checked = self.cover_passthrough_checkbox.isChecked()
@@ -354,14 +354,14 @@ def new_source(self, attachments):
self.cover_land_passthrough_checkbox.toggled.connect(lambda: self.cover_land_passthrough_check())
self.small_cover_land_passthrough_checkbox.toggled.connect(lambda: self.small_cover_land_passthrough_check())
- def reload_from_queue(self, streams, settings: VideoSettings):
+ def reload_from_queue(self, streams, attachment_tracks):
self.new_source(streams.attachment)
self.cover_passthrough_checkbox.setChecked(False)
self.cover_land_passthrough_checkbox.setChecked(False)
self.small_cover_land_passthrough_checkbox.setChecked(False)
self.small_cover_passthrough_checkbox.setChecked(False)
- for attachment in settings.attachment_tracks:
+ for attachment in attachment_tracks:
if attachment.filename == "cover":
if attachment.index is None:
self.cover_path.setText(str(attachment.file_path))
@@ -385,8 +385,8 @@ def reload_from_queue(self, streams, settings: VideoSettings):
# def update_cover_settings(self):
# start_outdex = (
# 1 # Video Track
-# + len(self.app.fastflix.current_video.video_settings.audio_tracks)
-# + len(self.app.fastflix.current_video.video_settings.subtitle_tracks)
+# + len(self.app.fastflix.current_video.audio_tracks)
+# + len(self.app.fastflix.current_video.subtitle_tracks)
# )
# attachments: list[AttachmentTrack] = []
#
diff --git a/fastflix/widgets/panels/queue_panel.py b/fastflix/widgets/panels/queue_panel.py
index 4d19c46e..9a8f0416 100644
--- a/fastflix/widgets/panels/queue_panel.py
+++ b/fastflix/widgets/panels/queue_panel.py
@@ -87,11 +87,11 @@ def __init__(self, parent, video: Video, index, first=False):
title.setFixedWidth(300)
settings = Box(copy.deepcopy(video.video_settings.dict()))
- settings.output_path = str(settings.output_path)
- for i, o in enumerate(settings.attachment_tracks):
- if o.get("file_path"):
- o["file_path"] = str(o["file_path"])
- del settings.conversion_commands
+ # settings.output_path = str(settings.output_path)
+ # for i, o in enumerate(video.attachment_tracks):
+ # if o.file_path:
+ # o["file_path"] = str(o["file_path"])
+ # del settings.conversion_commands
title.setToolTip(settings.video_encoder_settings.to_yaml())
del settings
@@ -144,8 +144,8 @@ def __init__(self, parent, video: Video, index, first=False):
# grid.addWidget(self.widgets.track_number, 0, 1)
grid.addWidget(title, 0, 1, 1, 3)
grid.addWidget(QtWidgets.QLabel(f"{video.video_settings.video_encoder_settings.name}"), 0, 4)
- grid.addWidget(QtWidgets.QLabel(f"{t('Audio Tracks')}: {len(video.video_settings.audio_tracks)}"), 0, 5)
- grid.addWidget(QtWidgets.QLabel(f"{t('Subtitles')}: {len(video.video_settings.subtitle_tracks)}"), 0, 6)
+ grid.addWidget(QtWidgets.QLabel(f"{t('Audio Tracks')}: {len(video.audio_tracks)}"), 0, 5)
+ grid.addWidget(QtWidgets.QLabel(f"{t('Subtitles')}: {len(video.subtitle_tracks)}"), 0, 6)
grid.addWidget(QtWidgets.QLabel(status), 0, 7)
if not video.status.error and video.status.complete and not get_bool_env("FF_DOCKERMODE"):
grid.addWidget(view_button, 0, 8)
diff --git a/fastflix/widgets/panels/subtitle_panel.py b/fastflix/widgets/panels/subtitle_panel.py
index 09a22244..f774e22c 100644
--- a/fastflix/widgets/panels/subtitle_panel.py
+++ b/fastflix/widgets/panels/subtitle_panel.py
@@ -49,22 +49,22 @@
class Subtitle(QtWidgets.QTabWidget):
extract_completed_signal = QtCore.Signal()
- def __init__(self, parent, subtitle, index, enabled=True, first=False, dispositions=None):
+ def __init__(self, app, parent, index, enabled=True, first=False):
self.loading = True
super(Subtitle, self).__init__(parent)
- self.parent = parent
+ self.app = app
+ self.parent: "SubtitleList" = parent
+ self.setObjectName("Subtitle")
self.index = index
self.outdex = None
- self.subtitle = Box(subtitle, default_box=True)
self.first = first
self.last = False
- self.subtitle_lang = subtitle.get("tags", {}).get("language")
- self.subtitle_type = subtitle_types.get(subtitle.get("codec_name", "text"), "text")
self.setFixedHeight(60)
+ sub_track: SubtitleTrack = self.app.fastflix.current_video.subtitle_tracks[index]
self.widgets = Box(
- track_number=QtWidgets.QLabel(f"{self.index}:{self.outdex}" if enabled else "❌"),
- title=QtWidgets.QLabel(f" {self.subtitle.codec_long_name}"),
+ track_number=QtWidgets.QLabel(f"{sub_track.index}:{sub_track.outdex}" if enabled else "❌"),
+ title=QtWidgets.QLabel(f" {sub_track.long_name}"),
up_button=QtWidgets.QPushButton(
QtGui.QIcon(get_icon("up-arrow", self.parent.app.fastflix.config.theme)), ""
),
@@ -72,7 +72,7 @@ def __init__(self, parent, subtitle, index, enabled=True, first=False, dispositi
QtGui.QIcon(get_icon("down-arrow", self.parent.app.fastflix.config.theme)), ""
),
enable_check=QtWidgets.QCheckBox(t("Preserve")),
- disposition=QtWidgets.QPushButton(),
+ disposition=QtWidgets.QPushButton(t("Dispositions")),
language=QtWidgets.QComboBox(),
burn_in=QtWidgets.QCheckBox(t("Burn In")),
)
@@ -97,7 +97,7 @@ def __init__(self, parent, subtitle, index, enabled=True, first=False, dispositi
# self.widgets.disposition.setCurrentIndex(dispositions.index("forced"))
self.setFixedHeight(60)
- self.widgets.title.setToolTip(self.subtitle.to_yaml())
+ # self.widgets.title.setToolTip(self.subtitle.to_yaml())
self.widgets.burn_in.setToolTip(
f"""{t("Overlay this subtitle track onto the video during conversion.")}\n
{t("Please make sure seek method is set to exact")}.\n
@@ -113,14 +113,10 @@ def __init__(self, parent, subtitle, index, enabled=True, first=False, dispositi
self.gif_label.setMovie(self.movie)
# self.movie.start()
- self.dispositions = dispositions if dispositions else {k: False for k in disposition_options}
- if not dispositions:
- for disposition, is_set in self.subtitle.disposition.items():
- if is_set:
- self.dispositions[disposition] = True
-
- self.disposition_widget = Disposition(self, f"Subtitle Track {index}", subs=True)
- self.set_dis_button()
+ self.disposition_widget = Disposition(
+ app=self.app, parent=self, track_name=f"Subtitle Track {index}", track_index=index, audio=False
+ )
+ # self.set_dis_button()
self.widgets.disposition.clicked.connect(self.disposition_widget.show)
disposition_layout = QtWidgets.QHBoxLayout()
@@ -132,7 +128,7 @@ def __init__(self, parent, subtitle, index, enabled=True, first=False, dispositi
self.grid.addWidget(self.widgets.track_number, 0, 1)
self.grid.addWidget(self.widgets.title, 0, 2)
self.grid.setColumnStretch(2, True)
- if self.subtitle_type == "text":
+ if sub_track.subtitle_type == "text":
self.grid.addWidget(self.widgets.extract, 0, 3)
self.grid.addWidget(self.gif_label, 0, 3)
self.gif_label.hide()
@@ -148,24 +144,6 @@ def __init__(self, parent, subtitle, index, enabled=True, first=False, dispositi
self.updating_burn = False
self.extract_completed_signal.connect(self.extraction_complete)
- def set_dis_button(self):
- output = ""
- for disposition, is_set in self.dispositions.items():
- if is_set:
- output += f"{t(disposition)},"
- if output:
- self.widgets.disposition.setText(output.rstrip(","))
- else:
- self.widgets.disposition.setText(t("none"))
-
- @property
- def dis_forced(self):
- return self.dispositions.get("forced", False)
-
- @property
- def dis_default(self):
- return self.dispositions.get("default", False)
-
def extraction_complete(self):
self.grid.addWidget(self.widgets.extract, 0, 3)
self.movie.stop()
@@ -214,19 +192,20 @@ def set_last(self, last=True):
self.widgets.down_button.setDisabled(self.last)
def set_outdex(self, outdex):
+ self.app.fastflix.current_video.subtitle_tracks[self.index].outdex = outdex
+ sub_track: SubtitleTrack = self.app.fastflix.current_video.subtitle_tracks[self.index]
self.outdex = outdex
if not self.enabled:
self.widgets.track_number.setText("❌")
else:
- self.widgets.track_number.setText(f"{self.index}:{self.outdex}")
-
- # @property
- # def disposition(self):
- # return None
+ self.widgets.track_number.setText(f"{sub_track.index}:{sub_track.outdex}")
@property
def enabled(self):
- return self.widgets.enable_check.isChecked()
+ try:
+ return self.app.fastflix.current_video.subtitle_tracks[self.index].enabled
+ except IndexError:
+ return False
@property
def language(self):
@@ -238,7 +217,9 @@ def burn_in(self):
def update_enable(self):
enabled = self.widgets.enable_check.isChecked()
- self.widgets.track_number.setText(f"{self.index}:{self.outdex}" if enabled else "❌")
+ sub_track = self.app.fastflix.current_video.subtitle_tracks[self.index]
+ sub_track.enabled = enabled
+ self.widgets.track_number.setText(f"{sub_track.index}:{sub_track.outdex}" if enabled else "❌")
self.parent.reorder(update=True)
def update_burn_in(self):
@@ -251,6 +232,8 @@ def update_burn_in(self):
error_message(t("There is an existing burn-in track, only one can be enabled at a time"))
if enable and self.parent.main.fast_time:
self.parent.main.widgets.fast_time.setCurrentText("exact")
+ sub_track = self.app.fastflix.current_video.subtitle_tracks[self.index]
+ sub_track.burn_in = enable
self.updating_burn = False
self.page_update()
@@ -263,7 +246,9 @@ class SubtitleList(FlixList):
def __init__(self, parent, app: FastFlixApp):
top_layout = QtWidgets.QHBoxLayout()
- top_layout.addWidget(QtWidgets.QLabel(t("Subtitle Tracks")))
+ label = QtWidgets.QLabel(t("Subtitle Tracks"))
+ label.setFixedHeight(30)
+ top_layout.addWidget(label)
top_layout.addStretch(1)
self.remove_all_button = QtWidgets.QPushButton(t("Unselect All"))
@@ -317,9 +302,28 @@ def lang_match(self, track: Union[Subtitle, dict], ignore_first=False):
def new_source(self):
self.tracks = []
self._first_selected = False
+ audio_end = len(self.app.fastflix.current_video.audio_tracks)
for index, track in enumerate(self.app.fastflix.current_video.streams.subtitle):
enabled = self.lang_match(track)
- new_item = Subtitle(self, track, index=track.index, first=True if index == 0 else False, enabled=enabled)
+ self.app.fastflix.current_video.subtitle_tracks.append(
+ SubtitleTrack(
+ index=track.index,
+ outdex=audio_end + index + 1,
+ dispositions={k: bool(v) for k, v in track.disposition.items()},
+ burn_in=False,
+ language=track.get("tags", {}).get("language", ""),
+ subtitle_type=subtitle_types.get(track.get("codec_name", "text"), "text"),
+ enabled=enabled,
+ )
+ )
+
+ new_item = Subtitle(
+ app=self.app,
+ parent=self,
+ index=index,
+ first=True if index == 0 else False,
+ enabled=enabled,
+ )
self.tracks.append(new_item)
if self.tracks:
self.tracks[0].set_first()
@@ -328,10 +332,18 @@ def new_source(self):
if self.app.fastflix.config.opt("subtitle_automatic_burn_in"):
first_default, first_forced = None, None
for track in self.tracks:
- if not first_default and track.dis_default and self.lang_match(track, ignore_first=True):
+ if (
+ not first_default
+ and self.app.fastflix.current_video.subtitle_tracks[track.index].dispositions.get("default", False)
+ and self.lang_match(track, ignore_first=True)
+ ):
first_default = track
break
- if not first_forced and track.dis_forced and self.lang_match(track, ignore_first=True):
+ if (
+ not first_forced
+ and self.app.fastflix.current_video.subtitle_tracks[track.index].dispositions.get("forced", False)
+ and self.lang_match(track, ignore_first=True)
+ ):
first_forced = track
break
if not self.app.fastflix.config.disable_automatic_subtitle_burn_in:
@@ -344,37 +356,35 @@ def new_source(self):
self.get_settings()
def get_settings(self):
- tracks = []
- burn_in_count = 0
- for track in self.tracks:
- if track.enabled:
- tracks.append(
- SubtitleTrack(
- index=track.index,
- outdex=track.outdex,
- dispositions=track.dispositions,
- language=track.language,
- burn_in=track.burn_in,
- subtitle_type=track.subtitle_type,
- )
- )
- if track.burn_in:
- burn_in_count += 1
- if burn_in_count > 1:
- raise FastFlixInternalException(t("More than one track selected to burn in"))
- clear_list(self.app.fastflix.current_video.video_settings.subtitle_tracks)
- self.app.fastflix.current_video.video_settings.subtitle_tracks = tracks
+ return # TODO remove
def reload(self, original_tracks):
- enabled_tracks = [x.index for x in original_tracks]
- self.new_source()
- for track in self.tracks:
- enabled = track.index in enabled_tracks
- track.widgets.enable_check.setChecked(enabled)
- if enabled:
- existing_track = [x for x in original_tracks if x.index == track.index][0]
- track.dispositions = existing_track.dispositions.copy()
- track.set_dis_button()
- track.widgets.burn_in.setChecked(existing_track.burn_in)
- track.widgets.language.setCurrentText(Lang(existing_track.language).name)
+ clear_list(self.tracks)
+
+ for i, track in enumerate(self.app.fastflix.current_video.subtitle_tracks):
+ self.tracks.append(
+ Subtitle(
+ app=self.app,
+ parent=self,
+ index=i,
+ first=True if i == 0 else False,
+ enabled=track.enabled,
+ )
+ )
super()._new_source(self.tracks)
+
+ def move_up(self, widget):
+ self.app.fastflix.current_video.subtitle_tracks.insert(
+ widget.index - 1, self.app.fastflix.current_video.subtitle_tracks.pop(widget.index)
+ )
+ index = self.tracks.index(widget)
+ self.tracks.insert(index - 1, self.tracks.pop(index))
+ self.reorder()
+
+ def move_down(self, widget):
+ self.app.fastflix.current_video.subtitle_tracks.insert(
+ widget.index + 1, self.app.fastflix.current_video.subtitle_tracks.pop(widget.index)
+ )
+ index = self.tracks.index(widget)
+ self.tracks.insert(index + 1, self.tracks.pop(index))
+ self.reorder()
diff --git a/fastflix/widgets/progress_bar.py b/fastflix/widgets/progress_bar.py
index ea13548c..edf55633 100644
--- a/fastflix/widgets/progress_bar.py
+++ b/fastflix/widgets/progress_bar.py
@@ -7,6 +7,7 @@
from PySide6 import QtCore, QtWidgets
from fastflix.language import t
+from fastflix.models.fastflix_app import FastFlixApp
logger = logging.getLogger("fastflix")
@@ -24,7 +25,7 @@ class ProgressBar(QtWidgets.QFrame):
def __init__(
self,
- app: QtWidgets.QApplication,
+ app: FastFlixApp,
tasks: list[Task],
signal_task: bool = False,
auto_run: bool = True,
@@ -86,6 +87,7 @@ def run(self):
else:
for i, task in enumerate(self.tasks, start=1):
+ logger.info(f"Running task {task.name}")
self.status.setText(task.name)
self.app.processEvents()
if self.app.fastflix.shutting_down:
diff --git a/fastflix/widgets/video_options.py b/fastflix/widgets/video_options.py
index f91308a6..e12bc80c 100644
--- a/fastflix/widgets/video_options.py
+++ b/fastflix/widgets/video_options.py
@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
import copy
import logging
+from typing import TYPE_CHECKING
from PySide6 import QtGui, QtWidgets, QtCore
@@ -19,6 +20,9 @@
from fastflix.widgets.panels.status_panel import StatusPanel
from fastflix.widgets.panels.subtitle_panel import SubtitleList
+if TYPE_CHECKING:
+ from fastflix.widgets.main import Main
+
logger = logging.getLogger("fastflix")
icons = {
@@ -38,7 +42,7 @@
class VideoOptions(QtWidgets.QTabWidget):
def __init__(self, parent, app: FastFlixApp, available_audio_encoders):
super().__init__(parent)
- self.main = parent
+ self.main: "Main" = parent
self.app = app
self.reloading = False
@@ -151,7 +155,7 @@ def change_conversion(self, conversion):
# Page update does a reload which bases itself off the current encoder so we have to do audio formats after
if not self.reloading:
self.audio.allowed_formats(self._get_audio_formats(encoder))
- self.update_profile()
+ # self.update_profile()
def get_settings(self):
if not self.app.fastflix.current_video:
@@ -192,6 +196,7 @@ def new_source(self):
self.current_settings.new_source()
self.queue.new_source()
self.advanced.new_source()
+ # TODO disable on loading from directory
self.info.reset()
self.debug.reset()
@@ -246,15 +251,16 @@ def reload(self):
if self.app.fastflix.current_video:
streams = copy.deepcopy(self.app.fastflix.current_video.streams)
settings = copy.deepcopy(self.app.fastflix.current_video.video_settings)
- audio_tracks = settings.audio_tracks
- subtitle_tracks = settings.subtitle_tracks
+ audio_tracks = copy.deepcopy(self.app.fastflix.current_video.audio_tracks or [])
+ subtitle_tracks = copy.deepcopy(self.app.fastflix.current_video.subtitle_tracks or [])
+ attachment_tracks = copy.deepcopy(self.app.fastflix.current_video.attachment_tracks or [])
try:
if getattr(self.main.current_encoder, "enable_audio", False):
self.audio.reload(audio_tracks, self.audio_formats)
if getattr(self.main.current_encoder, "enable_subtitles", False):
self.subtitles.reload(subtitle_tracks)
if getattr(self.main.current_encoder, "enable_attachments", False):
- self.attachments.reload_from_queue(streams, settings)
+ self.attachments.reload_from_queue(streams, attachment_tracks)
self.advanced.reset(settings=settings)
self.info.reset()
except Exception:
@@ -263,7 +269,7 @@ def reload(self):
self.debug.reset()
def clear_tracks(self):
- self.current_settings.update_profile()
+ # self.current_settings.update_profile()
self.audio.remove_all()
self.subtitles.remove_all()
self.attachments.clear_covers()
diff --git a/fastflix/widgets/windows/audio_conversion.py b/fastflix/widgets/windows/audio_conversion.py
new file mode 100644
index 00000000..b2a7869d
--- /dev/null
+++ b/fastflix/widgets/windows/audio_conversion.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+import logging
+
+from PySide6 import QtWidgets
+
+from fastflix.models.fastflix_app import FastFlixApp
+from fastflix.models.encode import AudioTrack
+
+
+from fastflix.language import t
+
+__all__ = ["AudioConversion"]
+
+logger = logging.getLogger("fastflix")
+
+# audio_disposition_options = [
+# "dub",
+# "original",
+# "comment",
+# "visual_impaired",
+# ]
+#
+# subtitle_disposition_options = [
+# "dub",
+# "original",
+# "comment",
+# "lyrics",
+# "karaoke",
+# "hearing_impaired",
+# ]
+
+channel_list = {
+ "mono": 1,
+ "stereo": 2,
+ "2.1": 3,
+ "3.0": 3,
+ "3.0(back)": 3,
+ "3.1": 4,
+ "4.0": 4,
+ "quad": 4,
+ "quad(side)": 4,
+ "5.0": 5,
+ "5.1": 6,
+ "6.0": 6,
+ "6.0(front)": 6,
+ "hexagonal": 6,
+ "6.1": 7,
+ "6.1(front)": 7,
+ "7.0": 7,
+ "7.0(front)": 7,
+ "7.1": 8,
+ "7.1(wide)": 8,
+}
+
+
+class AudioConversion(QtWidgets.QWidget):
+ def __init__(self, app: FastFlixApp, track_index, encoders, audio_track_update):
+ super().__init__(None)
+ self.app = app
+ self.audio_track_update = audio_track_update
+ self.setWindowTitle(f"Audio Conversion for Track {track_index}")
+ self.setMinimumWidth(400)
+ self.audio_track: AudioTrack = self.app.fastflix.current_video.audio_tracks[track_index]
+
+ # Conversion
+
+ self.conversion_codec = QtWidgets.QComboBox()
+ self.conversion_codec.addItems([t("None")] + list(sorted(encoders)))
+
+ if self.audio_track.conversion_codec:
+ self.conversion_codec.setCurrentText(self.audio_track.conversion_codec)
+ self.conversion_codec.currentIndexChanged.connect(self.codec_changed)
+
+ conversion_layout = QtWidgets.QHBoxLayout()
+ conversion_layout.addWidget(QtWidgets.QLabel(t("Codec")))
+ conversion_layout.addWidget(self.conversion_codec, 2)
+
+ # AQ vs Bitrate
+
+ self.aq = QtWidgets.QComboBox()
+ self.aq.addItems(
+ [
+ f"0 - {t('Near Lossless')}",
+ "1",
+ f"2 - {t('High Quality')}",
+ "3",
+ f"4 - {t('Medium Quality')}",
+ "5",
+ f"6 {t('Low Quality')}",
+ "7",
+ "8",
+ "9",
+ t("Custom Bitrate"),
+ ]
+ )
+ self.aq.setMinimumWidth(100)
+ self.aq.currentIndexChanged.connect(self.set_aq)
+ self.bitrate = QtWidgets.QLineEdit()
+ self.bitrate.setFixedWidth(50)
+
+ if self.audio_track.conversion_aq:
+ self.aq.setCurrentIndex(self.audio_track.conversion_aq)
+ self.bitrate.setDisabled(True)
+ elif self.audio_track.conversion_bitrate:
+ self.aq.setCurrentText(t("Custom Bitrate"))
+ self.bitrate.setText(self.audio_track.conversion_bitrate)
+ self.bitrate.setEnabled(True)
+
+ elif self.conversion_codec.currentText() in ["libopus"]:
+ self.aq.setCurrentIndex(10)
+ else:
+ self.aq.setCurrentIndex(3)
+
+ quality_layout = QtWidgets.QHBoxLayout()
+ quality_layout.addWidget(QtWidgets.QLabel(t("Audio Quality")))
+ quality_layout.addWidget(self.aq, 1)
+ quality_layout.addWidget(QtWidgets.QLabel(t("Bitrate")))
+ quality_layout.addWidget(self.bitrate)
+ quality_layout.addWidget(QtWidgets.QLabel("kb/s"))
+
+ # Downmix
+
+ self.downmix = QtWidgets.QComboBox()
+ self.downmix.addItems([t("None")] + list(channel_list.keys()))
+ self.downmix.setCurrentIndex(2)
+ if self.audio_track.downmix:
+ self.downmix.setCurrentText(self.audio_track.downmix)
+
+ downmix_layout = QtWidgets.QHBoxLayout()
+ downmix_layout.addWidget(QtWidgets.QLabel(t("Channel Layout")))
+ downmix_layout.addWidget(self.downmix, 2)
+
+ # Yes No
+
+ yes_no_layout = QtWidgets.QHBoxLayout()
+ cancel = QtWidgets.QPushButton(t("Cancel"))
+ cancel.clicked.connect(self.close)
+ yes_no_layout.addWidget(cancel)
+ save = QtWidgets.QPushButton(t("Save"))
+ save.clicked.connect(self.save)
+ yes_no_layout.addWidget(save)
+
+ layout = QtWidgets.QVBoxLayout()
+ layout.addLayout(conversion_layout)
+ layout.addLayout(quality_layout)
+ layout.addLayout(downmix_layout)
+ layout.addLayout(yes_no_layout)
+
+ self.setLayout(layout)
+
+ def set_aq(self):
+ index = self.aq.currentIndex()
+ if index == 10:
+ self.bitrate.setEnabled(True)
+ else:
+ self.bitrate.setDisabled(True)
+
+ def codec_changed(self):
+ if self.conversion_codec.currentText() in ["libopus"]:
+ self.aq.setCurrentIndex(10)
+ self.aq.setDisabled(True)
+ # self.bitrate.setEnabled(True)
+ else:
+ self.aq.setEnabled(True)
+ # self.bitrate.setDisabled(True)
+
+ def save(self):
+ if self.conversion_codec.currentIndex() != 0:
+ self.audio_track.conversion_codec = self.conversion_codec.currentText()
+ else:
+ self.audio_track.conversion_codec = ""
+
+ if self.aq.currentIndex() != 10:
+ self.audio_track.conversion_aq = self.aq.currentIndex()
+ self.audio_track.conversion_bitrate = None
+ else:
+ self.audio_track.conversion_bitrate = self.bitrate.text()
+ self.audio_track.conversion_aq = None
+
+ if self.downmix.currentIndex() != 0:
+ self.audio_track.downmix = self.downmix.currentText()
+ else:
+ self.audio_track.downmix = None
+ self.audio_track_update()
+ self.close()
diff --git a/fastflix/widgets/windows/disposition.py b/fastflix/widgets/windows/disposition.py
index 0a6ff507..9ad0d9e5 100644
--- a/fastflix/widgets/windows/disposition.py
+++ b/fastflix/widgets/windows/disposition.py
@@ -13,6 +13,7 @@
from fastflix.encoders.common import helpers
from fastflix.resources import get_icon
from fastflix.language import t
+from fastflix.models.fastflix_app import FastFlixApp
__all__ = ["Disposition"]
@@ -36,12 +37,15 @@
class Disposition(QtWidgets.QWidget):
- def __init__(self, parent, track_name, subs=False):
+ def __init__(self, app: FastFlixApp, parent, track_name, track_index, audio=True):
super().__init__(None)
self.parent = parent
+ self.app = app
self.track_name = track_name
- self.subs = subs
- self.dispositions = parent.dispositions
+ self.track_index = track_index
+ self.audio = audio
+
+ self.setMinimumWidth(400)
self.forced = QtWidgets.QCheckBox(t("Forced"))
@@ -67,13 +71,13 @@ def __init__(self, parent, track_name, subs=False):
group.addButton(none_extra)
layout.addWidget(none_extra)
- if subs:
- for dis in subtitle_disposition_options:
+ if audio:
+ for dis in audio_disposition_options:
self.widgets[dis] = QtWidgets.QRadioButton(t(dis))
group.addButton(self.widgets[dis])
layout.addWidget(self.widgets[dis])
else:
- for dis in audio_disposition_options:
+ for dis in subtitle_disposition_options:
self.widgets[dis] = QtWidgets.QRadioButton(t(dis))
group.addButton(self.widgets[dis])
layout.addWidget(self.widgets[dis])
@@ -85,23 +89,31 @@ def __init__(self, parent, track_name, subs=False):
self.setLayout(layout)
def set_dispositions(self):
- self.parent.dispositions["forced"] = self.forced.isChecked()
- self.parent.dispositions["default"] = self.default.isChecked()
+ if self.audio:
+ track = self.app.fastflix.current_video.audio_tracks[self.track_index]
+ else:
+ track = self.app.fastflix.current_video.subtitle_tracks[self.track_index]
+
+ track.dispositions["forced"] = self.forced.isChecked()
+ track.dispositions["default"] = self.default.isChecked()
for dis in self.widgets:
- self.parent.dispositions[dis] = self.widgets[dis].isChecked()
- self.parent.set_dis_button()
+ track.dispositions[dis] = self.widgets[dis].isChecked()
self.parent.page_update()
self.hide()
def show(self):
- self.forced.setChecked(self.parent.dispositions["forced"])
- self.default.setChecked(self.parent.dispositions["default"])
+ if self.audio:
+ dispositions = self.app.fastflix.current_video.audio_tracks[self.track_index].dispositions
+ else:
+ dispositions = self.app.fastflix.current_video.subtitle_tracks[self.track_index].dispositions
for dis in self.widgets:
- self.widgets[dis].setChecked(self.parent.dispositions.get(dis, False))
+ self.widgets[dis].setChecked(dispositions.get(dis, False))
super().show()
def close(self) -> bool:
del self.parent
- del self.subs
- del self.dispositions
+ del self.app
+ del self.track_name
+ del self.track_index
+ del self.audio
return super().close()
diff --git a/fastflix/widgets/windows/hdr10plus_inject.py b/fastflix/widgets/windows/hdr10plus_inject.py
new file mode 100644
index 00000000..39772e5f
--- /dev/null
+++ b/fastflix/widgets/windows/hdr10plus_inject.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+from pathlib import Path
+import os
+import logging
+import secrets
+from subprocess import run, PIPE
+
+from PySide6 import QtWidgets, QtGui, QtCore
+from PySide6.QtWidgets import QAbstractItemView
+
+from fastflix.language import t
+from fastflix.flix import probe
+from fastflix.shared import yes_no_message, error_message
+from fastflix.widgets.progress_bar import ProgressBar, Task
+from fastflix.resources import group_box_style, get_icon
+
+logger = logging.getLogger("fastflix")
+
+
+class HDR10PlusInjectWindow(QtWidgets.QWidget):
+ def __init__(self, app, main, items=None):
+ super().__init__(None)
+ self.app = app
+ self.main = main
+ self.selected_stream = None
+
+ self.movie_file = QtWidgets.QLineEdit()
+ self.movie_file.setEnabled(False)
+ self.movie_file.setFixedWidth(400)
+ self.movie_file_button = QtWidgets.QPushButton(
+ icon=QtGui.QIcon(get_icon("onyx-output", self.app.fastflix.config.theme))
+ )
+ self.movie_file_button.clicked.connect(self.movie_open)
+
+ self.hdr10p_file = QtWidgets.QLineEdit()
+ self.hdr10p_file.setEnabled(False)
+ self.hdr10p_file_button = QtWidgets.QPushButton(
+ icon=QtGui.QIcon(get_icon("onyx-output", self.app.fastflix.config.theme))
+ )
+ self.hdr10p_file_button.clicked.connect(self.hdr10p_open)
+
+ self.output_file = QtWidgets.QLineEdit()
+ self.output_file.setFixedWidth(400)
+ self.output_file_button = QtWidgets.QPushButton(
+ icon=QtGui.QIcon(get_icon("onyx-output", self.app.fastflix.config.theme))
+ )
+ self.output_file_button.clicked.connect(self.set_output_file)
+ self.output_file.textChanged.connect(self.prep_command)
+
+ line_1 = QtWidgets.QHBoxLayout()
+ line_1.addWidget(QtWidgets.QLabel("Movie File"))
+ line_1.addWidget(self.movie_file)
+ line_1.addWidget(self.movie_file_button)
+
+ line_3 = QtWidgets.QHBoxLayout()
+ line_3.addWidget(QtWidgets.QLabel("HDR10+ File"))
+ line_3.addWidget(self.hdr10p_file)
+ line_3.addWidget(self.hdr10p_file_button)
+
+ self.info_bubble = QtWidgets.QLabel("")
+ self.command_bubble = QtWidgets.QLineEdit("")
+ self.command_bubble.setFixedWidth(400)
+ # self.command_bubble.setWordWrap(True)
+ # self.command_bubble.setFixedHeight(400)
+
+ layout = QtWidgets.QVBoxLayout()
+
+ output_lin = QtWidgets.QHBoxLayout()
+ output_lin.addWidget(QtWidgets.QLabel("Output File"))
+ output_lin.addWidget(self.output_file)
+ output_lin.addWidget(self.output_file_button)
+
+ bottom_line = QtWidgets.QHBoxLayout()
+ cancel = QtWidgets.QPushButton("Cancel")
+ cancel.clicked.connect(self.hide)
+ bottom_line.addWidget(cancel)
+ start = QtWidgets.QPushButton("Start")
+ start.clicked.connect(self.start)
+ bottom_line.addWidget(start)
+
+ layout.addLayout(line_1)
+ layout.addWidget(self.info_bubble)
+ layout.addLayout(line_3)
+ layout.addLayout(output_lin)
+ layout.addWidget(self.command_bubble)
+ layout.addLayout(bottom_line)
+ self.setLayout(layout)
+
+ def movie_open(self):
+ self.selected_stream = None
+ self.movie_file.setText("")
+ movie_name = QtWidgets.QFileDialog.getOpenFileName(self)
+ if not movie_name or not movie_name[0]:
+ return
+ try:
+ results = probe(self.app, movie_name[0])
+ except Exception as err:
+ error_message(f"Invalid file: {err}")
+ return
+ for result in results["streams"]:
+ if result["codec_type"] == "video":
+ if result["codec_name"] == "hevc":
+ self.selected_stream = result
+ break
+ if not self.selected_stream:
+ error_message("No HEVC video stream found")
+ return
+ self.info_bubble.setText(f"Selected stream index: {self.selected_stream['index']}")
+ self.movie_file.setText(movie_name[0])
+ self.prep_command()
+
+ def hdr10p_open(self):
+ hdr10p_file = QtWidgets.QFileDialog.getOpenFileName(self)
+ if not hdr10p_file or not hdr10p_file[0]:
+ return
+ self.hdr10p_file.setText(hdr10p_file[0])
+ self.prep_command()
+
+ def set_output_file(self):
+ filename = QtWidgets.QFileDialog.getSaveFileName(
+ self,
+ caption="Save Video As",
+ # dir=str(Path(*self.generate_output_filename)) + f"{self.widgets.output_type_combo.currentText()}",
+ # filter=f"Save File (*.{extension})",
+ )
+ if filename and filename[0]:
+ self.output_file.setText(filename[0])
+ self.prep_command()
+
+ def prep_command(self):
+ print("called prep")
+ if not self.movie_file.text() or not self.hdr10p_file.text() or not self.output_file.text():
+ print("Nope", "1", self.movie_file.text(), "2", self.hdr10p_file.text(), "3", self.output_file.text())
+ return
+
+ command = (
+ f'{self.app.fastflix.config.ffmpeg} -loglevel panic -i "{self.movie_file.text()}" '
+ f'-map 0:{self.selected_stream["index"]} -c:v copy -vbsf hevc_mp4toannexb -f hevc - | '
+ f"{self.app.fastflix.config.hdr10plus_parser} inject -i - -j {self.hdr10p_file.text()} -o - | "
+ f'{self.app.fastflix.config.ffmpeg} -loglevel panic -i - -i {self.movie_file.text()} -map 0:0 -c:0 copy -map 1:a -map 1:s -map 1:d -c:1 copy "{self.output_file.text()}"'
+ )
+
+ print(command)
+ self.command_bubble.setText(command)
+
+ def start(self):
+ run(self.command_bubble.text(), shell=True)
+ error_message("Done")
diff --git a/pyproject.toml b/pyproject.toml
index ebf122d7..c92c2539 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.black]
line-length = 120
-target-version = ['py311']
+target-version = ['py312']
exclude = '''
/(
\.eggs
@@ -21,7 +21,7 @@ authors = [{ name = "Chris Griffith", email = "chris@cdgriffith.com" }]
readme = "README.md"
#url = "https://fastflix.org"
#download_url = "https://github.com/cdgriffith/FastFlix/releases"
-requires-python = ">=3.11,<3.12"
+requires-python = ">=3.12"
dynamic = ["version"]
dependencies = [
"appdirs~=1.4",
@@ -30,10 +30,11 @@ dependencies = [
"coloredlogs>=15.0,<16.0",
"iso639-lang==0.0.9",
"mistune>=2.0,<3.0",
+ "packaging>=23.2",
"pathvalidate>=2.4,<3.0",
"psutil>=5.9,<6.0",
"pydantic>=1.9,<2.0",
- "pyside6>=6.4.2,<7.0",
+ "pyside6>=6.4.2",
"python-box[all]>=6.0,<7.0",
"requests>=2.28,<3.0",
"reusables>=0.9.6,<0.10.0",
@@ -43,7 +44,7 @@ dependencies = [
develop = [
"wheel>=0.38.4",
"typing_extensions>=4.4",
- "pyinstaller>=5.7",
+ "pyinstaller>=6.8",
"pytest>=7.3",
"types-requests>=2.28",
"types-setuptools>=65.7",
diff --git a/tests/test_application.py b/tests/test_application.py
index 56018a23..9a29c75e 100644
--- a/tests/test_application.py
+++ b/tests/test_application.py
@@ -1,9 +1,15 @@
# -*- coding: utf-8 -*-
+from unittest.mock import MagicMock
+
from box import Box
from fastflix.application import init_encoders, create_app
from fastflix.models.config import Config
+# from fastflix.widgets.container import Container
+
+# import pytest
+
fake_app = Box(default_box=True)
fake_app.fastflix.config = Config()
@@ -13,6 +19,18 @@ def test_init_encoders():
assert "encoders" in fake_app.fastflix
+# def test_app(qtbot):
+# # app = create_app(enable_scaling=False)
+# container = Container(MagicMock())
+# qtbot.addWidget(container)
+# from pytestqt.qt_compat import qt_api
+#
+# qtbot.mouseClick(container.menuBar(), qt_api.QtCore.Qt.MouseButton.LeftButton)
+#
+# assert container.menuBar().actions()[0].text() == "File"
+# #assert widget.greet_label.text() == "Hello!"
+
+
# def test_get_ffmpeg_version():
# ffmpeg_configuration(fake_app, fake_app.fastflix.config)
# assert getattr(fake_app.fastflix, "ffmpeg_version")
diff --git a/tests/test_version_check.py b/tests/test_version_check.py
index 0ad54442..fdb4223b 100644
--- a/tests/test_version_check.py
+++ b/tests/test_version_check.py
@@ -1,18 +1,17 @@
# -*- coding: utf-8 -*-
from subprocess import run, PIPE
import re
-from distutils.version import StrictVersion
+from packaging import version
import requests
-from box import Box
def test_version():
with open("fastflix/version.py") as version_file:
- code_version = StrictVersion(re.search(r"__version__ *= *['\"](.+)['\"]", version_file.read()).group(1))
+ code_version = version.parse(re.search(r"__version__ *= *['\"](.+)['\"]", version_file.read()).group(1))
url = "https://api.github.com/repos/cdgriffith/FastFlix/releases/latest"
data = requests.get(url).json()
assert (
- StrictVersion(data["tag_name"]) < code_version
- ), f"Last Release Version {StrictVersion(data['tag_name'])} vs Code Version {code_version}"
+ version.parse(data["tag_name"]) < code_version
+ ), f"Last Release Version {version.parse(data['tag_name'])} vs Code Version {code_version}"
diff --git a/velocemente/__init__.py b/velocemente/__init__.py
new file mode 100644
index 00000000..e69de29b