Plugin for Dispatcharr that provides greater control over which VOD stream it serves through its proxy for a given title — control not currently exposed to clients. It adds two independent, composable features:
- Prefer 4K — re-order a title's provider streams by quality before serving, so the 4K copy wins even if a lower-quality provider has higher account priority. Stream quality is determined by measured video resolution, then the stream name, then the provider name, in that order of priority.
- Remember my UI pick — when you play a specific stream from the Dispatcharr UI, save it as that title's default so the proxy serves the same choice next time. Useful when the default stream for a title is broken in some non-obvious way. Movie choices are saved at the stream level; series choices are saved at the provider level (with finer control available via "Prefer 4K" where needed — see below for Dispatcharr's inherent limitations on remembering stream-level picks for series).
You still get Dispatcharr's provider slot management and failover; this plugin
only re-orders the candidate list Dispatcharr already built. It works with a
single, reload-safe monkeypatch of views._get_content_and_relation — the one
function reached on both the UI /proxy/vod/…?stream_id=… path (how a pick is
captured) and the Xtream-Codes path clients play through (where preferences are
applied). See
DESIGN.md for the rationale and patch.py for the code.
Targets current Dispatcharr's VOD proxy. If the internals it patches change — or a request hits an unexpected error — it falls back to native selection (account priority) rather than breaking playback. No user data leaves the box.
- Download
dispatcharr_vod_preferences.zipfrom the latest release. - Dispatcharr UI → Plugins → Import → upload the zip.
- Toggle the plugin enabled (accept the trust warning — plugins run server-side code).
- Restart the Dispatcharr container (see "Why restart?" below).
- Clone or copy this repo into
data/plugins/dispatcharr_vod_preferences/on the host (→/app/data/plugins/…in the container). The folder must be nameddispatcharr_vod_preferencesand containplugin.json,plugin.py, andpatch.py. (The folder name matters — the plugin stores its saved picks under a key derived from it.) - UI → Plugins → click reload (or
POST /api/plugins/plugins/reload/). - Enable the plugin, then restart the container.
Dispatcharr runs multiple uWSGI workers with lazy-apps = true. Each worker
imports an enabled plugin's code at boot and applies the monkeypatch then.
Enabling without a restart only reliably patches the worker that handled the
enable request; a restart patches all of them.
Plugins page → Dispatcharr VOD Preferences → Settings tab:
- Prefer quality —
Off/Prefer 4K. DefaultOff. (Only these two are exposed: on real provider data sub-4K streams carry no resolution label, so a "Prefer 1080p" option would be misleading. The full tier ladder still lives in the code if a provider ever labels sub-4K.) - Remember my UI pick — on/off (default on). Governs both capturing new picks and applying stored ones.
- Title key to clear — a text box used only by the Clear one action below.
Actions tab:
- Check status — is the patch live in this worker, plus current config and saved-pick count.
- List saved picks — dump the remembered map (
title key → provider/stream). - Clear one — type a pick's key (from List saved picks, e.g.
tmdb:954orseries:…, or a bare tmdb/imdb id) into the Title key to clear box on the Settings tab, then click this. - Clear all — wipe every saved pick (quality preference untouched).
For every VOD request the plugin calls the native selector, then re-decides, most-specific-first:
| # | Rule | When it fires |
|---|---|---|
| 1 | Explicit request pick | The request named a specific stream (stream_id, movie UI play) or provider (m3u_account_id, series/episode UI play). Passed through untouched; if Remember my UI pick is on, it's saved as this title's default. |
| 2 | Saved UI pick | A remembered pick for this title. Movies pin the exact (provider, stream). TV pins the provider for the whole series and still applies the quality rule within that provider (so Prefer 4K picks the 4K copy when one provider carries both). Dropped automatically if the provider no longer carries the title. |
| 3 | Quality rule | Prefer quality is set. Candidates are stable-sorted by quality; the best becomes primary and failover follows quality order. |
| 4 | Native | None of the above — untouched account priority. |
Streams with no quality signal keep their native account-priority order (the sort is stable), so the plugin never reshuffles titles it has no opinion about, and Dispatcharr's failover still walks the full candidate list.
Quality signal. Each stream is ranked by a waterfall of per-stream signals.
Actual video dimensions are the top signal — ground truth that outranks any
text label, so a stream mislabeled 4K whose real track is 1920×1080 ranks as
1080p:
- Real video pixel dimensions (movies:
custom_properties.detailed_info; episodes:info.info). Attached cover images (PNG/JPEG posters) are skipped so a 4K episode's 1920×1080 poster isn't misread as its resolution. Often absent until an advanced/detailed refresh has populated it. custom_properties['quality']/['resolution']— explicit provider label (rarely populated on a normal instance).- The provider stream name (movies:
basic_data.name; episodes:info.title) — the reliable signal when dims are absent and the provider puts4K/2160pin the title. - The provider/account name (
m3u_account.name) — catches the "separate 4K provider" setup (an account named e.g.… 4K), and is often the only 4K marker for episodes whose titles don't carry it.
Matching is word-boundary based, so free-text names don't misfire
(Wednesday isn't read as SD, 24K Gold isn't read as 4K).
Because the account-name rung exists, avoid putting a bare
HD/SDin an M3U account name unless you mean it.4K,1080p,720p,480pare the safe, unambiguous tokens.
Movies vs TV — an asymmetry rooted in the UI. A movie play sends the exact
stream_id, so movie picks pin the exact stream. A series play sends only the
m3u_account_id (the Series UI has no per-episode stream id), so TV picks are
provider-granular: they remember which provider to use for the show, and lean
on Prefer 4K to choose quality within that provider. Consequence: to get 4K for
a show whose 4K and non-4K copies live on the same account, Prefer 4K must
be on — the saved pick supplies the provider, Prefer 4K supplies the quality.
- UI → Plugins → toggle off (Dispatcharr calls the plugin's
stop(), which reverts the monkeypatch in that worker and deactivates it in the rest). - For a clean, guaranteed revert across all workers, restart the container after disabling.
- Saved picks survive a delete. They live in their own
CoreSettingsrow (not the plugin's config), so deleting the plugin leaves them (and reuses them if you reinstall). Click Clear all first if you want them gone. No changes are made to Dispatcharr's own VOD tables.
Each worker logs once at boot / first hit (INFO — always visible):
[VOD-PREF] installed VOD preferences patch in worker pid=<PID>
[VOD-PREF] active in worker pid=<PID> at select
After enabling + restarting, collect the distinct pid= values across a few
plays. You should see more than one worker PID. If you only ever see one, the
patch isn't in every worker — restart again and re-check. (The Check status
button only reports the one worker that handled that click, so the logs are the
real confirmation.)
Each selection logs one line — but at DEBUG level, so it's quiet by default.
To watch it, set DISPATCHARR_LOG_LEVEL=DEBUG in the container's environment and
restart, then:
docker logs -f <dispatcharr-container> 2>&1 | grep "VOD-PREF"A decision line reads:
[VOD-PREF] episode tmdb:12345: quality:4k -> account 7 stream 1978843 (tier=4k, changed=True, candidates=5)
reason— which rung fired:request-pick,saved-pick,quality:4k,quality:4k:no-signal, or nothing logged =native.changed=True— the plugin moved off the native primary (it did real work);changed=False— its choice already matched native (still confirmation it ran).candidates=N— how many streams the title has right now (see the episode staleness note below if this looks low).
changed=False proves the rule ran; to prove it can override priority, force a
conflict:
- Temporarily set the 4K provider(s) to a lower account priority than the non-4K one(s), so native would pick a non-4K stream.
- Prefer quality = Off, play a title with both → serves the non-4K stream.
- Prefer quality = Prefer 4K, play again → serves the 4K stream with
changed=True, tier=4k. - Revert the priorities.
- Play a specific stream (movie) or provider (series) from the Dispatcharr UI →
List saved picks shows a new entry (
tmdb:…for a movie,series:…for a show). - Play the same title through a client (e.g. Emby) → the log shows
saved-pickand it serves your remembered choice.
python test_logic.pyExercises the full ladder, the quality-signal waterfall (dims-first, cover-image exclusion, name/account fallbacks, word-boundary guards), movie exact picks, TV show-level picks + same-account 4K composition, capture via stream_id/account_id, persistence, and clear operations. No Dispatcharr or DB required.
-
Operates within one title — can't reach an unmerged sibling. The plugin re-orders the streams of a single
Movie/Episode. If a provider's 4K copy of a show is a separate, unmerged title in Dispatcharr (different or absent tmdb/imdb, so dedup didn't fuse them), it's on a different content row and will never appear in this episode's candidate list. That's a Dispatcharr merge/metadata matter (the two need to share a tmdb), not something re-ordering can fix. Tell-tale:candidates=1with a version you expected missing. -
TV episode staleness — the candidate list is only as fresh as Dispatcharr made it. Dispatcharr does not refresh episode streams on its own: the VOD provider refresh only re-scans listings, and per-episode data is fetched lazily (via the XC
get_series_infoaction / opening the series in the UI), gated to once per 24h. So a provider that adds a 4K stream later can be invisible — the plugin faithfully serves the best of whatever streams currently exist, which may be a stale single 720p. This is the most impactful gotcha for the TV side; keep episodes fresh by (a) having your library/.strmtooling callget_series_infofor curated series, or (b) scheduling Dispatcharr's (otherwise dormant)batch_refresh_series_episodestask. Movies are unaffected (their streams are part of the listing scan). -
TV picks are provider-granular. The Series UI conveys only the account, not a stream, so a saved TV pick can't distinguish two same-account, same-quality streams (e.g. one wrong-aspect-ratio 1080p vs another 1080p). Same-account 4K vs non-4K is recovered via Prefer 4K; a same-quality distinction isn't expressible for TV.
-
Coverage. Applies to VOD selection on the proxy path (movies + episodes), reached from both the UI and the Xtream-Codes (XC) play path. Live TV, EPG, and DVR are not touched. The XC play path never captures a pick (its redirect carries no
stream_id/m3u_account_id), only applies — capture happens on a stream-specific UI play. -
Fail-open. If Dispatcharr's internals don't match at install, the plugin doesn't patch (native behaviour intact). If the ladder ever errors on a request, it falls back to native selection for that request. Disable / delete / reload reverts the patch via
stop().
- Saved picks: a dedicated
CoreSettingsrow,key = dispatcharr_vod_preferences_picks, value{ title_key: {…} }. Kept out of the plugin's own settings on purpose — the Plugins UI re-saves the whole settings blob before every action, which would otherwise clobber picks written server-side. Movie keys:tmdb:→imdb:→as:<account>:<stream>. TV keys (per series):stmdb:→simdb:→series:<uuid>. Picks from a pre-1.0 install migrate across automatically on first write. - Settings (
prefer_quality,remember_ui_picks,clear_key): the plugin's normalPluginConfig.settings.
Dispatcharr's own VOD tables are never modified.
Runs cleanly alongside the
VOD Concurrency Fix
plugin — that one touches stream_vod / profile reservation, this one only
re-orders the relation list. Disjoint functions, verified running together. This
plugin decides which provider/stream backs a title; the concurrency fix keeps
the resulting playback from failing over across providers mid-burst.
Designed and built by andyj682 with Claude (Anthropic) as a pair-programming collaborator — Dispatcharr code analysis, selection design, and implementation.