mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-30 06:40:50 +00:00
fix: Do not create command prompt window on subprocess (#436)
* fix: Do not create command prompt window on subcmd Patches files from abandoned libraries are located and updated in src/qt/helpers/vendored with modified sections labeld PATCHED. A wrapper around subprocess.Popen automatically sets the creation flag to no window on windows. * fix: Replace Popen in mediainfo_json decoder * fixup: Pipe stdin to stdin * chore: Exclude vendored dir from tooling checks * suppress mypy warnings
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
[tool.ruff]
|
||||
exclude = ["main_window.py", "home_ui.py", "resources.py", "resources_rc.py"]
|
||||
exclude = ["main_window.py", "home_ui.py", "resources.py", "resources_rc.py", "**/vendored/"]
|
||||
|
||||
[tool.mypy]
|
||||
strict_optional = false
|
||||
disable_error_code = ["union-attr", "annotation-unchecked", "import-untyped"]
|
||||
explicit_package_bases = true
|
||||
warn_unused_ignores = true
|
||||
exclude = ['tests']
|
||||
exclude = ['tests', 'src/qt/helpers/vendored']
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import ffmpeg
|
||||
from pathlib import Path
|
||||
|
||||
from src.qt.helpers.vendored.ffmpeg import _probe
|
||||
|
||||
|
||||
def is_readable_video(filepath: Path | str):
|
||||
"""Test if a video is in a readable format. Examples of unreadable videos
|
||||
@@ -15,7 +17,7 @@ def is_readable_video(filepath: Path | str):
|
||||
filepath (Path | str):
|
||||
"""
|
||||
try:
|
||||
probe = ffmpeg.probe(Path(filepath))
|
||||
probe = _probe(Path(filepath))
|
||||
for stream in probe["streams"]:
|
||||
# DRM check
|
||||
if stream.get("codec_tag_string") in [
|
||||
|
||||
64
tagstudio/src/qt/helpers/silent_popen.py
Normal file
64
tagstudio/src/qt/helpers/silent_popen.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def promptless_Popen(
|
||||
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,
|
||||
restore_signals=True,
|
||||
start_new_session=False,
|
||||
pass_fds=(),
|
||||
*,
|
||||
group=None,
|
||||
extra_groups=None,
|
||||
user=None,
|
||||
umask=-1,
|
||||
encoding=None,
|
||||
errors=None,
|
||||
text=None,
|
||||
pipesize=-1,
|
||||
process_group=None,
|
||||
):
|
||||
creation_flags = 0
|
||||
if sys.platform == "win32":
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW
|
||||
|
||||
return subprocess.Popen(
|
||||
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,
|
||||
universal_newlines=universal_newlines,
|
||||
startupinfo=startupinfo,
|
||||
creationflags=creation_flags,
|
||||
restore_signals=restore_signals,
|
||||
start_new_session=start_new_session,
|
||||
pass_fds=pass_fds,
|
||||
group=group,
|
||||
extra_groups=extra_groups,
|
||||
user=user,
|
||||
umask=umask,
|
||||
encoding=encoding,
|
||||
errors=errors,
|
||||
text=text,
|
||||
pipesize=pipesize,
|
||||
process_group=process_group,
|
||||
)
|
||||
34
tagstudio/src/qt/helpers/vendored/ffmpeg.py
Normal file
34
tagstudio/src/qt/helpers/vendored/ffmpeg.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Copyright (C) 2022 Karl Kroening (kkroening).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Vendored from ffmpeg-python and ffmpeg-python PR#790 by amamic1803
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
|
||||
import ffmpeg
|
||||
|
||||
from src.qt.helpers.silent_popen import promptless_Popen
|
||||
|
||||
def _probe(filename, cmd='ffprobe', timeout=None, **kwargs):
|
||||
"""Run ffprobe on the specified file and return a JSON representation of the output.
|
||||
|
||||
Raises:
|
||||
:class:`ffmpeg.Error`: if ffprobe returns a non-zero exit code,
|
||||
an :class:`Error` is returned with a generic error message.
|
||||
The stderr output can be retrieved by accessing the
|
||||
``stderr`` property of the exception.
|
||||
"""
|
||||
args = [cmd, '-show_format', '-show_streams', '-of', 'json']
|
||||
args += ffmpeg._utils.convert_kwargs_to_cmd_line_args(kwargs)
|
||||
args += [filename]
|
||||
|
||||
# PATCHED
|
||||
p = promptless_Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
communicate_kwargs = {}
|
||||
if timeout is not None:
|
||||
communicate_kwargs['timeout'] = timeout
|
||||
out, err = p.communicate(**communicate_kwargs)
|
||||
if p.returncode != 0:
|
||||
raise ffmpeg.Error('ffprobe', out, err)
|
||||
return json.loads(out.decode('utf-8'))
|
||||
1403
tagstudio/src/qt/helpers/vendored/pydub/audio_segment.py
Normal file
1403
tagstudio/src/qt/helpers/vendored/pydub/audio_segment.py
Normal file
File diff suppressed because it is too large
Load Diff
88
tagstudio/src/qt/helpers/vendored/pydub/utils.py
Normal file
88
tagstudio/src/qt/helpers/vendored/pydub/utils.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from pydub.utils import (
|
||||
get_prober_name,
|
||||
fsdecode,
|
||||
_fd_or_path_or_tempfile,
|
||||
get_extra_info,
|
||||
)
|
||||
|
||||
from src.qt.helpers.silent_popen import promptless_Popen
|
||||
|
||||
def _mediainfo_json(filepath, read_ahead_limit=-1):
|
||||
"""Return json dictionary with media info(codec, duration, size, bitrate...) from filepath
|
||||
"""
|
||||
prober = get_prober_name()
|
||||
command_args = [
|
||||
"-v", "info",
|
||||
"-show_format",
|
||||
"-show_streams",
|
||||
]
|
||||
try:
|
||||
command_args += [fsdecode(filepath)]
|
||||
stdin_parameter = None
|
||||
stdin_data = None
|
||||
except TypeError:
|
||||
if prober == 'ffprobe':
|
||||
command_args += ["-read_ahead_limit", str(read_ahead_limit),
|
||||
"cache:pipe:0"]
|
||||
else:
|
||||
command_args += ["-"]
|
||||
stdin_parameter = subprocess.PIPE
|
||||
file, close_file = _fd_or_path_or_tempfile(filepath, 'rb', tempfile=False)
|
||||
file.seek(0)
|
||||
stdin_data = file.read()
|
||||
if close_file:
|
||||
file.close()
|
||||
|
||||
command = [prober, '-of', 'json'] + command_args
|
||||
# PATCHED
|
||||
res = promptless_Popen(command, stdin=stdin_parameter, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
output, stderr = res.communicate(input=stdin_data)
|
||||
output = output.decode("utf-8", 'ignore')
|
||||
stderr = stderr.decode("utf-8", 'ignore')
|
||||
|
||||
try:
|
||||
info = json.loads(output)
|
||||
except json.decoder.JSONDecodeError:
|
||||
# If ffprobe didn't give any information, just return it
|
||||
# (for example, because the file doesn't exist)
|
||||
return None
|
||||
if not info:
|
||||
return info
|
||||
|
||||
extra_info = get_extra_info(stderr)
|
||||
|
||||
audio_streams = [x for x in info['streams'] if x['codec_type'] == 'audio']
|
||||
if len(audio_streams) == 0:
|
||||
return info
|
||||
|
||||
# We just operate on the first audio stream in case there are more
|
||||
stream = audio_streams[0]
|
||||
|
||||
def set_property(stream, prop, value):
|
||||
if prop not in stream or stream[prop] == 0:
|
||||
stream[prop] = value
|
||||
|
||||
for token in extra_info[stream['index']]:
|
||||
m = re.match(r'([su]([0-9]{1,2})p?) \(([0-9]{1,2}) bit\)$', token)
|
||||
m2 = re.match(r'([su]([0-9]{1,2})p?)( \(default\))?$', token)
|
||||
if m:
|
||||
set_property(stream, 'sample_fmt', m.group(1))
|
||||
set_property(stream, 'bits_per_sample', int(m.group(2)))
|
||||
set_property(stream, 'bits_per_raw_sample', int(m.group(3)))
|
||||
elif m2:
|
||||
set_property(stream, 'sample_fmt', m2.group(1))
|
||||
set_property(stream, 'bits_per_sample', int(m2.group(2)))
|
||||
set_property(stream, 'bits_per_raw_sample', int(m2.group(2)))
|
||||
elif re.match(r'(flt)p?( \(default\))?$', token):
|
||||
set_property(stream, 'sample_fmt', token)
|
||||
set_property(stream, 'bits_per_sample', 32)
|
||||
set_property(stream, 'bits_per_raw_sample', 32)
|
||||
elif re.match(r'(dbl)p?( \(default\))?$', token):
|
||||
set_property(stream, 'sample_fmt', token)
|
||||
set_property(stream, 'bits_per_sample', 64)
|
||||
set_property(stream, 'bits_per_raw_sample', 64)
|
||||
return info
|
||||
@@ -27,7 +27,8 @@ from PIL import (
|
||||
)
|
||||
from PIL.Image import DecompressionBombError
|
||||
from pillow_heif import register_avif_opener, register_heif_opener
|
||||
from pydub import AudioSegment, exceptions
|
||||
from pydub import exceptions
|
||||
from src.qt.helpers.vendored.pydub.audio_segment import _AudioSegment as AudioSegment # type: ignore
|
||||
from PySide6.QtCore import QObject, QSize, Qt, Signal
|
||||
from PySide6.QtGui import QGuiApplication, QPixmap
|
||||
from src.core.constants import FONT_SAMPLE_SIZES, FONT_SAMPLE_TEXT
|
||||
|
||||
Reference in New Issue
Block a user