From f69eb691d77c62257e7631e59295f73714676c8c Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 25 Jul 2025 18:27:14 +0000 Subject: [PATCH] =?UTF-8?q?feat(binaries):=20=E2=9C=A8=20Add=20support=20f?= =?UTF-8?q?or=20`MKVToolNix`=20and=20`mkvpropedit`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduced `MKVToolNix` and `mkvpropedit` binaries to the project. * Updated the environment check to include required status for dependencies. * Enhanced the `Tracks` class to raise an error if `MKVToolNix` is not found. * Modified the `_apply_tags` function to utilize the `mkvpropedit` binary from the binaries module. --- unshackle/commands/env.py | 43 ++++++++++++++++++--------------- unshackle/core/binaries.py | 4 +++ unshackle/core/tracks/tracks.py | 6 ++++- unshackle/core/utils/tags.py | 7 +++--- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/unshackle/commands/env.py b/unshackle/commands/env.py index b9b5506..88e56a8 100644 --- a/unshackle/commands/env.py +++ b/unshackle/commands/env.py @@ -15,7 +15,6 @@ from unshackle.core.config import POSSIBLE_CONFIG_PATHS, config, config_path from unshackle.core.console import console from unshackle.core.constants import context_settings from unshackle.core.services import Services -from unshackle.core.utils.osenvironment import get_os_arch @click.group(short_help="Manage and configure the project environment.", context_settings=context_settings) @@ -28,31 +27,37 @@ def check() -> None: """Checks environment for the required dependencies.""" table = Table(title="Dependencies", expand=True) table.add_column("Name", no_wrap=True) + table.add_column("Required", justify="center") table.add_column("Installed", justify="center") table.add_column("Path", no_wrap=False, overflow="fold") - # builds shaka-packager based on os, arch - packager_dep = get_os_arch("packager") - - # Helper function to find binary with multiple possible names - def find_binary(*names): - for name in names: - if binaries.find(name): - return name - return names[0] # Return first name as fallback for display - + # Define all dependencies with their binary objects and required status dependencies = [ - {"name": "CCExtractor", "binary": "ccextractor"}, - {"name": "FFMpeg", "binary": "ffmpeg"}, - {"name": "MKVToolNix", "binary": "mkvmerge"}, - {"name": "Shaka-Packager", "binary": find_binary("shaka-packager", "packager", packager_dep)}, - {"name": "N_m3u8DL-RE", "binary": find_binary("N_m3u8DL-RE", "n-m3u8dl-re")}, - {"name": "Aria2(c)", "binary": "aria2c"}, + {"name": "FFMpeg", "binary": binaries.FFMPEG, "required": True}, + {"name": "FFProbe", "binary": binaries.FFProbe, "required": True}, + {"name": "Shaka-Packager", "binary": binaries.ShakaPackager, "required": True}, + {"name": "MKVToolNix", "binary": binaries.MKVToolNix, "required": True}, + {"name": "Mkvpropedit", "binary": binaries.Mkvpropedit, "required": True}, + {"name": "CCExtractor", "binary": binaries.CCExtractor, "required": False}, + {"name": "FFPlay", "binary": binaries.FFPlay, "required": False}, + {"name": "SubtitleEdit", "binary": binaries.SubtitleEdit, "required": False}, + {"name": "Aria2(c)", "binary": binaries.Aria2, "required": False}, + {"name": "HolaProxy", "binary": binaries.HolaProxy, "required": False}, + {"name": "MPV", "binary": binaries.MPV, "required": False}, + {"name": "Caddy", "binary": binaries.Caddy, "required": False}, + {"name": "N_m3u8DL-RE", "binary": binaries.N_m3u8DL_RE, "required": False}, ] for dep in dependencies: - path = binaries.find(dep["binary"]) + path = dep["binary"] + # Required column + if dep["required"]: + required = "[red]Yes[/red]" + else: + required = "No" + + # Installed column if path: installed = "[green]:heavy_check_mark:[/green]" path_output = str(path) @@ -61,7 +66,7 @@ def check() -> None: path_output = "Not Found" # Add to the table - table.add_row(dep["name"], installed, path_output) + table.add_row(dep["name"], required, installed, path_output) # Display the result console.print(Padding(table, (1, 5))) diff --git a/unshackle/core/binaries.py b/unshackle/core/binaries.py index aa46a53..bc93dbf 100644 --- a/unshackle/core/binaries.py +++ b/unshackle/core/binaries.py @@ -49,6 +49,8 @@ HolaProxy = find("hola-proxy") MPV = find("mpv") Caddy = find("caddy") N_m3u8DL_RE = find("N_m3u8DL-RE", "n-m3u8dl-re") +MKVToolNix = find("mkvmerge") +Mkvpropedit = find("mkvpropedit") __all__ = ( @@ -63,5 +65,7 @@ __all__ = ( "MPV", "Caddy", "N_m3u8DL_RE", + "MKVToolNix", + "Mkvpropedit", "find", ) diff --git a/unshackle/core/tracks/tracks.py b/unshackle/core/tracks/tracks.py index 92f93e6..14edf4d 100644 --- a/unshackle/core/tracks/tracks.py +++ b/unshackle/core/tracks/tracks.py @@ -11,6 +11,7 @@ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeRe from rich.table import Table from rich.tree import Tree +from unshackle.core import binaries from unshackle.core.config import config from unshackle.core.console import console from unshackle.core.constants import LANGUAGE_MAX_DISTANCE, AnyTrack, TrackT @@ -290,8 +291,11 @@ class Tracks: progress: Update a rich progress bar via `completed=...`. This must be the progress object's update() func, pre-set with task id via functools.partial. """ + if not binaries.MKVToolNix: + raise RuntimeError("MKVToolNix (mkvmerge) is required for muxing but was not found") + cl = [ - "mkvmerge", + str(binaries.MKVToolNix), "--no-date", # remove dates from the output for security ] diff --git a/unshackle/core/utils/tags.py b/unshackle/core/utils/tags.py index ba7648b..7359e6a 100644 --- a/unshackle/core/utils/tags.py +++ b/unshackle/core/utils/tags.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging import os import re -import shutil import subprocess import tempfile from difflib import SequenceMatcher @@ -12,6 +11,7 @@ from typing import Optional, Tuple import requests +from unshackle.core import binaries from unshackle.core.config import config from unshackle.core.titles.episode import Episode from unshackle.core.titles.movie import Movie @@ -175,8 +175,7 @@ def external_ids(tmdb_id: int, kind: str) -> dict: def _apply_tags(path: Path, tags: dict[str, str]) -> None: if not tags: return - mkvpropedit = shutil.which("mkvpropedit") - if not mkvpropedit: + if not binaries.Mkvpropedit: log.debug("mkvpropedit not found on PATH; skipping tags") return log.debug("Applying tags to %s: %s", path, tags) @@ -189,7 +188,7 @@ def _apply_tags(path: Path, tags: dict[str, str]) -> None: tmp_path = Path(f.name) try: subprocess.run( - [mkvpropedit, str(path), "--tags", f"global:{tmp_path}"], + [str(binaries.Mkvpropedit), str(path), "--tags", f"global:{tmp_path}"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,