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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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].
HLS/DASH/ISM iterdir included leftover .!dev control files from aborted runs, crashing HLS merge_discontinuity and silently corrupting DASH/ISM merged output.
Replace get_ip_info + get_cached_ip_info pair with a single unshackle.core.utils.ip_info module providing a normalized return shape across providers. Adds optional ipinfo_api_key config for the ipinfo.io Lite endpoint (higher rate limits, ASN/org/continent data), swaps the ipapi.co fallback for ip-api.in, and migrates all callers (service, gluetun, remote_service, api/handlers, DSNP, YT) to the new import path. Auth token is sent per-request and never attached to the shared session headers.
Allow output_template.folder to be a dict with movies/series/songs keys so music libraries can use artist/album folder layouts while movies and series keep their own scheme. Legacy string form still applies to all title types.
Single-URL tracks (no DASH/HLS/ISM manifest) previously streamed sequentially over one TCP connection, capping throughput at per-flow CDN shaping limits. Probe ranges via a 1-byte GET; if supported and total size >= 64MB, split the byte range across N workers (capped by --workers) writing to a pre-allocated file at offsets. Each worker delegates to download() in part mode for shared retry/Range-resume semantics. ~2-3x speedup observed on shaped CDN edges.
Filters service tables in source vaults against the locally installed services (config.directories.services), so users don't pull keys for services they don't have. Mutually exclusive with --service.
Adds 'kv search <KID>' with optional -s/--service and -v/--vault
filters. Iterates configured vaults, short-circuits on first hit, and
renders results in the same Rich tree style used by the DRM key
display. Remote vaults that cannot enumerate services without a
service tag are skipped with a clear hint to re-run with --service.
When the active EXT-X-KEY changes but no segments precede the new key (e.g. rotation at the first segment), no separate decrypt batch is flushed for the previous DRM and its content keys are lost. The merged file still contains samples encrypted under those keys, so the final mp4decrypt/shaka call decrypts them as garbage.
Carry the previous DRM's content keys into the new DRM via setdefault so every key needed across the merged segments is present at decrypt time. Existing zero-KID fallback handling (PlayReady, Widevine) remains the disambiguator for tracks whose tenc default_KID is all-zero.
Mirrors the PlayReady fix (fbc4aa2) for Widevine. HLS manifests with per-segment EXT-X-KEY changes generate distinct PSSH per segment, so service callbacks building the license URI from cached track-level PSSH can mismatch the challenge KID and trigger CEKNotFound. Forward pssh from the active DRM and fall back to the legacy single-arg call when a service hasn't adopted the kwarg.
HLS manifests with per-segment EXT-X-KEY changes generate distinct WRMHEADERs per segment. Service license callbacks that build the license URI from cached track-level PSSH state can mismatch the challenge KID, causing the license server to omit it and triggering CEKNotFound. Forward pssh_b64 from the active DRM and fall back to the legacy single-arg call when a service hasn't adopted the kwarg.
Video.Range.from_cicp() crashed with ValueError on any CICP primaries, transfer, or matrix value outside the small subset previously enumerated (e.g. TransferCharacteristics=4 / BT.470M gamma 2.2 seen in SCTE-stitched live MPDs). Extend the three inner enums to cover all documented ITU-T H.273 / ISO/IEC 23001-8 values and fall back to Unspecified on unknown codes so SDR content no longer fails to parse.
Source: https://raw.githubusercontent.com/FFmpeg/FFmpeg/master/libavutil/pixfmt.h (authoritative implementation of ITU-T H.273 / ISO/IEC 23001-8 tables). ITU spec itself: https://www.itu.int/rec/T-REC-H.273
Multi-period DASH manifests using SegmentBase with shared BaseURLs were downloading the entire file once per period. Deduplicate identical segments across periods so each file is only downloaded once. Also demote multi-period log message from info to debug.
Originally authored by panitan103 — adds optional session parameter to Track class allowing services to pass per-track sessions with different headers/cookies/auth.
Changes for dev branch integration:
- Fix type hints to support both requests.Session and RnetSession
- Fix session fallback in dl.py: track.session or service.session
- Remove redundant `session or None` assignment
Co-Authored-By: panitan103 <panitan103@users.noreply.github.com>
Previously, unexpected errors only showed a generic message without the actual exception details or traceback. Simplify the error handler to always include the exception type/message and print the full trace.
Wrapped metadata provider lookup in try/except so custom tags (Group) are always applied even when IMDB/TMDB lookups fail. Also log mkvpropedit errors instead of silently discarding them.
Shaka-packager can emit non-UTF-8 bytes in its log output, causing UnicodeDecodeError when reading stderr in text mode. Use explicit errors="replace" encoding. Also harden try_ensure_utf8 fallback paths to always return valid UTF-8 instead of raw bytes.
Partial downloads are now preserved across interruptions and retries. When a control file and partial data exist, the downloader sends a Range header to resume from the last byte. Falls back to full re-download if the server doesn't support Range requests (no 206).
The REST API download endpoint was broken after recent dl command changes.
- Add missing vbitrate_range, abitrate_range, and worst parameters to the API call and DEFAULT_DOWNLOAD_PARAMS
- Convert wanted episode strings (S01E01) to internal SxE format via SeasonRange so episode filtering works correctly
- Track completed output files via dl.completed_files instead of returning an empty list
Closes#98
WireGuard is stateless and never emits the OpenVPN-specific "initialization sequence completed" log line, causing the readiness check to always time out. Also accept "public ip address is" which gluetun logs once the WireGuard tunnel is up.
Closes#99
Multi-period DASH manifests using SegmentBase with shared BaseURLs were downloading the entire file once per period, causing massive file size inflation. Parse the SIDX box to extract proper per-segment byte ranges and deduplicate identical segments across periods.