Skip to content

Commit 880ca07

Browse files
fix: stop ffmpeg cmd windows, refactor ffmpeg_checker (#855)
* fix: remove log statement as it is redundant (#840) * refactor: rework ffmpeg_checker.py Move backend logic from ffmpeg_checker.py to vendored/ffmpeg.py, add translation strings for ffmpeg_checker, update vendored/ffmpeg.py * fix: stop ffmpeg cmd windows, fix version outputs * chore: ensure stdout is cast to str --------- Co-authored-by: Jann Stute <[email protected]>
1 parent 0701a45 commit 880ca07

File tree

7 files changed

+141
-71
lines changed

7 files changed

+141
-71
lines changed

src/tagstudio/qt/helpers/file_tester.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import ffmpeg
99

10-
from tagstudio.qt.helpers.vendored.ffmpeg import _probe
10+
from tagstudio.qt.helpers.vendored.ffmpeg import probe
1111

1212

1313
def is_readable_video(filepath: Path | str):
@@ -19,8 +19,8 @@ def is_readable_video(filepath: Path | str):
1919
filepath (Path | str): The filepath of the video to check.
2020
"""
2121
try:
22-
probe = _probe(Path(filepath))
23-
for stream in probe["streams"]:
22+
result = probe(Path(filepath))
23+
for stream in result["streams"]:
2424
# DRM check
2525
if stream.get("codec_tag_string") in [
2626
"drma",

src/tagstudio/qt/helpers/silent_popen.py

+76
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,79 @@ def silent_Popen( # noqa: N802
8484
pipesize=pipesize,
8585
process_group=process_group,
8686
)
87+
88+
89+
def silent_run( # noqa: N802
90+
args,
91+
bufsize=-1,
92+
executable=None,
93+
stdin=None,
94+
stdout=None,
95+
stderr=None,
96+
preexec_fn=None,
97+
close_fds=True,
98+
shell=False,
99+
cwd=None,
100+
env=None,
101+
universal_newlines=None,
102+
startupinfo=None,
103+
creationflags=0,
104+
restore_signals=True,
105+
start_new_session=False,
106+
pass_fds=(),
107+
*,
108+
capture_output=False,
109+
group=None,
110+
extra_groups=None,
111+
user=None,
112+
umask=-1,
113+
encoding=None,
114+
errors=None,
115+
text=None,
116+
pipesize=-1,
117+
process_group=None,
118+
):
119+
"""Call subprocess.run without creating a console window."""
120+
if sys.platform == "win32":
121+
creationflags |= subprocess.CREATE_NO_WINDOW
122+
import ctypes
123+
124+
ctypes.windll.kernel32.SetDllDirectoryW(None)
125+
elif (
126+
sys.platform == "linux"
127+
or sys.platform.startswith("freebsd")
128+
or sys.platform.startswith("openbsd")
129+
):
130+
# pass clean environment to the subprocess
131+
env = os.environ
132+
original_env = env.get("LD_LIBRARY_PATH_ORIG")
133+
env["LD_LIBRARY_PATH"] = original_env if original_env else ""
134+
135+
return subprocess.run(
136+
args=args,
137+
bufsize=bufsize,
138+
executable=executable,
139+
stdin=stdin,
140+
stdout=stdout,
141+
stderr=stderr,
142+
preexec_fn=preexec_fn,
143+
close_fds=close_fds,
144+
shell=shell,
145+
cwd=cwd,
146+
env=env,
147+
startupinfo=startupinfo,
148+
creationflags=creationflags,
149+
restore_signals=restore_signals,
150+
start_new_session=start_new_session,
151+
pass_fds=pass_fds,
152+
capture_output=capture_output,
153+
group=group,
154+
extra_groups=extra_groups,
155+
user=user,
156+
umask=umask,
157+
encoding=encoding,
158+
errors=errors,
159+
text=text,
160+
pipesize=pipesize,
161+
process_group=process_group,
162+
)

src/tagstudio/qt/helpers/vendored/ffmpeg.py

+31-7
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
# Licensed under the GPL-3.0 License.
33
# Vendored from ffmpeg-python and ffmpeg-python PR#790 by amamic1803
44

5+
import contextlib
56
import json
67
import platform
7-
import shutil
88
import subprocess
9+
from shutil import which
910

1011
import ffmpeg
1112
import structlog
1213

13-
from tagstudio.qt.helpers.silent_popen import silent_Popen
14+
from tagstudio.qt.helpers.silent_popen import silent_Popen, silent_run
1415

1516
logger = structlog.get_logger(__name__)
1617

@@ -21,29 +22,33 @@ def _get_ffprobe_location() -> str:
2122
cmd: str = "ffprobe"
2223
if platform.system() == "Darwin":
2324
for loc in FFMPEG_MACOS_LOCATIONS:
24-
if shutil.which(loc + cmd):
25+
if which(loc + cmd):
2526
cmd = loc + cmd
2627
break
27-
logger.info(f"[FFMPEG] Using FFprobe location: {cmd}")
28+
logger.info(
29+
f"[FFmpeg] Using FFprobe location: {cmd}{' (Found)' if which(cmd) else ' (Not Found)'}"
30+
)
2831
return cmd
2932

3033

3134
def _get_ffmpeg_location() -> str:
3235
cmd: str = "ffmpeg"
3336
if platform.system() == "Darwin":
3437
for loc in FFMPEG_MACOS_LOCATIONS:
35-
if shutil.which(loc + cmd):
38+
if which(loc + cmd):
3639
cmd = loc + cmd
3740
break
38-
logger.info(f"[FFMPEG] Using FFmpeg location: {cmd}")
41+
logger.info(
42+
f"[FFmpeg] Using FFmpeg location: {cmd}{' (Found)' if which(cmd) else ' (Not Found)'}"
43+
)
3944
return cmd
4045

4146

4247
FFPROBE_CMD = _get_ffprobe_location()
4348
FFMPEG_CMD = _get_ffmpeg_location()
4449

4550

46-
def _probe(filename, cmd=FFPROBE_CMD, timeout=None, **kwargs):
51+
def probe(filename, cmd=FFPROBE_CMD, timeout=None, **kwargs):
4752
"""Run ffprobe on the specified file and return a JSON representation of the output.
4853
4954
Raises:
@@ -65,3 +70,22 @@ def _probe(filename, cmd=FFPROBE_CMD, timeout=None, **kwargs):
6570
if p.returncode != 0:
6671
raise ffmpeg.Error("ffprobe", out, err)
6772
return json.loads(out.decode("utf-8"))
73+
74+
75+
def version():
76+
"""Checks the version of FFmpeg and FFprobe and returns None if they dont exist."""
77+
version: dict[str, str | None] = {"ffmpeg": None, "ffprobe": None}
78+
79+
if which(FFMPEG_CMD):
80+
ret = silent_run([FFMPEG_CMD, "-version"], shell=False, capture_output=True, text=True)
81+
if ret.returncode == 0:
82+
with contextlib.suppress(Exception):
83+
version["ffmpeg"] = str(ret.stdout).split(" ")[2]
84+
85+
if which(FFPROBE_CMD):
86+
ret = silent_run([FFPROBE_CMD, "-version"], shell=False, capture_output=True, text=True)
87+
if ret.returncode == 0:
88+
with contextlib.suppress(Exception):
89+
version["ffprobe"] = str(ret.stdout).split(" ")[2]
90+
91+
return version

src/tagstudio/qt/modals/about.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from tagstudio.core.constants import VERSION, VERSION_BRANCH
2222
from tagstudio.core.enums import Theme
2323
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
24-
from tagstudio.qt.modals.ffmpeg_checker import FfmpegChecker
24+
from tagstudio.qt.helpers.vendored import ffmpeg
2525
from tagstudio.qt.resource_manager import ResourceManager
2626
from tagstudio.qt.translations import Translations
2727

@@ -31,7 +31,6 @@ def __init__(self, config_path):
3131
super().__init__()
3232
self.setWindowTitle(Translations["about.title"])
3333

34-
self.fc: FfmpegChecker = FfmpegChecker()
3534
self.rm: ResourceManager = ResourceManager()
3635

3736
# TODO: There should be a global button theme somewhere.
@@ -82,7 +81,7 @@ def __init__(self, config_path):
8281
self.desc_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
8382

8483
# System Info ----------------------------------------------------------
85-
ff_version = self.fc.version()
84+
ff_version = ffmpeg.version()
8685
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
8786
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
8887
missing = Translations["generic.missing"]
+21-53
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import contextlib
2-
import subprocess
31
from shutil import which
42

53
import structlog
64
from PySide6.QtCore import Qt, QUrl
75
from PySide6.QtGui import QDesktopServices
86
from PySide6.QtWidgets import QMessageBox
97

8+
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
109
from tagstudio.qt.helpers.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
10+
from tagstudio.qt.translations import Translations
1111

1212
logger = structlog.get_logger(__name__)
1313

@@ -20,10 +20,11 @@ class FfmpegChecker(QMessageBox):
2020
def __init__(self):
2121
super().__init__()
2222

23-
self.setWindowTitle("Warning: Missing dependency")
24-
self.setText("Warning: Could not find FFmpeg installation")
23+
ffmpeg = "FFmpeg"
24+
ffprobe = "FFprobe"
25+
title = Translations.format("dependency.missing.title", dependency=ffmpeg)
26+
self.setWindowTitle(title)
2527
self.setIcon(QMessageBox.Icon.Warning)
26-
# Blocks other application interactions until resolved
2728
self.setWindowModality(Qt.WindowModality.ApplicationModal)
2829

2930
self.setStandardButtons(
@@ -34,52 +35,19 @@ def __init__(self):
3435
self.setDefaultButton(QMessageBox.StandardButton.Ignore)
3536
# Enables the cancel button but hides it to allow for click X to close dialog
3637
self.button(QMessageBox.StandardButton.Cancel).hide()
38+
self.button(QMessageBox.StandardButton.Help).clicked.connect(
39+
lambda: QDesktopServices.openUrl(QUrl(self.HELP_URL))
40+
)
3741

38-
self.ffmpeg = False
39-
self.ffprobe = False
40-
41-
def installed(self):
42-
"""Checks if both FFmpeg and FFprobe are installed and in the PATH."""
43-
if which(FFMPEG_CMD):
44-
self.ffmpeg = True
45-
if which(FFPROBE_CMD):
46-
self.ffprobe = True
47-
48-
logger.info("FFmpeg found: {self.ffmpeg}, FFprobe found: {self.ffprobe}")
49-
return self.ffmpeg and self.ffprobe
50-
51-
def version(self):
52-
"""Checks the version of ffprobe and ffmpeg and returns None if they dont exist."""
53-
version: dict[str, str | None] = {"ffprobe": None, "ffmpeg": None}
54-
self.installed()
55-
if self.ffprobe:
56-
ret = subprocess.run(
57-
[FFPROBE_CMD, "-show_program_version"], shell=False, capture_output=True, text=True
58-
)
59-
if ret.returncode == 0:
60-
with contextlib.suppress(Exception):
61-
version["ffprobe"] = ret.stdout.split("\n")[1].replace("-", "=").split("=")[1]
62-
if self.ffmpeg:
63-
ret = subprocess.run(
64-
[FFMPEG_CMD, "-version"], shell=False, capture_output=True, text=True
65-
)
66-
if ret.returncode == 0:
67-
with contextlib.suppress(Exception):
68-
version["ffmpeg"] = ret.stdout.replace("-", " ").split(" ")[2]
69-
return version
70-
71-
def show_warning(self):
72-
"""Displays the warning to the user and awaits response."""
73-
missing = "FFmpeg"
74-
# If ffmpeg is installed but not ffprobe
75-
if not self.ffprobe and self.ffmpeg:
76-
missing = "FFprobe"
77-
78-
self.setText(f"Warning: Could not find {missing} installation")
79-
self.setInformativeText(f"{missing} is required for multimedia thumbnails and playback")
80-
# Shows the dialog
81-
selection = self.exec()
82-
83-
# Selection will either be QMessageBox.Help or (QMessageBox.Ignore | QMessageBox.Cancel)
84-
if selection == QMessageBox.StandardButton.Help:
85-
QDesktopServices.openUrl(QUrl(self.HELP_URL))
42+
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
43+
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
44+
missing = f"<span style='color:{red}'>{Translations["generic.missing"]}</span>"
45+
found = f"<span style='color:{green}'>{Translations['about.module.found']}</span>"
46+
status = Translations.format(
47+
"ffmpeg.missing.status",
48+
ffmpeg=ffmpeg,
49+
ffmpeg_status=found if which(FFMPEG_CMD) else missing,
50+
ffprobe=ffprobe,
51+
ffprobe_status=found if which(FFPROBE_CMD) else missing,
52+
)
53+
self.setText(f"{Translations["ffmpeg.missing.description"]}<br><br>{status}")

src/tagstudio/qt/ts_qt.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import time
2020
from pathlib import Path
2121
from queue import Queue
22+
from shutil import which
2223
from warnings import catch_warnings
2324

2425
import structlog
@@ -76,6 +77,7 @@
7677
from tagstudio.qt.helpers.custom_runnable import CustomRunnable
7778
from tagstudio.qt.helpers.file_deleter import delete_file
7879
from tagstudio.qt.helpers.function_iterator import FunctionIterator
80+
from tagstudio.qt.helpers.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
7981
from tagstudio.qt.main_window import Ui_MainWindow
8082
from tagstudio.qt.modals.about import AboutModal
8183
from tagstudio.qt.modals.build_tag import BuildTagPanel
@@ -676,11 +678,9 @@ def create_about_modal():
676678
if path_result.success and path_result.library_path:
677679
self.open_library(path_result.library_path)
678680

679-
# check ffmpeg and show warning if not
680-
# NOTE: Does this need to use self?
681-
self.ffmpeg_checker = FfmpegChecker()
682-
if not self.ffmpeg_checker.installed():
683-
self.ffmpeg_checker.show_warning()
681+
# Check if FFmpeg or FFprobe are missing and show warning if so
682+
if not which(FFMPEG_CMD) or not which(FFPROBE_CMD):
683+
FfmpegChecker().show()
684684

685685
app.exec()
686686
self.shutdown()

src/tagstudio/resources/translations/en.json

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"color.primary": "Primary Color",
2424
"color.secondary": "Secondary Color",
2525
"color.title.no_color": "No Color",
26+
"dependency.missing.title": "{dependency} Not Found",
2627
"drop_import.description": "The following files match file paths that already exist in the library",
2728
"drop_import.duplicates_choice.plural": "The following {count} files match file paths that already exist in the library.",
2829
"drop_import.duplicates_choice.singular": "The following file matches a file path that already exists in the library.",
@@ -62,6 +63,8 @@
6263
"entries.unlinked.scanning": "Scanning Library for Unlinked Entries...",
6364
"entries.unlinked.search_and_relink": "&Search && Relink",
6465
"entries.unlinked.title": "Fix Unlinked Entries",
66+
"ffmpeg.missing.description": "FFmpeg and/or FFprobe were not found. FFmpeg is required for multimedia playback and thumbnails.",
67+
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
6568
"field.copy": "Copy Field",
6669
"field.edit": "Edit Field",
6770
"field.paste": "Paste Field",

0 commit comments

Comments
 (0)