mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-31 15:19:10 +00:00
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 <46534683+Computerdores@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0701a45e75
commit
880ca07a6f
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
|
||||
import ffmpeg
|
||||
|
||||
from tagstudio.qt.helpers.vendored.ffmpeg import _probe
|
||||
from tagstudio.qt.helpers.vendored.ffmpeg import probe
|
||||
|
||||
|
||||
def is_readable_video(filepath: Path | str):
|
||||
@@ -19,8 +19,8 @@ def is_readable_video(filepath: Path | str):
|
||||
filepath (Path | str): The filepath of the video to check.
|
||||
"""
|
||||
try:
|
||||
probe = _probe(Path(filepath))
|
||||
for stream in probe["streams"]:
|
||||
result = probe(Path(filepath))
|
||||
for stream in result["streams"]:
|
||||
# DRM check
|
||||
if stream.get("codec_tag_string") in [
|
||||
"drma",
|
||||
|
||||
@@ -84,3 +84,79 @@ def silent_Popen( # noqa: N802
|
||||
pipesize=pipesize,
|
||||
process_group=process_group,
|
||||
)
|
||||
|
||||
|
||||
def silent_run( # noqa: N802
|
||||
args,
|
||||
bufsize=-1,
|
||||
executable=None,
|
||||
stdin=None,
|
||||
stdout=None,
|
||||
stderr=None,
|
||||
preexec_fn=None,
|
||||
close_fds=True,
|
||||
shell=False,
|
||||
cwd=None,
|
||||
env=None,
|
||||
universal_newlines=None,
|
||||
startupinfo=None,
|
||||
creationflags=0,
|
||||
restore_signals=True,
|
||||
start_new_session=False,
|
||||
pass_fds=(),
|
||||
*,
|
||||
capture_output=False,
|
||||
group=None,
|
||||
extra_groups=None,
|
||||
user=None,
|
||||
umask=-1,
|
||||
encoding=None,
|
||||
errors=None,
|
||||
text=None,
|
||||
pipesize=-1,
|
||||
process_group=None,
|
||||
):
|
||||
"""Call subprocess.run without creating a console window."""
|
||||
if sys.platform == "win32":
|
||||
creationflags |= subprocess.CREATE_NO_WINDOW
|
||||
import ctypes
|
||||
|
||||
ctypes.windll.kernel32.SetDllDirectoryW(None)
|
||||
elif (
|
||||
sys.platform == "linux"
|
||||
or sys.platform.startswith("freebsd")
|
||||
or sys.platform.startswith("openbsd")
|
||||
):
|
||||
# pass clean environment to the subprocess
|
||||
env = os.environ
|
||||
original_env = env.get("LD_LIBRARY_PATH_ORIG")
|
||||
env["LD_LIBRARY_PATH"] = original_env if original_env else ""
|
||||
|
||||
return subprocess.run(
|
||||
args=args,
|
||||
bufsize=bufsize,
|
||||
executable=executable,
|
||||
stdin=stdin,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
preexec_fn=preexec_fn,
|
||||
close_fds=close_fds,
|
||||
shell=shell,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
startupinfo=startupinfo,
|
||||
creationflags=creationflags,
|
||||
restore_signals=restore_signals,
|
||||
start_new_session=start_new_session,
|
||||
pass_fds=pass_fds,
|
||||
capture_output=capture_output,
|
||||
group=group,
|
||||
extra_groups=extra_groups,
|
||||
user=user,
|
||||
umask=umask,
|
||||
encoding=encoding,
|
||||
errors=errors,
|
||||
text=text,
|
||||
pipesize=pipesize,
|
||||
process_group=process_group,
|
||||
)
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Vendored from ffmpeg-python and ffmpeg-python PR#790 by amamic1803
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from shutil import which
|
||||
|
||||
import ffmpeg
|
||||
import structlog
|
||||
|
||||
from tagstudio.qt.helpers.silent_popen import silent_Popen
|
||||
from tagstudio.qt.helpers.silent_popen import silent_Popen, silent_run
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -21,10 +22,12 @@ def _get_ffprobe_location() -> str:
|
||||
cmd: str = "ffprobe"
|
||||
if platform.system() == "Darwin":
|
||||
for loc in FFMPEG_MACOS_LOCATIONS:
|
||||
if shutil.which(loc + cmd):
|
||||
if which(loc + cmd):
|
||||
cmd = loc + cmd
|
||||
break
|
||||
logger.info(f"[FFMPEG] Using FFprobe location: {cmd}")
|
||||
logger.info(
|
||||
f"[FFmpeg] Using FFprobe location: {cmd}{' (Found)' if which(cmd) else ' (Not Found)'}"
|
||||
)
|
||||
return cmd
|
||||
|
||||
|
||||
@@ -32,10 +35,12 @@ def _get_ffmpeg_location() -> str:
|
||||
cmd: str = "ffmpeg"
|
||||
if platform.system() == "Darwin":
|
||||
for loc in FFMPEG_MACOS_LOCATIONS:
|
||||
if shutil.which(loc + cmd):
|
||||
if which(loc + cmd):
|
||||
cmd = loc + cmd
|
||||
break
|
||||
logger.info(f"[FFMPEG] Using FFmpeg location: {cmd}")
|
||||
logger.info(
|
||||
f"[FFmpeg] Using FFmpeg location: {cmd}{' (Found)' if which(cmd) else ' (Not Found)'}"
|
||||
)
|
||||
return cmd
|
||||
|
||||
|
||||
@@ -43,7 +48,7 @@ FFPROBE_CMD = _get_ffprobe_location()
|
||||
FFMPEG_CMD = _get_ffmpeg_location()
|
||||
|
||||
|
||||
def _probe(filename, cmd=FFPROBE_CMD, timeout=None, **kwargs):
|
||||
def probe(filename, cmd=FFPROBE_CMD, timeout=None, **kwargs):
|
||||
"""Run ffprobe on the specified file and return a JSON representation of the output.
|
||||
|
||||
Raises:
|
||||
@@ -65,3 +70,22 @@ def _probe(filename, cmd=FFPROBE_CMD, timeout=None, **kwargs):
|
||||
if p.returncode != 0:
|
||||
raise ffmpeg.Error("ffprobe", out, err)
|
||||
return json.loads(out.decode("utf-8"))
|
||||
|
||||
|
||||
def version():
|
||||
"""Checks the version of FFmpeg and FFprobe and returns None if they dont exist."""
|
||||
version: dict[str, str | None] = {"ffmpeg": None, "ffprobe": None}
|
||||
|
||||
if which(FFMPEG_CMD):
|
||||
ret = silent_run([FFMPEG_CMD, "-version"], shell=False, capture_output=True, text=True)
|
||||
if ret.returncode == 0:
|
||||
with contextlib.suppress(Exception):
|
||||
version["ffmpeg"] = str(ret.stdout).split(" ")[2]
|
||||
|
||||
if which(FFPROBE_CMD):
|
||||
ret = silent_run([FFPROBE_CMD, "-version"], shell=False, capture_output=True, text=True)
|
||||
if ret.returncode == 0:
|
||||
with contextlib.suppress(Exception):
|
||||
version["ffprobe"] = str(ret.stdout).split(" ")[2]
|
||||
|
||||
return version
|
||||
|
||||
@@ -21,7 +21,7 @@ from PySide6.QtWidgets import (
|
||||
from tagstudio.core.constants import VERSION, VERSION_BRANCH
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.modals.ffmpeg_checker import FfmpegChecker
|
||||
from tagstudio.qt.helpers.vendored import ffmpeg
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
@@ -31,7 +31,6 @@ class AboutModal(QWidget):
|
||||
super().__init__()
|
||||
self.setWindowTitle(Translations["about.title"])
|
||||
|
||||
self.fc: FfmpegChecker = FfmpegChecker()
|
||||
self.rm: ResourceManager = ResourceManager()
|
||||
|
||||
# TODO: There should be a global button theme somewhere.
|
||||
@@ -82,7 +81,7 @@ class AboutModal(QWidget):
|
||||
self.desc_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
# System Info ----------------------------------------------------------
|
||||
ff_version = self.fc.version()
|
||||
ff_version = ffmpeg.version()
|
||||
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
|
||||
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
|
||||
missing = Translations["generic.missing"]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import contextlib
|
||||
import subprocess
|
||||
from shutil import which
|
||||
|
||||
import structlog
|
||||
@@ -7,7 +5,9 @@ from PySide6.QtCore import Qt, QUrl
|
||||
from PySide6.QtGui import QDesktopServices
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.helpers.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -20,10 +20,11 @@ class FfmpegChecker(QMessageBox):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle("Warning: Missing dependency")
|
||||
self.setText("Warning: Could not find FFmpeg installation")
|
||||
ffmpeg = "FFmpeg"
|
||||
ffprobe = "FFprobe"
|
||||
title = Translations.format("dependency.missing.title", dependency=ffmpeg)
|
||||
self.setWindowTitle(title)
|
||||
self.setIcon(QMessageBox.Icon.Warning)
|
||||
# Blocks other application interactions until resolved
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
|
||||
self.setStandardButtons(
|
||||
@@ -34,52 +35,19 @@ class FfmpegChecker(QMessageBox):
|
||||
self.setDefaultButton(QMessageBox.StandardButton.Ignore)
|
||||
# Enables the cancel button but hides it to allow for click X to close dialog
|
||||
self.button(QMessageBox.StandardButton.Cancel).hide()
|
||||
self.button(QMessageBox.StandardButton.Help).clicked.connect(
|
||||
lambda: QDesktopServices.openUrl(QUrl(self.HELP_URL))
|
||||
)
|
||||
|
||||
self.ffmpeg = False
|
||||
self.ffprobe = False
|
||||
|
||||
def installed(self):
|
||||
"""Checks if both FFmpeg and FFprobe are installed and in the PATH."""
|
||||
if which(FFMPEG_CMD):
|
||||
self.ffmpeg = True
|
||||
if which(FFPROBE_CMD):
|
||||
self.ffprobe = True
|
||||
|
||||
logger.info("FFmpeg found: {self.ffmpeg}, FFprobe found: {self.ffprobe}")
|
||||
return self.ffmpeg and self.ffprobe
|
||||
|
||||
def version(self):
|
||||
"""Checks the version of ffprobe and ffmpeg and returns None if they dont exist."""
|
||||
version: dict[str, str | None] = {"ffprobe": None, "ffmpeg": None}
|
||||
self.installed()
|
||||
if self.ffprobe:
|
||||
ret = subprocess.run(
|
||||
[FFPROBE_CMD, "-show_program_version"], shell=False, capture_output=True, text=True
|
||||
)
|
||||
if ret.returncode == 0:
|
||||
with contextlib.suppress(Exception):
|
||||
version["ffprobe"] = ret.stdout.split("\n")[1].replace("-", "=").split("=")[1]
|
||||
if self.ffmpeg:
|
||||
ret = subprocess.run(
|
||||
[FFMPEG_CMD, "-version"], shell=False, capture_output=True, text=True
|
||||
)
|
||||
if ret.returncode == 0:
|
||||
with contextlib.suppress(Exception):
|
||||
version["ffmpeg"] = ret.stdout.replace("-", " ").split(" ")[2]
|
||||
return version
|
||||
|
||||
def show_warning(self):
|
||||
"""Displays the warning to the user and awaits response."""
|
||||
missing = "FFmpeg"
|
||||
# If ffmpeg is installed but not ffprobe
|
||||
if not self.ffprobe and self.ffmpeg:
|
||||
missing = "FFprobe"
|
||||
|
||||
self.setText(f"Warning: Could not find {missing} installation")
|
||||
self.setInformativeText(f"{missing} is required for multimedia thumbnails and playback")
|
||||
# Shows the dialog
|
||||
selection = self.exec()
|
||||
|
||||
# Selection will either be QMessageBox.Help or (QMessageBox.Ignore | QMessageBox.Cancel)
|
||||
if selection == QMessageBox.StandardButton.Help:
|
||||
QDesktopServices.openUrl(QUrl(self.HELP_URL))
|
||||
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
|
||||
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
|
||||
missing = f"<span style='color:{red}'>{Translations["generic.missing"]}</span>"
|
||||
found = f"<span style='color:{green}'>{Translations['about.module.found']}</span>"
|
||||
status = Translations.format(
|
||||
"ffmpeg.missing.status",
|
||||
ffmpeg=ffmpeg,
|
||||
ffmpeg_status=found if which(FFMPEG_CMD) else missing,
|
||||
ffprobe=ffprobe,
|
||||
ffprobe_status=found if which(FFPROBE_CMD) else missing,
|
||||
)
|
||||
self.setText(f"{Translations["ffmpeg.missing.description"]}<br><br>{status}")
|
||||
|
||||
@@ -19,6 +19,7 @@ import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from shutil import which
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
@@ -76,6 +77,7 @@ from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.helpers.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.helpers.file_deleter import delete_file
|
||||
from tagstudio.qt.helpers.function_iterator import FunctionIterator
|
||||
from tagstudio.qt.helpers.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
|
||||
from tagstudio.qt.main_window import Ui_MainWindow
|
||||
from tagstudio.qt.modals.about import AboutModal
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
@@ -676,11 +678,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
if path_result.success and path_result.library_path:
|
||||
self.open_library(path_result.library_path)
|
||||
|
||||
# check ffmpeg and show warning if not
|
||||
# NOTE: Does this need to use self?
|
||||
self.ffmpeg_checker = FfmpegChecker()
|
||||
if not self.ffmpeg_checker.installed():
|
||||
self.ffmpeg_checker.show_warning()
|
||||
# Check if FFmpeg or FFprobe are missing and show warning if so
|
||||
if not which(FFMPEG_CMD) or not which(FFPROBE_CMD):
|
||||
FfmpegChecker().show()
|
||||
|
||||
app.exec()
|
||||
self.shutdown()
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"color.primary": "Primary Color",
|
||||
"color.secondary": "Secondary Color",
|
||||
"color.title.no_color": "No Color",
|
||||
"dependency.missing.title": "{dependency} Not Found",
|
||||
"drop_import.description": "The following files match file paths that already exist in the library",
|
||||
"drop_import.duplicates_choice.plural": "The following {count} files match file paths that already exist in the library.",
|
||||
"drop_import.duplicates_choice.singular": "The following file matches a file path that already exists in the library.",
|
||||
@@ -62,6 +63,8 @@
|
||||
"entries.unlinked.scanning": "Scanning Library for Unlinked Entries...",
|
||||
"entries.unlinked.search_and_relink": "&Search && Relink",
|
||||
"entries.unlinked.title": "Fix Unlinked Entries",
|
||||
"ffmpeg.missing.description": "FFmpeg and/or FFprobe were not found. FFmpeg is required for multimedia playback and thumbnails.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.copy": "Copy Field",
|
||||
"field.edit": "Edit Field",
|
||||
"field.paste": "Paste Field",
|
||||
|
||||
Reference in New Issue
Block a user