Commit Graph

606 Commits

Author SHA1 Message Date
sp4rk.y
6ae0fc2c81 Merge pull request #114 from unshackle-dl/dependabot/uv/aiohttp-3.14.0
chore(deps): bump aiohttp from 3.13.5 to 3.14.0
2026-06-05 15:06:12 -06:00
dependabot[bot]
a79cf2b5ee chore(deps): bump aiohttp from 3.13.5 to 3.14.0
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.14.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 00:21:47 +00:00
imSp4rky
3dcc584b2f chore(changelog): update changelog and cleanup readme 5.1.0 2026-06-03 11:11:23 -06:00
imSp4rky
538deac63e fix(session): proxy auth on http targets and string params support
Drop the manually-set Proxy-Authorization header in the Service base class. It was malformed (base64 of user:pass with no "Basic" scheme) and redundant: both rnet (Proxy.all) and requests authenticate from the credentials embedded in the proxy URL. The broken header was tunnelled harmlessly on HTTPS (CONNECT) but handed to the proxy on plaintext-http forward requests, causing HTTP 407 (e.g. http MPD/segment URLs behind an authenticated geofence proxy).

Also make RnetSession._build_url accept the same params shapes as requests (mapping, sequence of pairs, or a pre-built str/bytes query). urlencode() previously raised TypeError on a string params value.
2026-05-30 18:13:55 +00:00
imSp4rky
0561623825 docs: simplify README with demo and requirements 2026-05-29 19:01:57 -06:00
imSp4rky
4ce0dd5bb9 chore(changelog): update version date and add new features and fixes for 5.1.0 2026-05-29 18:27:31 -06:00
imSp4rky
191c4f47e0 refactor: remove dead aria2c and n_m3u8dl_re downloader code
The legacy aria2c, curl-impersonate, and N_m3u8DL-RE downloader backends were replaced by the unified in-process requests/rnet downloader. This strips the leftover references those removals missed.
2026-05-29 18:25:56 -06:00
imSp4rky
accfe9cee1 refactor(dl): declare hybrid_base_only flag and extract standalone mux helper
Make the DV-ingredient marker a declared Video attribute (sibling of needs_duration_fix) instead of a runtime-stamped attribute read via getattr, so the flag is explicit and type-checked.

Extract mux_video_standalone() to replace the duplicated per-track mux logic shared by the hybrid and normal-mode paths, removing the copy-paste and the getattr probes. Behavior unchanged.
2026-05-29 17:13:29 -06:00
imSp4rky
5899c1eec8 refactor(example): showcase full unshackle feature surface 2026-05-29 12:54:37 -06:00
imSp4rky
6bc601db39 fix(dl): mux all requested ranges and select highest DV alongside hybrid
When -r included HYBRID, the dl pipeline only muxed the hybrid output and dropped every other downloaded range. DV was also treated purely as a hybrid ingredient pool, so an explicitly requested DV range never produced a standalone deliverable - only the lowest DV (the RPU ingredient) was selected.

- Select the best DV per resolution as a standalone deliverable when DV is in -r, while still using the lowest DV as the hybrid ingredient (dv_is_deliverable).
- Flag the ingredient-only DV (hybrid_base_only) so it is downloaded for the hybrid build but skipped during standalone muxing.
- Mux every requested range standalone after hybrid processing; build hybrids from deepcopied ingredients so the originals stay muxable.
- Add Tracks.merge_video_selections to de-dup the ingredient/deliverable overlap so a shared DV track is not downloaded or muxed twice.
2026-05-28 21:25:30 -06:00
imSp4rky
fb8dc0bd9d refactor(ip_info): simplify lookup and trim cache
Cache only country/country_code (drop full IP/org/asn), bump CACHE_KEY and auto-purge stale cache versions. Dedup the three provider parsers into one normalize(). Use a plain retry-free requests session for lookups instead of the TLS-fingerprinted session, carrying only the proxy over, so a 429 returns directly and hands over to the next provider reliably.
2026-05-27 22:48:39 -06:00
imSp4rky
40104be738 fix(dash): inherit SegmentTemplate attributes across AdaptationSet/Representation
A SegmentTemplate can sit at both the AdaptationSet and Representation levels, with shared attrs like @timescale only on the outer node. The parser picked one or the other, dropping the outer node when an inner existed - so @timescale defaulted to 1 and only the first segment was emitted.

Merge instead: Representation node as base, inheriting any attribute or SegmentTimeline it omits from the AdaptationSet node (per ISO/IEC 23009-1).
2026-05-27 12:12:42 -06:00
imSp4rky
96fd971af0 feat(import): include cover-art attachments in --export/--import
Export URL-backed attachments per title and rebuild them on import so they ride along into the muxed output. Local font attachments are skipped (not portable).
2026-05-26 17:37:44 -06:00
imSp4rky
1cb0e4b766 feat(naming): per-service title_map remapping (#106)
Add services.<TAG>.title_map exact-match dict to rewrite service-provided titles before naming/output. Shared remap_titles helper applied on local dl, import, and client-side dl --remote (server stays raw so clients can override names for services they don't have installed locally).
2026-05-26 14:08:52 -06:00
imSp4rky
f4544b4a70 fix(hls): resolve per-rendition KID in no-EXT-X-KEY fallback
Manifests that signal DRM only via EXT-X-SESSION-DATA share one session_drm across all renditions, carrying just the manifest-level KID. The fallback licensing path licensed without the track's real KID, so --vaults-only only looked up the shared KID and could not find the rendition's key even when the vault held it (--cdm-only masked this since the license returns all keys). Resolve the KID from the init segment's tenc box and pass it through, matching the EXT-X-KEY path.
2026-05-26 14:04:06 -06:00
imSp4rky
f1febe7e43 chore(release): bump version to 5.1.0 2026-05-26 10:54:14 -06:00
imSp4rky
b3a6db915b fix(cli): report broken command/service loads once and cleanly
Loaders raised at import time, and since Python does not cache a failed import, every command importing services re-ran and re-reported the same errors. Build the registry once, collect failures into LOAD_ERRORS, and surface them via a single click.ClickException at the list/get chokepoints so the message renders once without a traceback or cascade.
2026-05-25 19:50:11 -06:00
imSp4rky
5e3ffdeaa1 fix(hybrid): correct static L6 source and reset stale L5 active area
level_6() read MaxCLL/MaxFALL from the dovi_tool L1 line (dynamic per-shot content-light peak) and baked it into the static L6 field, so displays tone-map to a phantom peak and HDR brightness breaks. Read MaxCLL/MaxFALL from the static L6 block instead (mastering display parse unchanged), falling back to the HDR10 base stream only when the RPU has no L6 block. Add sanitize_l6() to clamp MaxCLL to the mastering-display peak and MaxFALL to MaxCLL (0 = "unknown" preserved); also applied to the HDR10+ -> DV path.

level_5() early-returned when cropdetect found no bars on the base, leaving whatever L5 offsets the DV source carried. When DV and HDR10 geometry differ (e.g. a letterboxed DV injected into an already-cropped base) the stale offsets rode through and signalled phantom bars. Always write the detected active area, including zeros, so stale source L5 resets to the full base frame.
2026-05-25 17:04:50 -06:00
imSp4rky
7017bd0460 feat(import): reconstruct downloads from an --export sidecar
Add a shareable --export schema and a new `unshackle import` command that rebuilds tracks from the export, injects the licensed content keys, and runs the normal download/decrypt/mux pipeline with no re-authentication or licensing.
2026-05-24 21:17:31 -06:00
imSp4rky
13f924f825 feat(dl): add real bitrate probing (-rvb/-rab)
Add --real-video-bitrate/-rvb and --real-audio-bitrate/-rab to measure a track's true bitrate from actual media size instead of the manifest's declared value, which is often inflated. Useful for accurate track listings and --vbitrate/--vbitrate-range selection.
Single-file tracks are measured exactly; segmented HLS/DASH/ISM are sampled. Only the top renditions per quality tier are probed, in parallel, so it stays fast. Without the flags, behaviour is unchanged.
2026-05-24 17:30:40 -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
b0ae88812c fix(vaults): enable WAL on SQLite vault to fix concurrent locks 2026-05-21 23:00:39 +00: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
imSp4rky
9c905ef7a3 fix(api): load real CDM for service init in list/session/download flows 2026-05-21 10:31:52 -06:00
imSp4rky
c19a5ebdd2 fix(api): propagate Click default=None through service instantiation
Manual kwarg binding dropped None defaults, so services with required positional args like `type` or `profile` raised TypeError via the API while local CLI worked. Replace it with `ctx.invoke(service.cli, ...)` so Click handles defaults, and factor the duplicated setup into load_service_yaml / build_parent_ctx / instantiate_service.

Also resolve a real CDM (load_full_cdm) for search/list_titles/list_tracks; the lightweight stub lacks device_type/security_levelthat services read in __init__.
2026-05-21 08:49:16 -06:00
imSp4rky
d2380cff97 feat(dl): live countdown for --slow delay 2026-05-18 13:33:46 -06:00
imSp4rky
684e56eb97 feat(tracks): configurable audio codec priority for tie-breaking
Adds optional `audio.codec_priority` list in unshackle.yaml to define preferred audio codec order when tracks share the same bitrate and language. Listed codecs rank in the order given; unlisted codecs retain bitrate-based order and fall after the listed group (soft priority - nothing dropped). Atmos and descriptive rules still apply last.
2026-05-18 11:08:55 -06:00
imSp4rky
900ad1fde1 fix(titles): normalize odd resolutions in filename quality token
Non-16:9 aspect ratios with widths like 1918x1080 or 1620x720 were producing filenames like 911p. Snap width to the nearest standard (3840/2560/1920/1280) within 50px and snap the resulting resolution back to the track's actual height when within 10px or already a standard step.
2026-05-17 19:06:53 -06:00
imSp4rky
64da561534 feat(vaults): tolerate vault failures during key get/add
Wrap vault get_key/add_key/add_keys calls in broad exception handlers so a single failing vault (network, auth, driver error) no longer aborts the operation - other vaults are still consulted/written. Failure cause is logged at WARNING so issues remain debuggable.

Inspired by unshackle-dl/unshackle#104 by @CodeName393.

Co-authored-by: CodeName393 <62503817+CodeName393@users.noreply.github.com>
2026-05-17 12:18:32 -06:00
sp4rk.y
13dcd7aa1a Merge pull request #109 from JohnVeness/seasons
episode.py: season/seasons pluralization
2026-05-17 12:03:54 -06:00
imSp4rky
61fe16e8d7 feat(api): sync /api/download with dl CLI flags and add serve.* defaults
- Wire --no-proxy-download through download_manager + handlers + swagger
- Add tag/proxy/tmdb_id/animeapi_id/enrich/worst to DEFAULT_DOWNLOAD_PARAMS
- Normalize `slow` (bool/"MIN-MAX" string/list) to tuple before invoking dl.result
- Overlay any /api/download flag declared under `serve:` in unshackle.yaml as a default (downloads, workers, best_available, etc.); request body still wins
- Quiet successful worker stderr from `warning` to `debug` (kept under job.worker_stderr for ?full=true)
- Include HYBRID in range validator
- Document new flags + overlay layering + max_concurrent_downloads / download_job_retention_hours
2026-05-17 11:54:02 -06:00
John Veness
e5a287bc14 episode.py: season/seasons pluralization
If num_seasons = 0, output "0 seasons" (not sure if this would ever occur). If num_seasons = 1, output "1 season". If num_seasons >=2 output "X seasons".
2026-05-17 15:06:55 +01:00
imSp4rky
34a6e2d8e2 fix(service): render request_input prompt via rich console 2026-05-16 16:43:47 -06:00
imSp4rky
f26f9bcbe2 feat(video): normalize SPS VUI to match manifest-derived range
Some services ship HDR10/HLG bitstreams whose SPS VUI still carries BT.709 colour primaries/transfer/matrix, causing mediainfo and downstream players to mis-classify the file. The manifest-derived `Video.range` is the source of truth; rewrite the VUI with ffmpeg h264_metadata/hevc_metadata BSF after repackage and before mux. Skips SDR, DV, and HYBRID; no-op when the VUI is already correct.
2026-05-16 13:55:05 -06:00
imSp4rky
ead88fe066 feat(hls): detect DV-composite tracks and restore signaling post-mux
Parse HLS SUPPLEMENTAL-CODECS to identify tracks that ship Dolby Vision RPU NALs in a stream whose primary codec is plain hvc1 (e.g. fMP4 ladders signalled as dvh1.08.x only via SUPPLEMENTAL-CODECS). Tag such tracks with the new `dv_compatible_bitstream` flag on Video.

Add DVFixup helper that runs `dovi_tool extract-rpu | inject-rpu` on flagged tracks before mux so the muxed MKV is recognised as Dolby Vision. Soft-fails to the source bitstream if dovi_tool is missing or any step errors.

Range stays whatever VIDEO-RANGE signalled. HDR10+ presence is a bitstream feature, not a codec-string feature, so services that know their encoder embeds HDR10+ SEI must override Video.range themselves.
2026-05-16 13:52:57 -06:00
imSp4rky
b4d422459c refactor(hybrid): extract dovi_tool and run_step helpers
Centralise dovi_tool subcommand invocations behind a thin wrapper module so Hybrid no longer re-implements argv construction, status spinners, and stderr handling per call site. Adds a generic `run_step` helper for subprocess steps that must produce a non-empty output file.
2026-05-16 13:50:55 -06:00
imSp4rky
cda8120b6d fix(title): use original-language audio for filename metadata
When a non-original audio language is the default (via muxing.default_language or sort order), the filename audio codec/channel fields still reflect the title's original-language track instead of whichever track appears first in the muxed MKV.
2026-05-16 11:56:04 -06:00
imSp4rky
c0dc1eb91f docs(output): document muxing.default_language override 2026-05-15 08:22:45 -06:00
imSp4rky
ccf8bafaf7 feat(mux): add muxing.default_language to override default track per type
Allows users to force a preferred audio/video/subtitle language as the MKV default track regardless of the title's original_language. Each track type falls back to its previous default rule when no match is found.
2026-05-14 14:24:23 -06:00
imSp4rky
0aca0e8888 feat(dl): cache content keys in-memory to skip duplicate license requests
A title with many tracks sharing the same KID issued one license request per track even when keys were identical. Add an in-memory KID -> key cache shared across all tracks of a single invocation, populated on vault hit and on license success. Subsequent tracks with cached KIDs short-circuit before the vault and license calls, reducing traffic to one request per unique KID.
2026-05-14 11:58:31 -06:00
imSp4rky
60441f05c4 fix(title): detect Atmos across all audio tracks for filename template
The {atmos?} placeholder checked only the first MediaInfo audio track, so a mux with a non-Atmos dub listed first dropped the Atmos tag from the filename even when another track carried JOC. Scan all audio tracks instead.
2026-05-14 10:22:55 -06:00
imSp4rky
82ab996777 fix(dl): prefer Atmos in -l best/all language selection
The per-language picker used max() keyed on bitrate only, so a higher-bitrate non-Atmos track was selected over a lower-bitrate Atmos track. Switch the key to (bool(x.atmos), x.bitrate) so Atmos wins with bitrate as tiebreaker, matching Tracks.sort_audio.
2026-05-14 10:00:25 -06:00
imSp4rky
c0929bf217 fix(template_formatter): preserve dash separator around empty conditional
When an empty conditional sits between a dot and a dash (e.g. `.{atmos?}-{tag}`),
the left-side dot was kept and the dash before the tag was dropped, producing
`...DDP5.1.TAG` instead of `...DDP5.1-TAG`. Prefer the dash when it is the
right-side separator.

Closes #107
2026-05-12 08:24:31 -06:00
imSp4rky
20c15f761b fix(proxies/nordvpn): use *.proxy.nordvpn.com for HTTPS proxy
NordVPN's HTTPS proxy endpoints now serve a certificate valid only for *.proxy.nordvpn.com. Connecting to the legacy <server>.nordvpn.com:89 hostname fails with SSLCertVerificationError (hostname mismatch).

Rewrite direct queries, server_map entries, and API-returned hostnames to the proxy subdomain so cert validation succeeds.
2026-05-11 09:55:48 -06:00
imSp4rky
9e4fdcdcd8 fix(dl): re-pick DV/HDR10 when HYBRID falls back under best_available
When HYBRID is requested alongside other ranges with best_available and no HDR10 base layer exists, the pre-validation hybrid selection had already locked in the lowest-resolution DV track. Snapshot the pre-hybrid pool and redo Cartesian range/quality/codec/lang selection over surviving ranges so DV (or HDR10-only) honors --worst and default best-pick semantics.
2026-05-10 11:04:29 -06:00
imSp4rky
7fb88e9a97 docs: update docs to match current codebase 2026-05-08 17:54:45 -06:00
imSp4rky
4c981e2ffd fix(tracks): honor --worst in hybrid range selection
select_hybrid always picked max-bitrate HDR10 base layer, ignoring the --worst flag. Thread worst through to use min bitrate when requested.
2026-05-08 15:37:09 -06:00
imSp4rky
5984eefcbe fix(sanitize): preserve parentheses, strip unidecode bracket artifacts (#105)
Commit 10cca7d re-added () to the stripped character set, which broke output_template patterns like ({year?}). The original reason for stripping parens was that unidecode maps 【】 to "[(" and ")]", leaving artifacts like [(SERIES NAME)] in filenames.

Allow parens in filenames so templates render correctly, and collapse the unidecode "[(" / ")]" sequences immediately after transliteration so unicode brackets still come out as [SERIES NAME].
2026-05-05 08:39:38 -06:00
imSp4rky
08c0862691 fix(manifests): clean stale .!dev resume markers before merge
HLS/DASH/ISM iterdir included leftover .!dev control files from aborted runs, crashing HLS merge_discontinuity and silently corrupting DASH/ISM merged output.
2026-05-04 22:51:45 -06:00
imSp4rky
db313a8ee2 refactor(routes, subtitle, track): improve code readability by formatting list structures 2026-05-04 22:21:03 -06:00