mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-15 13:35:05 +00:00
fix(core): decode subprocess output as UTF-8 with replacement
Strict UTF-8 decode of external tool output (ffmpeg/shaka/mp4decrypt) crashed reader threads on non-English Windows (cp950/Big5).
This commit is contained in:
@@ -842,19 +842,27 @@ class dl:
|
||||
try:
|
||||
if name == "shaka_packager":
|
||||
r = subprocess.run(
|
||||
[str(binary), "--version"], capture_output=True, text=True, timeout=5
|
||||
[str(binary), "--version"],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
|
||||
)
|
||||
version = (r.stdout or r.stderr or "").strip()
|
||||
elif name in ("ffmpeg", "ffprobe"):
|
||||
r = subprocess.run([str(binary), "-version"], capture_output=True, text=True, timeout=5)
|
||||
r = subprocess.run(
|
||||
[str(binary), "-version"],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
|
||||
)
|
||||
version = (r.stdout or "").split("\n")[0].strip()
|
||||
elif name == "mkvmerge":
|
||||
r = subprocess.run(
|
||||
[str(binary), "--version"], capture_output=True, text=True, timeout=5
|
||||
[str(binary), "--version"],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
|
||||
)
|
||||
version = (r.stdout or "").strip()
|
||||
elif name == "mp4decrypt":
|
||||
r = subprocess.run([str(binary)], capture_output=True, text=True, timeout=5)
|
||||
r = subprocess.run(
|
||||
[str(binary)],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
|
||||
)
|
||||
output = (r.stdout or "") + (r.stderr or "")
|
||||
lines = [line.strip() for line in output.split("\n") if line.strip()]
|
||||
version = " | ".join(lines[:2]) if lines else None
|
||||
|
||||
@@ -282,6 +282,8 @@ def test(path: Path, map_: str) -> None:
|
||||
],
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
reached_output = False
|
||||
errors = 0
|
||||
|
||||
@@ -1504,7 +1504,9 @@ def _binary_version(path: Any) -> Optional[str]:
|
||||
|
||||
for flag in ("--version", "-version"):
|
||||
try:
|
||||
proc = subprocess.run([str(path), flag], capture_output=True, text=True, timeout=5)
|
||||
proc = subprocess.run(
|
||||
[str(path), flag], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError, ValueError):
|
||||
return None
|
||||
match = _VERSION_RE.search(proc.stdout or "") or _VERSION_RE.search(proc.stderr or "")
|
||||
|
||||
@@ -223,7 +223,10 @@ class ClearKeyCENC:
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8")
|
||||
subprocess.run(
|
||||
cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
||||
encoding="utf-8", errors="replace",
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr if e.stderr else f"mp4decrypt failed with exit code {e.returncode}"
|
||||
raise subprocess.CalledProcessError(e.returncode, cmd, output=e.stdout, stderr=error_msg)
|
||||
@@ -262,6 +265,8 @@ class ClearKeyCENC:
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
stream_skipped = False
|
||||
|
||||
@@ -247,6 +247,8 @@ class MonaLisa:
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
startupinfo=startupinfo,
|
||||
timeout=worker_timeout_s,
|
||||
)
|
||||
|
||||
@@ -438,7 +438,10 @@ class PlayReady:
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8")
|
||||
subprocess.run(
|
||||
cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
||||
encoding="utf-8", errors="replace",
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr if e.stderr else f"mp4decrypt failed with exit code {e.returncode}"
|
||||
raise subprocess.CalledProcessError(e.returncode, cmd, output=e.stdout, stderr=error_msg)
|
||||
|
||||
@@ -378,7 +378,10 @@ class Widevine:
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8")
|
||||
subprocess.run(
|
||||
cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
||||
encoding="utf-8", errors="replace",
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr if e.stderr else f"mp4decrypt failed with exit code {e.returncode}"
|
||||
raise subprocess.CalledProcessError(e.returncode, cmd, output=e.stdout, stderr=error_msg)
|
||||
@@ -426,6 +429,8 @@ class Widevine:
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
stream_skipped = False
|
||||
|
||||
@@ -544,6 +544,8 @@ class Gluetun(Proxy):
|
||||
["docker", "ps", "--format", "{{.Ports}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
@@ -892,6 +894,8 @@ class Gluetun(Proxy):
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
@@ -926,6 +930,8 @@ class Gluetun(Proxy):
|
||||
["docker", "inspect", container_name, "--format", "{{.NetworkSettings.Ports}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
@@ -1109,6 +1115,8 @@ class Gluetun(Proxy):
|
||||
["docker", "inspect", container_name, "--format", "{{.State.ExitCode}}:{{.State.Error}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
@@ -1310,6 +1318,8 @@ class Gluetun(Proxy):
|
||||
["docker", "ps", "-a", "--filter", f"name=^{container_name}$", "--format", "{{.Names}}:{{.Status}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
@@ -1328,6 +1338,8 @@ class Gluetun(Proxy):
|
||||
["docker", "rm", "-f", container_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=10,
|
||||
)
|
||||
return True
|
||||
@@ -1346,6 +1358,8 @@ class Gluetun(Proxy):
|
||||
["docker", "rm", "-f", container_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=10,
|
||||
)
|
||||
else:
|
||||
@@ -1354,6 +1368,8 @@ class Gluetun(Proxy):
|
||||
["docker", "stop", container_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=10,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
@@ -1363,6 +1379,8 @@ class Gluetun(Proxy):
|
||||
["docker", "rm", "-f", container_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=5,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
|
||||
@@ -35,7 +35,7 @@ class Hola(Proxy):
|
||||
|
||||
p = subprocess.check_output(
|
||||
[self.binary, "-country", query, "-list-proxies"], stderr=subprocess.STDOUT
|
||||
).decode()
|
||||
).decode("utf-8", errors="replace")
|
||||
|
||||
if "Transaction error: temporary ban detected." in p:
|
||||
raise ConnectionError("Hola banned your IP temporarily from it's services. Try change your IP.")
|
||||
@@ -55,6 +55,6 @@ class Hola(Proxy):
|
||||
|
||||
def get_countries(self) -> list[dict[str, str]]:
|
||||
"""Get a list of available Countries."""
|
||||
p = subprocess.check_output([self.binary, "-list-countries"]).decode("utf8")
|
||||
p = subprocess.check_output([self.binary, "-list-countries"]).decode("utf-8", errors="replace")
|
||||
|
||||
return [{code: name} for country in p.splitlines() for (code, name) in [country.split(" - ", maxsplit=1)]]
|
||||
|
||||
@@ -225,6 +225,8 @@ class Hybrid:
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
if result_duration.returncode != 0:
|
||||
@@ -257,6 +259,8 @@ class Hybrid:
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
if result_streams.returncode != 0:
|
||||
@@ -300,6 +304,8 @@ class Hybrid:
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
# cropdetect outputs crop=w:h:x:y
|
||||
@@ -603,6 +609,8 @@ class Hybrid:
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
max_mdl = 1000
|
||||
|
||||
@@ -697,7 +697,7 @@ class Tracks:
|
||||
errors = []
|
||||
warnings = []
|
||||
mux_start_time = time.monotonic()
|
||||
p = subprocess.Popen(full_command, text=True, stdout=subprocess.PIPE)
|
||||
p = subprocess.Popen(full_command, text=True, encoding="utf-8", errors="replace", stdout=subprocess.PIPE)
|
||||
for line in iter(p.stdout.readline, ""):
|
||||
if line.startswith("#GUI#error") or line.startswith("#GUI#warning"):
|
||||
errors.append(line)
|
||||
|
||||
@@ -456,6 +456,8 @@ class Video(Track):
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
vals = dict(line.split("=", 1) for line in probe.stdout.splitlines() if "=" in line)
|
||||
current = (vals.get("color_primaries"), vals.get("color_transfer"), vals.get("color_space"))
|
||||
|
||||
@@ -84,7 +84,9 @@ def info_summary(rpu: Path) -> str:
|
||||
"""Return the textual summary (`dovi_tool info -i ... -s`) for an RPU file."""
|
||||
tool = _require_dovi_tool()
|
||||
info_start = time.monotonic()
|
||||
p = subprocess.run([tool, "info", "-i", str(rpu), "-s"], capture_output=True, text=True)
|
||||
p = subprocess.run(
|
||||
[tool, "info", "-i", str(rpu), "-s"], capture_output=True, text=True, encoding="utf-8", errors="replace"
|
||||
)
|
||||
log_tool_run(
|
||||
"dovi_tool info",
|
||||
"dovi_tool",
|
||||
|
||||
@@ -66,7 +66,7 @@ def ffprobe(uri: Union[bytes, Path]) -> dict:
|
||||
context={"tool": "ffprobe", "duration_ms": round((time.monotonic() - start) * 1000, 1)},
|
||||
)
|
||||
return {}
|
||||
result = json.loads(ff.stdout.decode("utf8"))
|
||||
result = json.loads(ff.stdout.decode("utf-8", errors="replace"))
|
||||
if dl:
|
||||
dl.log(
|
||||
level="DEBUG",
|
||||
|
||||
@@ -41,6 +41,8 @@ def apply_tags(path: Path, tags: dict[str, str]) -> None:
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
log_tool_run(
|
||||
"mkvpropedit tags",
|
||||
|
||||
Reference in New Issue
Block a user