mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
feat(binary): add custom binary registration for services
- Allow services to declare required binaries via `@staticmethod def get_binaries()` - Support accessing registered custom binaries directly via `binaries.<Name>` - Automatically search service local `binaries/` directories
This commit is contained in:
+77
-10
@@ -19,10 +19,28 @@ from unshackle.core.temp import TASK_PREFIX, is_stale
|
||||
|
||||
def get_dependencies() -> list[dict]:
|
||||
"""Binary dependency inventory shared by `env check` and the API env check."""
|
||||
return [
|
||||
{"name": "FFmpeg", "binary": binaries.FFMPEG, "required": True, "desc": "Media processing", "cat": "Core"},
|
||||
{"name": "FFprobe", "binary": binaries.FFProbe, "required": True, "desc": "Media analysis", "cat": "Core"},
|
||||
{"name": "MKVToolNix", "binary": binaries.MKVToolNix, "required": True, "desc": "MKV muxing", "cat": "Core"},
|
||||
deps = [
|
||||
{
|
||||
"name": "FFmpeg",
|
||||
"binary": binaries.FFMPEG,
|
||||
"required": True,
|
||||
"desc": "Media processing",
|
||||
"cat": "Core",
|
||||
},
|
||||
{
|
||||
"name": "FFprobe",
|
||||
"binary": binaries.FFProbe,
|
||||
"required": True,
|
||||
"desc": "Media analysis",
|
||||
"cat": "Core",
|
||||
},
|
||||
{
|
||||
"name": "MKVToolNix",
|
||||
"binary": binaries.MKVToolNix,
|
||||
"required": True,
|
||||
"desc": "MKV muxing",
|
||||
"cat": "Core",
|
||||
},
|
||||
{
|
||||
"name": "mkvpropedit",
|
||||
"binary": binaries.Mkvpropedit,
|
||||
@@ -44,7 +62,13 @@ def get_dependencies() -> list[dict]:
|
||||
"desc": "DRM decryption",
|
||||
"cat": "DRM",
|
||||
},
|
||||
{"name": "dovi_tool", "binary": binaries.DoviTool, "required": False, "desc": "Dolby Vision", "cat": "HDR"},
|
||||
{
|
||||
"name": "dovi_tool",
|
||||
"binary": binaries.DoviTool,
|
||||
"required": False,
|
||||
"desc": "Dolby Vision",
|
||||
"cat": "HDR",
|
||||
},
|
||||
{
|
||||
"name": "HDR10Plus_tool",
|
||||
"binary": binaries.HDR10PlusTool,
|
||||
@@ -66,8 +90,20 @@ def get_dependencies() -> list[dict]:
|
||||
"desc": "CC extraction",
|
||||
"cat": "Subtitle",
|
||||
},
|
||||
{"name": "FFplay", "binary": binaries.FFPlay, "required": False, "desc": "Simple player", "cat": "Player"},
|
||||
{"name": "MPV", "binary": binaries.MPV, "required": False, "desc": "Advanced player", "cat": "Player"},
|
||||
{
|
||||
"name": "FFplay",
|
||||
"binary": binaries.FFPlay,
|
||||
"required": False,
|
||||
"desc": "Simple player",
|
||||
"cat": "Player",
|
||||
},
|
||||
{
|
||||
"name": "MPV",
|
||||
"binary": binaries.MPV,
|
||||
"required": False,
|
||||
"desc": "Advanced player",
|
||||
"cat": "Player",
|
||||
},
|
||||
{
|
||||
"name": "HolaProxy",
|
||||
"binary": binaries.HolaProxy,
|
||||
@@ -75,11 +111,42 @@ def get_dependencies() -> list[dict]:
|
||||
"desc": "Proxy service",
|
||||
"cat": "Network",
|
||||
},
|
||||
{"name": "Caddy", "binary": binaries.Caddy, "required": False, "desc": "Web server", "cat": "Network"},
|
||||
{"name": "Docker", "binary": binaries.Docker, "required": False, "desc": "Gluetun VPN", "cat": "Network"},
|
||||
{"name": "git", "binary": binaries.Git, "required": False, "desc": "Service repos", "cat": "Network"},
|
||||
{
|
||||
"name": "Caddy",
|
||||
"binary": binaries.Caddy,
|
||||
"required": False,
|
||||
"desc": "Web server",
|
||||
"cat": "Network",
|
||||
},
|
||||
{
|
||||
"name": "Docker",
|
||||
"binary": binaries.Docker,
|
||||
"required": False,
|
||||
"desc": "Gluetun VPN",
|
||||
"cat": "Network",
|
||||
},
|
||||
{
|
||||
"name": "git",
|
||||
"binary": binaries.Git,
|
||||
"required": False,
|
||||
"desc": "Service repos",
|
||||
"cat": "Network",
|
||||
},
|
||||
]
|
||||
|
||||
for reg in binaries.get_registered_dependencies():
|
||||
deps.append(
|
||||
{
|
||||
"name": reg["name"],
|
||||
"binary": getattr(binaries, reg["attr"], None),
|
||||
"required": False,
|
||||
"desc": reg["desc"],
|
||||
"cat": "Service",
|
||||
}
|
||||
)
|
||||
|
||||
return deps
|
||||
|
||||
|
||||
def clear_directory(path: Path) -> tuple[int, int]:
|
||||
"""Delete a directory's contents, returning (files_removed, freed_bytes). Recreates the dir.
|
||||
|
||||
@@ -6,18 +6,31 @@ from typing import Optional
|
||||
__shaka_platform = {"win32": "win", "darwin": "osx"}.get(sys.platform, sys.platform)
|
||||
|
||||
|
||||
def find(*names: str) -> Optional[Path]:
|
||||
def find(*names: str, search_dirs: Optional[list[Path]] = None) -> Optional[Path]:
|
||||
"""Find the path of the first found binary name."""
|
||||
current_dir = Path(__file__).resolve().parent.parent
|
||||
local_binaries_dir = current_dir / "binaries"
|
||||
services_dir = current_dir / "services"
|
||||
|
||||
dirs_to_check: list[Path] = [local_binaries_dir]
|
||||
if services_dir.exists():
|
||||
for s_dir in services_dir.iterdir():
|
||||
s_bin = s_dir / "binaries"
|
||||
if s_bin.is_dir():
|
||||
dirs_to_check.append(s_bin)
|
||||
|
||||
if search_dirs:
|
||||
dirs_to_check.extend(search_dirs)
|
||||
|
||||
ext = ".exe" if sys.platform == "win32" else ""
|
||||
|
||||
for name in names:
|
||||
if local_binaries_dir.exists():
|
||||
candidate_paths = [local_binaries_dir / f"{name}{ext}", local_binaries_dir / name / f"{name}{ext}"]
|
||||
for base_dir in dirs_to_check:
|
||||
if not base_dir.exists():
|
||||
continue
|
||||
candidate_paths = [base_dir / f"{name}{ext}", base_dir / name / f"{name}{ext}"]
|
||||
|
||||
for subdir in local_binaries_dir.iterdir():
|
||||
for subdir in base_dir.iterdir():
|
||||
if subdir.is_dir():
|
||||
candidate_paths.append(subdir / f"{name}{ext}")
|
||||
|
||||
@@ -63,11 +76,50 @@ __binaries = {
|
||||
"Git": ("git",),
|
||||
}
|
||||
|
||||
_registered_binaries: dict[str, dict] = {}
|
||||
|
||||
|
||||
def register(
|
||||
attr_name: str,
|
||||
*candidates: str,
|
||||
desc: str = "Custom tool",
|
||||
) -> None:
|
||||
"""Register a custom binary dynamically."""
|
||||
file_candidates = candidates if candidates else (attr_name.lower(),)
|
||||
__binaries[attr_name] = file_candidates
|
||||
_registered_binaries[attr_name] = {
|
||||
"name": attr_name,
|
||||
"attr": attr_name,
|
||||
"desc": desc,
|
||||
}
|
||||
|
||||
|
||||
def get_registered_dependencies() -> list[dict]:
|
||||
"""Return all registered custom binary dependencies for env check."""
|
||||
return list(_registered_binaries.values())
|
||||
|
||||
|
||||
_services_loaded = False
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Optional[Path]:
|
||||
global _services_loaded
|
||||
candidates = __binaries.get(name)
|
||||
if candidates is None and not _services_loaded:
|
||||
_services_loaded = True
|
||||
try:
|
||||
candidates = __binaries.get(name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if candidates is None:
|
||||
# Fallback to direct name and lowercase candidate
|
||||
resolved = find(name, name.lower())
|
||||
if resolved is not None:
|
||||
globals()[name] = resolved
|
||||
return resolved
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
# Cache the result (including None for an absent binary) so later access is a plain attr hit.
|
||||
resolved = find(*candidates)
|
||||
globals()[name] = resolved
|
||||
@@ -93,4 +145,6 @@ __all__ = (
|
||||
"ML_Worker",
|
||||
"Git",
|
||||
"find",
|
||||
"register",
|
||||
"get_registered_dependencies",
|
||||
)
|
||||
|
||||
@@ -405,6 +405,14 @@ class Service(metaclass=ABCMeta):
|
||||
session.mount("http://", session.adapters["https://"])
|
||||
return session
|
||||
|
||||
@staticmethod
|
||||
def get_binaries() -> list[dict]:
|
||||
"""
|
||||
Declare custom binary dependencies required by this service.
|
||||
:returns: List of dicts specifying name, candidates, desc, etc.
|
||||
"""
|
||||
return []
|
||||
|
||||
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
|
||||
"""
|
||||
Authenticate the Service with Cookies and/or Credentials (Email/Username and Password).
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Any, Iterable
|
||||
|
||||
import click
|
||||
|
||||
from unshackle.core import binaries
|
||||
from unshackle.core.config import config
|
||||
from unshackle.core.service import Service
|
||||
from unshackle.core.service_repo import DirtyServiceRepo, is_repo_spec, refresh_repo, resolve_service_repo
|
||||
@@ -102,7 +103,26 @@ def load_services(paths: list[Path]) -> tuple[dict[str, object], list[str]]:
|
||||
return modules, errors
|
||||
|
||||
|
||||
def register_service_binaries(modules: dict[str, object]) -> None:
|
||||
"""Register custom binary dependencies declared by services via get_binaries()."""
|
||||
|
||||
for tag, service_cls in modules.items():
|
||||
get_binaries_fn = getattr(service_cls, "get_binaries", None)
|
||||
if callable(get_binaries_fn):
|
||||
try:
|
||||
for dep in get_binaries_fn():
|
||||
name = dep.get("name")
|
||||
if not name:
|
||||
continue
|
||||
candidates = dep.get("candidates") or (name.lower(),)
|
||||
desc = dep.get("desc", f"{tag} dependency")
|
||||
binaries.register(name, *candidates, desc=desc)
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to register custom binaries for service {tag}: {e}")
|
||||
|
||||
|
||||
MODULES, LOAD_ERRORS = load_services(SERVICES)
|
||||
register_service_binaries(MODULES)
|
||||
|
||||
ALIASES = {tag: getattr(module, "ALIASES", ()) for tag, module in MODULES.items()}
|
||||
|
||||
@@ -143,6 +163,7 @@ def reload_services(tags: Iterable[str]) -> list[str]:
|
||||
except Exception as e:
|
||||
errors.append(str(e))
|
||||
LOAD_ERRORS.extend(errors)
|
||||
register_service_binaries(MODULES)
|
||||
PENDING.difference_update(wanted)
|
||||
return errors
|
||||
|
||||
|
||||
Reference in New Issue
Block a user