fix(services): register modules in sys.modules so repo-cloned services can self-import and remove old benchmark that should have been deleted

This commit is contained in:
imSp4rky
2026-07-14 13:46:29 -06:00
parent b8ec9e2498
commit c6ecdb605c
3 changed files with 16 additions and 98 deletions
+15
View File
@@ -67,6 +67,20 @@ unshackle dl EX 20914 # via an ALIAS
placeholders while the secrets that fill them stay out of it. Both halves placeholders while the secrets that fill them stay out of it. Both halves
read back through the same `self.config[...]`. read back through the same `self.config[...]`.
!!! warning "Split across files? Use relative imports"
If your service grows beyond a single `__init__.py`, import your own
modules **relatively**:
```python
from .helpers import parse_manifest # correct
from unshackle.services.TAG.helpers import parse_manifest # breaks from a repo
```
A service loaded from a remote repo lives outside the `unshackle` package,
so the absolute `unshackle.services.<TAG>.*` path does not resolve and the
import fails with `no known parent package`. Relative imports work both
locally and from a cloned repo.
--- ---
## The Service base class ## The Service base class
@@ -629,6 +643,7 @@ unshackle dl MYSVC https://myservice.com/watch/abc123
- [ ] Tracks come from a manifest parser with `to_tracks(language=title.language)`; no filtering by quality/language. - [ ] Tracks come from a manifest parser with `to_tracks(language=title.language)`; no filtering by quality/language.
- [ ] Encrypted tracks are marked with DRM; the license callbacks you need return responses **unmodified**. - [ ] Encrypted tracks are marked with DRM; the license callbacks you need return responses **unmodified**.
- [ ] URLs, user agents, and certificates live in `config.yaml`, read via `self.config[...]`. - [ ] URLs, user agents, and certificates live in `config.yaml`, read via `self.config[...]`.
- [ ] Multi-file services import their own modules relatively (`from .helpers import x`), so they work when loaded from a repo.
Read `unshackle/services/EXAMPLE/` end to end; it annotates every feature Read `unshackle/services/EXAMPLE/` end to end; it annotates every feature
touched above in one place. touched above in one place.
-98
View File
@@ -1,98 +0,0 @@
#!/usr/bin/env python3
"""
Benchmark subtitle conversion backends to (re-)tune the preference ranks in
``unshackle/core/tracks/subtitle_convert.py``.
Runs every backend that can read each input file, converting to a target format (default
SRT), and reports cue count, leaked ASS override tags, and output size — so you can compare
fidelity per (source, target) pair on real files. Read-only: copies inputs to a temp dir.
Usage:
uv run python scripts/bench_subtitle_backends.py <file-or-dir> [<file-or-dir> ...] [--target SRT]
Example:
uv run python scripts/bench_subtitle_backends.py downloads/
"""
from __future__ import annotations
import argparse
import re
import shutil
import tempfile
from pathlib import Path
from unshackle.core.tracks import subtitle_convert as sc
from unshackle.core.tracks.subtitle import Subtitle
Codec = Subtitle.Codec
EXT_TO_CODEC = {
".srt": Codec.SubRip,
".vtt": Codec.WebVTT,
".ass": Codec.SubStationAlphav4,
".ssa": Codec.SubStationAlpha,
".ttml": Codec.TimedTextMarkupLang,
".smi": Codec.SAMI,
".sami": Codec.SAMI,
}
def gather(paths: list[str]) -> list[Path]:
files: list[Path] = []
for p in paths:
path = Path(p)
if path.is_dir():
files.extend(f for f in path.rglob("*") if f.suffix.lower() in EXT_TO_CODEC)
elif path.suffix.lower() in EXT_TO_CODEC:
files.append(path)
return sorted(files)
def metrics(text: str) -> tuple[int, int, int]:
cues = len(re.findall(r"-->", text))
ass_residue = len(re.findall(r"\{\\", text))
return cues, ass_residue, len(text)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("paths", nargs="+", help="subtitle files or directories")
ap.add_argument("--target", default="SRT", help="target codec value (SRT, VTT, ASS, ...)")
args = ap.parse_args()
target = Codec(args.target.upper())
files = gather(args.paths)
if not files:
print("No subtitle files found.")
return
tmp = Path(tempfile.mkdtemp(prefix="subbench_"))
print(f"{'file':40} {'source':10} {'backend':12} {'ok':3} {'cues':>5} {'resid':>5} {'bytes':>7}")
for f in files:
source = EXT_TO_CODEC[f.suffix.lower()]
if source == target:
continue
for backend in sc.REGISTRY:
if not (backend.is_available() and backend.can_convert(source, target)):
continue
work = tmp / f"{f.stem}.{backend.name}{f.suffix}"
shutil.copy2(f, work)
sub = Subtitle(url="x", language="en", codec=source)
sub.path = work
try:
# Call the backend directly so each row reflects only that backend (no fallback).
out = work.with_suffix(f".{target.value.lower()}")
backend.convert(sub, target, out)
cues, resid, size = metrics(out.read_text("utf8", errors="replace"))
print(
f"{f.name[:40]:40} {source.name[:10]:10} {backend.name:12} {'Y':3} {cues:>5} {resid:>5} {size:>7}"
)
except Exception as e: # noqa: BLE001 - benchmark reports failures, does not raise
print(
f"{f.name[:40]:40} {source.name[:10]:10} {backend.name:12} {'N':3} {'-':>5} {'-':>5} {'-':>7} {type(e).__name__}"
)
if __name__ == "__main__":
main()
+1
View File
@@ -111,6 +111,7 @@ def import_module_by_path(path: Path) -> ModuleType:
spec = importlib.util.spec_from_file_location(name, path) spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec) module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module) spec.loader.exec_module(module)
return module return module