feat(api): aggregate REST download progress with weighting, track labels and mux stage

Replace the class-level Track.download monkeypatch with a per-job progress sink threaded through dl.result(). The API now reports a single aggregate signal instead of each track's bouncing 0-100%:

- bitrate-weighted completion so video/audio dominate subtitles
- completed_tracks/total_tracks counts and active_tracks labels (e.g. "video 2160p DV", "audio en-US 5.1")
- downloads fill 0-90%; repackaging (when needed) and a "muxing" stage carry it to 100% so post-download work is no longer frozen at 100%
- monotonic throughout (handles the download->decrypt callable reuse)

Also:
- accept "HDR10P" as the canonical API range value ("HDR10+" still works)
- declare AUTH_METHODS opt-in on the Service base
- raise typed APIError (WORKER_ERROR/DOWNLOAD_ERROR) from the worker path
- move the progress helpers to unshackle/core/api/progress.py
This commit is contained in:
imSp4rky
2026-06-08 15:37:40 -06:00
parent 1a3cd09fc8
commit 2f35a4d468
8 changed files with 333 additions and 76 deletions

View File

@@ -9,13 +9,8 @@ import pytest
from aiohttp import web
from unshackle.core.api import handlers
from unshackle.core.api.download_manager import (
DownloadJob,
JobStatus,
_redact_parameters,
_redact_text,
_secret_values,
)
from unshackle.core.api.download_manager import (DownloadJob, JobStatus, _redact_parameters, _redact_text,
_secret_values)
from unshackle.core.api.errors import APIError, APIErrorCode
pytestmark = pytest.mark.unit
@@ -140,3 +135,18 @@ async def test_credential_allowed_when_enabled(stub_handler):
stub_handler.setattr(handlers.config, "serve", {"allow_job_credentials": True})
resp = await handlers.download_handler({"service": "ATV", "title_id": "t", "credential": "u:p"})
assert isinstance(resp, web.Response)
# ---------- range validation ----------
def test_range_validation_accepts_hdr10p_and_alias():
# canonical "HDR10P" and back-compat "HDR10+" both pass; mixed casing too
assert handlers.validate_download_parameters({"range": ["HDR10P", "DV", "SDR"]}) is None
assert handlers.validate_download_parameters({"range": ["hdr10+"]}) is None
assert handlers.validate_download_parameters({"range": "HYBRID"}) is None
def test_range_validation_rejects_unknown_and_lists_hdr10p():
err = handlers.validate_download_parameters({"range": ["HDR99"]})
assert err and "HDR10P" in err and "HDR99" in err