mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-06-11 03:32:10 +00:00
* feat(api): live download phase, granular progress, swallowed-failure detection, per-request CDM
- Tee Track.download progress so the job reports real percentage (not just 5/100%) and a
human-readable phase ('downloading video 1080p') via the new job.phase field.
- Detect a swallowed download-worker failure (dl.result() prints 'Download Failed' but
exits 0) and raise, so the job is marked failed instead of completed-with-no-output.
- Per-request CDM override (dl_instance.cdm_override; get_cdm prefers it) so a job can use
a specific CDM device without mutating shared config.
* feat(api): expose service capability flags + auth methods in /services
needs_auth / has_search / has_drm derived from which Service hooks are overridden, and
auth_methods inferred from what the service's authenticate() body references (cookies /
credentials), so clients can show only the relevant auth options per service.
* feat(api): per-request credential injection for downloads
Accept a 'credential' ('user:pass') job parameter and feed it into the credentials map that
dl.get_credentials() reads, so a client can authenticate a download without persisting
anything to disk. (Kept on the deployment branch; the PR branch uses the client-sent path.)
* feat(api): gate per-request CDM override behind serve.cdm_overrides
A per-request `cdm` selects a server-side device, so honour it only when allow-listed.
`serve.cdm_overrides` opts in: a list permits those device names, or `true` permits any
(single trusted client). Unset/false rejects every override with 403, so an arbitrary device
can't be selected by default.
* fix(api): redact credentials and proxy userinfo in serialized job parameters
Job parameters can carry a raw user:pass credential and a proxy URL with embedded userinfo;
mask them wherever parameters are serialized for an API response so secrets don't leak via
the job-detail endpoint. Also read skipped-subtitle / download-failure state from the dl
instance instead of scraping stdout, and drop the dead n_m3u8dl percent branch.
* feat(api): prefer explicit AUTH_METHODS class var over source inference in /services
Inferring auth methods from authenticate() source mostly returns both options because services
call super().authenticate(cookies, credential). Prefer an explicit AUTH_METHODS class var when a
service declares one, falling back to inference.
* style: use plain hyphens instead of em-dashes in comments
* feat(api): gate per-job credentials, isolate caches, scrub error fields
Address review feedback on #113:
- Gate per-request credential/credentials behind serve.allow_job_credentials (default off,
403 when not opted in), mirroring the existing serve.cdm_overrides CDM gate.
- Isolate the token cache per credential: when a per-job credential is set, namespace
config.directories.cache by its hash in the worker, so two clients on the same service
with different credentials can't share a cached session.
- Scrub the credential, its password half, and proxy userinfo out of error_message,
error_details, error_traceback and worker_stderr before they leave via the job-detail API.
- Remove the unused _execute_download_sync in-process path (would have leaked one job's
credential into the shared global config).
- Document serve.cdm_overrides and serve.allow_job_credentials in the example config.
- Add tests for both gates (403 default, allowlist pass) and the parameter/error redaction.
* fix(dl): flag download_failed when result() swallows a worker failure
dl.result() catches a download-worker exception, reports it, and returns
without re-raising so the CLI still exits cleanly. An embedding caller (the
API worker) had no way to tell the title actually failed and would report
the job completed with no output. Expose a download_failed flag, set in the
swallow path, that the worker reads after result() returns.
* feat(api): surface skipped subtitles and pass skip_subtitle_errors
Thread skip_subtitle_errors from the job into dl() so the API can opt into
non-fatal subtitle handling, and report which subtitles were skipped: store
them on the job (dl.SkippedSubtitle dicts) and include them in job details
so a client can tell the user which weren't available.
---------
Co-authored-by: Avi Cohen <avraham.coh770@gmail.com>
tests/remote/
Test suite for the unshackle remote-services subsystem:
unshackle/core/remote_service.py—RemoteClient,RemoteService, helpersunshackle/core/api/— routes, handlers, session_store, input_bridge, download_manager, errors, compressionunshackle/commands/serve.py— CLI surface
Two tiers:
| Tier | Where | Network | Default |
|---|---|---|---|
| unit | tests/remote/unit/ |
mocked (responses, in-process aiohttp) |
runs by default |
| e2e | tests/remote/e2e/ |
hits an unshackle serve instance |
skipped unless --live |
Install test deps
uv sync --group test
Run unit tests
uv run pytest tests/remote/unit -v
Fast, hermetic, no external calls.
Run e2e tests
The suite can either:
- Spawn its own serve (default with
--live): startsuv run unshackle serve --host 127.0.0.1 --port <free> --no-key --remote-onlyfor the session, waits for/api/health, tears it down at the end. - Talk to an external serve you started in another shell: pass
--server-url http://host:port.
Spawn mode is controlled by --spawn-serve {auto, always, never} (default
auto — spawn only when --server-url is empty).
Auto-spawn (recommended)
uv run pytest tests/remote/e2e -v --live
External serve
# in shell A
uv run unshackle serve --host 0.0.0.0 --no-key --remote-only
# in shell B
uv run pytest tests/remote/e2e -v --live --server-url http://localhost:8786
With API key
uv run pytest tests/remote/e2e -v --live --secret-key your-key
Limit which services run
uv run pytest tests/remote/e2e -v --live --services FOO,BAR
Configure e2e scenarios
Copy tests/remote/e2e/fixtures/fixtures-example.yaml to
tests/remote/e2e/fixtures/fixtures.yaml (gitignored) and fill in the
services you have access to. Schema (see the example file for full docs):
services:
EXAMPLE:
title_url: "..." # required
series_url: "..." # optional — overrides movie target when set
target_season: 1
target_episode: 1
search_query: "..."
expected_quality:
min_height: 1080
min_codecs: [AVC]
min_ranges: [SDR]
min_track_count: 4
runs_download_test: true # opt in to the download smoke test
runs_license_test: false # opt in to the license test
license_drm: widevine # or "playready"
license_quality: 1080
Tests skip a service if its session can't be created (auth missing,
geofence, etc.) or if the matching runs_*_test flag is false.
Run everything
uv run pytest tests/remote -v --live
CLI flags added
| Flag | Default | Purpose |
|---|---|---|
--live |
off | Opt in to e2e tests |
--server-url URL |
"" (or $UNSHACKLE_SERVE_URL) |
Target external server; empty triggers auto-spawn |
--spawn-serve {auto,always,never} |
auto |
Spawn serve when no URL given |
--secret-key KEY |
"" (or $UNSHACKLE_SECRET_KEY) |
X-Secret-Key header |
--services A,B |
(all) | Restrict e2e to these tags |
Markers
unit— fast, mocked (default)live— needs--liveslow— hits real services; combine with--live
Run only fast unit tests:
uv run pytest tests/remote -m "unit and not slow"
Adding a new service to e2e
- Add a block under
services:in your localfixtures.yaml. - No new Python needed — every e2e test is parametrized over the YAML.