Vaults.add_key and add_keys now push to every vault at the same time, so one unreachable vault costs its own timeout instead of the sum. MySQL defaults connect_timeout to vault_timeout.
serve.server_accounts lends region-tagged server credentials to remote sessions (round-robin per region, per-key opt-in, per-account cache). Remote sessions no longer fall back to the server's local credentials when the client sends none.
Remote mode resolved names with the local alias table and built an empty stub for any unknown tag, so an unentitled service showed a blank help page. Resolve against the server's tags and aliases, and reject unknown names with the list the server does offer.
The server no longer spends its configured proxy providers on a remote client unless the API key sets server_proxy. It places the client by geolocating the connection address and asks for a proxy when that region does not match its own.
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.
Add serve.services_refresh_interval; refresh pulls the repos and re-imports only the changed services, staging any with active jobs until they finish. Report skipped/broken services in the log and on /api/services.
Opt-in decrypt_segments runs mp4decrypt --fragments-info per segment on a thread pool as segments land (DASH, single-init HLS), then decrypts the init alone; hides the post-download decrypt step with byte-identical output.
Strip cue classes, <v>/<lang>/ruby and karaoke timestamps before conversion, decode entities for SRT/SSA/ASS, load pysubs2 as utf-8-sig, and route sdh_method: filter-subs through convert_before_strip instead of raising.
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.
Grow the service session pool to downloads * workers before track downloads, run mkvmerge jobs concurrently (muxing.concurrency, default 4), lock DRM preparation per key set instead of globally, and query local vaults first then remote vaults together.
Delete the dead grouped-download block, the planner, and the music service hooks; Song now flows through the one download loop, gets tagged via tag_file, and answers -w with {disc}x{track} keys.
BREAKING CHANGE: GROUP_AUDIO_DOWNLOADS, get_music_track_options and get_music_collection_label are gone. A music service returns one Audio track per quality and lets the framework choose.
New post_scripts config and repeatable --postscript flag run a command per output, season/album folder, or run, substituting filename-template variables into pre-tokenized argv. The REST API rejects hook commands.
Closes#150
Render all click help through rich-click, themed by a new `theme` config key (9 palettes + aliases, previewable via `unshackle env theme`), and show service docstrings in local and remote `dl <TAG> -h`.
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.
Episode gains an additive absolute field services can set, an {absolute}
naming token (3-wide), and an --enrich fill from TVDB's absolute order
that never rewrites season or episode numbers.
--anilist supersedes --animeapi (REST: anilist_id); the keyless AniList
GraphQL provider joins the registry with title search, mal: ID lookup,
an ANILIST mkv tag, and the anilist_title_language config option.
Services mark anime via Service.ANIME / Title.anime for anilist-first
metadata resolution with fall-through.
Supplied IDs resolve directly via a shared resolve_by_ids and never fall back to title search; metadata_providers gains per-kind ordering and a disable_metadata switch stops all automatic lookups while explicit IDs keep working.
BREAKING: --tmdb/--imdb/--tvdb are now mutually exclusive and require a usable provider; --enrich also overwrites original language; the imdbapi provider and imdb_api_enabled are replaced by a keyless imdb provider.
--enrich now sets Title.language when the service left it unset, so video tracks stop falling back to und. Adds original_language to MetadataResult (TMDB, TVDB, OMDb, IMDxAPI) and falls back to OMDb for --imdb, which previously did nothing while imdb_api_enabled defaults to false.
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.
subtitle.language_priority and audio.language_priority prepend to the CLI
languages, so the named languages sort first and nothing is filtered.
--exact-lang now applies to the sorts too, through matching_languages, which
follows the same RFC 4647 rule as Tracks.by_language and ignores invalid tags.
Document tvdb_api_key, tvdb_pin, tvdb_order and metadata_providers, the
--tvdb and --tvdb-order flags with their behaviour when an order does
not cover a whole series, the new REST API parameters, and the Matroska
TVDB2 tag prefixes.
sort_subtitles applied the language sort before the type sort. Python sorts
are stable, so the last pass wins and type became the major key: every forced
track, then every normal, then every SDH, each block ordered by language. In a
title with many languages this put a track far from its own SDH version, for
example Finnish at track 5 and Finnish SDH at track 42.
Add a group_by parameter that selects the major key. "type" is the default and
keeps the current order byte for byte. "language" swaps the two passes so each
language stays next to its own variants. Read it from the new subtitle.group_by
config key.
Also correct the docs: type_priority was described as sorting the variants
within each language, which was never true. It ranks the types across all
languages.
Upstream migrated the free IMDxAPI from imdbapi.dev to api.tiffara.com.
Endpoints and JSON schema are unchanged, so only BASE_URL and the
user-facing docs/docstrings needed updating.
Swap cookie/PKCE auth for the OAuth 2.0 device authorization grant with a cached rotating refresh token. Fix the connection-token key (`cat`) and move the cache to {cache}/vpn/ to match ProtonVPN.
BREAKING CHANGE: cookie-file auth removed; run once with `enable: true` to approve a device code.
Optional ranking of forced/normal/sdh per language, mirroring audio.codec_priority; first track gets the default flag, so this also picks the default subtitle. Unset = existing behavior.