Commit Graph
28 Commits
Author SHA1 Message Date
imSp4rky 3aa275035b feat(serve): gate server_cdm per service and let clients follow it
serve.users.<key>.server_cdm accepts a list of service tags; retry re-attaches the job service and a bare string no longer fails open. An unset client server_cdm now follows the server's per-service /tracks flag.
2026-08-27 10:10:46 -06:00
imSp4rky 1aa6315dc3 feat(drm): let a track pin the DRM system it licenses with
Adds Track.drm_preference so a service can pin a track to Widevine or
PlayReady when a manifest advertises both but only one will license it.
Honoured in DASH/HLS selection, the dl licence path and the remote/API
track round-trip. Also fixes quality-keyed CDM maps sending audio to the
default entry, and a licence callable binding the CDM before the swap.
2026-08-23 16:32:23 -06:00
imSp4rky d8739b3561 feat(dl): add --require-audio/--require-video and --warn-only alias
Gate a title on audio or video languages that must exist, independent of what -l/-al/-vl select. --require-subs joins the same gate: its --s-lang conflict and download-all side effect are gone, so -sl now decides what to keep. Requiring a language that is also excluded fails at parse time.
2026-08-23 12:24:45 -06:00
imSp4rky 36add10e66 fix(api): survive Windows errno 13 on the job progress file 2026-08-21 17:20:15 -06:00
imSp4rky 29d0828c33 style: clean up prose 2026-08-19 13:45:58 -06:00
imSp4rky 511f96d8db docs: strip non-essential comments, fold key facts into docstrings 2026-08-16 23:32:02 -06:00
imSp4rky ee9d3edfe3 Merge branch 'dev' into feat/downloader-speed
# Conflicts:
#	unshackle/core/api/download_manager.py
#	unshackle/core/downloaders/requests.py
#	unshackle/core/manifests/dash.py
#	unshackle/core/session.py
#	unshackle/core/utils/webvtt.py
2026-08-16 12:43:01 -06:00
imSp4rky 21d5f1c5e1 refactor(core)!: drop leading underscores from internal definitions
Renames 314 defs and 41 module/class-level constants. Keeps 11 names underscored where the stripped form collides in scope, plus 5.5.0 shims for the three symbols external service repos call.
2026-08-15 22:07:41 -06:00
imSp4rky 415a195957 test(serve): strip ANSI from CLI output before asserting error message 2026-08-13 16:33:08 -06:00
imSp4rky a248ae79bd feat(serve): gate server CDM licensing behind a per-key server_cdm opt-in
Keys configured under serve.users default to off, so they license with their own local CDM; session licensing and download jobs both refuse otherwise.
2026-08-13 11:40:30 -06:00
imSp4rky 5796729bf3 feat(remote): accept serve.users keys in api-only mode and add client auth-header fallback
api-only/remote-only serve now authenticates every serve.users key (str-coerced), not just api_secret, so per-user service allowlists apply. RemoteClient tries X-Secret-Key then X-Api-Key on 401 (sticky winner); remote_services.auth_headers prepends extra header names to the defaults.
2026-08-12 17:58:55 -06:00
imSp4rky ec594b0b9c fix(api): carry air_date, absolute, anime and daily across the title wire
serialize_title and title_to_meta now emit them as conditional keys,
_build_title restores them, and service-class flags stamp per title.
2026-08-11 17:50:38 -06:00
imSp4rky cfae8f8882 Merge branch 'dev' into feat/downloader-speed
Catch the branch up with dev (45 commits) so testers can run the perf work
against current dev.

Conflict resolutions:
- downloaders/requests.py: keep the branch's racing-gated read1, adaptive
  controller speed reporting and byte-range slice guards; take dev's
  content-encoding checks (raw reads are skipped on an encoded body) and its
  rolling SpeedWindow for the no-controller speed fallback. The single-URL
  ranged-parallel gate now requires no speed limit and no caller Range.
- service.py / session.py: keep the branch's TimeoutHTTPAdapter and retry
  config, raise POOL_MAX_SIZE to 64 so dev's pool-starvation fix survives
  (pool_block=True stalls threads on a pool smaller than the in-flight count).
- docs: dev split configuration.md into per-area pages, so the branch's rnet
  network option rows moved to reference/configuration/network.md.
2026-08-07 10:45:47 -06:00
imSp4rky 041ba5293b feat(titles): add optional Episode.part, selected with -w s1e1.2
Opt-in and additive: an episode without a part behaves exactly as before.
2026-08-04 17:38:30 -06:00
imSp4rky 036368b3c3 fix(api): keep sub_format "original" working through the API 2026-07-16 23:22:13 -06:00
imSp4rky e31e60a418 style: run ruff format over the codebase 2026-07-15 14:49:27 -06:00
imSp4rky a450184ab8 test(remote): isolate job history writes from the real api_history.jsonl 2026-07-12 10:30:30 -06:00
imSp4rky 5cd50544a8 fix(remote): harden serve API access control and input handling
Bind sessions and download jobs to the API key (not source IP), reject path-traversal cache keys, bound decompression of peer-supplied blobs, gate the CDM device before the vault fallback, and compare API keys in constant time.
2026-07-06 23:54:05 -06:00
imSp4rky 52e04002e4 test(remote): add RemoteService method-contract tests 2026-07-03 21:26:47 -06:00
imSp4rky 3f5b5e0fe1 refactor(api): drop dead error codes, lock error contract harness
Remove 4 unused APIErrorCode members; strengthen test_errors.py to pin the full (status, code, retryable) contract before the PRD 05 collapse.
2026-06-21 14:13:44 -06:00
imSp4rky d4b0c81c63 refactor(api): collapse route registration into single ROUTES table
Derive both setup_routes and setup_swagger from one ROUTES table, and move the repeated trailing APIError->response block into an @api_handler decorator.
2026-06-21 13:47:23 -06:00
imSp4rky fb90cd87b0 refactor(api): dedup handler serializers, validation, and service setup
Collapse repeated serializer/validation/CDM/list-setup logic in handlers.py behind small helpers; output behaviour unchanged.
2026-06-21 13:23:22 -06:00
imSp4rky 8a9a1e1bdc refactor(api): remove dead code paths in serve/remote layer
Delete code paths that never executed in the serve/API/remote stack:

- BridgeCancelledError: raised but never caught; replace raises with RuntimeError and drop the class.
- InputBridge.error setter and _error field: written at the handler layer, never read (session.auth_error is the surfaced channel).
- cancel_event param and _check_cancel inner fn on _perform_download: the only caller always passed None (the live job.cancel_event is untouched).
- result_with_progress monkeypatch: its status pings were redundant with the file-polled progress_sink, which is now the sole path.
- session param on _resolve_manifest_data: unused in the body.

Reconcile SessionStore's hardcoded 600s auth-state TTL into a shared AUTH_INPUT_TIMEOUT constant referenced by both the InputBridge input timeout and the store, removing the magic number without changing the longer grace window.

Update input_bridge/session_store tests for the new channels and add an assertion that auth-state entries expire at the single rule.
2026-06-21 13:07:45 -06:00
imSp4rky a2f670b140 feat: support Python 3.14 and widen range to 3.11-3.14
Move the supported interpreter range to >=3.11,<3.15, dropping the EOL 3.10 floor and unblocking installs on 3.13/3.14.

- pyproject: requires-python ">=3.10,<3.13" -> ">=3.11,<3.15"
- update_checker: replace deprecated asyncio.get_event_loop() with asyncio.to_thread() (warns on 3.12+, slated for removal)
- test_compression: simplify _run() to plain asyncio.run()
- README: bump documented Python range to 3.11 - 3.14
- uv.lock: regenerate for the new interpreter floor
2026-06-20 15:25:42 -06:00
imSp4rky 2f35a4d468 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
2026-06-08 15:37:40 -06:00
AviDevandAvi Cohen 1a3cd09fc8 fix(api): repair REST API downloads, add /services flags & live progress (#113)
* 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>
2026-06-08 11:38:08 -06:00
imSp4rky 7654e91ebc feat(dl): gate s_lang/a_lang miss behind --best-available
Missing requested subtitle and audio languages now warn and continue when --best-available is set instead of hard-exiting. Without the flag, missing languages still produce an error and exit, matching the prior strict behavior. Audio missing-lang detection is now symmetric with subtitles.

- add find_missing_langs helper in core/utilities for reuse between s_lang and a_lang paths (skips all/best/orig sentinels)
- refactor dl.py s_lang/a_lang checks to share the helper
- add tests/lang_selection covering match primitives, helper output, and tricky langcodes corners (zh-Hans/zh-Hant/zh-CN/zh-TW/zh-HK, cmn/yue, fil/tl/tgl)
- clean up unused-var ruff F841 in tests/remote/unit/
2026-05-22 13:52:35 -06:00
imSp4rky 746b573711 test(remote): add unit + e2e suite for remote-services subsystem
Covers RemoteClient/RemoteService, REST routes, handlers, SessionStore, InputBridge, DownloadQueueManager, errors, compression, and serve CLI. E2e tier opts in via --live and can auto-spawn its own serve.
2026-05-21 10:45:25 -06:00