Skip to content

Configuration

Every setting QualityGate stores, what it does, and which ones do not restrict playback.

Configuration lives in config/plugins/configurations/Jellyfin.Plugin.QualityGate.xml on the server, outside the plugin folder. That matters: it survives the plugin being removed, reinstalled or purged. An unloaded plugin returns an empty policy list from the API, which looks like data loss and is not.

Fields that do not restrict playback

Read this first. It is the single thing most likely to waste your afternoon.

QualityGate started as a filename-pattern filter. In 3.4.0.0 the port to Jellyfin 12 stopped registering MediaSourceResultFilter, the component that read those patterns. The class is still in the assembly and the fields are still in the config page, but nothing calls them any more.

Field Status
AllowedFilenamePatterns Does not restrict playback
BlockedFilenamePatterns Does not restrict playback
FallbackTranscode Does not change how media is delivered
FallbackMaxHeight No effect at all
FallbackMaxBitrateKbps No effect at all
BlockedMessageHeader No effect at all
BlockedMessageText No effect at all
BlockedMessageTimeoutMs No effect at all

The filename patterns and FallbackTranscode are not entirely unread. The intro provider still consults them to decide whether to skip an intro video, on the theory that playing an intro before a refusal is a poor experience. They influence intros and nothing else.

So a policy that blocks - 2160p by filename pattern and sets no maximum resolution restricts nobody. If you are migrating from 3.3.x or earlier, translate your patterns into a Maximum Resolution value.

Fields that work

Plugin-wide

Field Type Default Effect
Policies list empty The policies you define
UserPolicies list empty Which user gets which policy
DefaultPolicyId string empty Policy for users with no explicit assignment. Empty means unrestricted
DefaultIntroVideoPath string empty Intro played when a user's policy names none
ApiKeyPolicyId string empty Policy applied to API-key and anonymous requests, which carry no user. Empty leaves them uncapped
EnableVersionGrouping bool false Group an encoded copy with its original. See one library, two qualities
VersionGroupingRoots list empty Library paths where grouping applies. Empty means every movie library
VersionGroupingSuffixes list empty Suffixes that mark a file as an encoded copy. Empty means " - 720p"

Encode priority

Off by default. See encode priority for what it does and how to set it up. The list is read by quality-gate-encoder 1.5.4 or newer. None of these fields changes playback.

Numbers outside their range are clamped when a run reads them, and an unknown value in a string field falls back to its default. Each correction is logged once as a warning.

Field Type Default Range Effect
EnableEncodePriority bool false Master switch. When off, nothing is built and lists the plugin wrote earlier are removed
EncodeTargets list empty 0 to 16 used One entry per encoder, described below
PriorityNowPlaying bool true Rank items a capped viewer is playing right now
PriorityContinueWatching bool true Rank each capped viewer's resumable items
PriorityContinueWatchingPerUser int 20 1 to 100 Most resumable items per viewer
PriorityNextUp bool true Rank the next episodes of shows a capped viewer is following
PriorityNextUpDepth int 3 1 to 20 Episodes ahead per show, counting the next-up episode
PriorityNextUpShowsPerUser int 10 1 to 50 Most shows per viewer, most recently played first
PriorityNextUpIncludeSpecials bool false Include season 0 in the lookahead
PriorityFavourites bool false Rank favourite films and the next episodes of favourite shows
PriorityFavouritesPerUser int 25 1 to 200 Most favourites per viewer. Each favourite show costs up to about 3 queries per run (its Next up, a first-unplayed fallback and a lookahead), so a high value with many viewers can hit the run budget
PriorityWatchedWithinDays int 30 1 to 365 Only activity in the last N days counts; idle viewers are skipped
PriorityUnprobedNeedsEncode bool false Treat a version with no known height as over the cap when building the list. Playback still treats it as within the cap
PriorityRefreshOnPlayback bool true Queue a run after a capped viewer starts playback
PriorityDebounceMinutes int 10 1 to 240 Minimum gap between playback-triggered runs
PriorityRunBudgetSeconds int 180 10 to 1800 Longest a run may take. A run that goes over writes nothing and keeps the previous lists

Per encoder (EncodeTargets)

Field Type Default Range Effect
Id string empty (the page mints a GUID) unique, required Stable key for state and logs. Its first 8 characters name the Data folder file. A target with no id is ignored with a warning until it is saved from the page
Name string empty 1 to 64 characters, unique Label on the page, in the logs and in the file
Enabled bool true A disabled encoder gets no list, and its old file is removed
Folders list empty at least one when enabled Folder mappings, described below
OutputHeight int 720 144 to 4320 The height the encoder produces
OutputMode string SourceFolder SourceFolder, DataFolder, Custom Where the file goes
OutputPath string empty absolute The file, as Jellyfin sees it. Custom only; a file inside a library must be a dotfile
AudienceMode string Auto Auto, Policies, Users Who counts: capped viewers this encoder can help, only viewers on chosen policies, or chosen users even if uncapped
AudiencePolicyIds list empty at least one in Policies mode Policies whose viewers count
AudienceUserIds list empty at least one in Users mode Users who count. An uncapped one is treated as capped at the output height
ExcludedUserIds list empty Users who never count
ResolveSymlinks bool false Resolve each file to its final target before mapping
MaxEntries int 300 1 to 5000 Longest list written
DryRun bool false Build and report, write no file. An earlier file is removed

Per folder (Folders)

Field Type Default Effect
JellyfinPath string empty The folder as Jellyfin sees it. Must be absolute
EncoderPath string empty Where that folder sits inside the encoder's source folder: relative, / separators, no ... Empty means the source folder itself

A folder whose Jellyfin path is not absolute, or whose encoder path is absolute, contains a \ or contains .., is ignored by a run and logged. Two encoders may share a folder. The page does not let one encoder list overlapping folders; a hand-edited config that does is mapped through the most specific one.

Per policy

Field Type Default Effect
Id string new GUID Identifier referenced by assignments
Name string empty Shown in the admin page and written to the server log
Description string empty Your own note
MaxHeight int 0 The cap. Maximum video height in pixels. 0 disables enforcement
Enabled bool true A disabled policy does not resolve. See the warning below
IntroVideoPath string empty Intro for users under this policy
KeepWithinCapVersionsDirect bool false Shown as Play within-cap versions as they are. A version within MaxHeight is not transcoded when a client's bitrate limit is the only reason; a codec, container, audio or subtitle reason still transcodes it. See how it works

MaxHeight is measured against the item's actual video stream height, never its filename. Rename a file and the cap is unchanged. That is deliberate: on a library whose lower-quality tree symlinks to originals under their original names, the filename carries no quality information at all.

Per user

Field Effect
UserId The Jellyfin user
Username Display only
PolicyId A policy Id, or __FULL_ACCESS__ for explicitly unrestricted, or empty to fall through to the default

How a user's policy is chosen

A request that resolves to no user at all, which is what an API key and an unauthenticated request both produce, does not enter this table. It takes ApiKeyPolicyId, or no policy when that is unset. An ApiKeyPolicyId naming a missing or disabled policy gets the deny-all sentinel, the same as a broken assignment. See how it works.

explicit assignment for this user?
├── __FULL_ACCESS__      -> unrestricted
├── a policy id           -> that policy, if it exists AND is enabled
│                            otherwise the deny-all sentinel (see below)
└── empty                 -> fall through
no assignment?
├── DefaultPolicyId set   -> that policy, if it exists AND is enabled
│                            otherwise the deny-all sentinel
└── not set               -> unrestricted

A policy that cannot be found denies

When an assignment, DefaultPolicyId or ApiKeyPolicyId points at a policy that was deleted, disabled or mistyped, the caller gets an internal deny-all sentinel and is refused playback. PlaybackInfo, GET or POST, is answered before Jellyfin negotiates anything, with no media sources and the error NotAllowed, which clients show as not being allowed to play the item. Every delivery route the filter gates answers 403. No intro plays either. The admin page's user table labels these users Denied (invalid policy) or DENIED (invalid default), and deleting or disabling a policy asks first, saying how many users lose playback.

Two Live TV routes are not among the gated delivery routes: /LiveTv/LiveStreamFiles/{id}/stream.{container} and /LiveTv/LiveRecordings/{id}/stream. A client that calls them directly is not refused. jellyfin-web only reaches them through PlaybackInfo, which is.

The server log says why, once per user and policy id each time Jellyfin starts:

QualityGate: policy '<policy id>' named by the assignment does not exist or is disabled — refusing playback for user <user id> until it points at an enabled policy

assignment reads default policy or API key policy when that is the setting at fault.

To give such a user playback back, point the assignment at an enabled policy, at __FULL_ACCESS__, or clear it so the user falls through to the default.

Up to 3.9.0.1 the sentinel carried no maximum resolution, so the resolution cap read it as unrestricted: deleting or disabling a policy gave its users full access, and a missing API-key policy left those requests uncapped. Upgrading with such a dangling reference takes playback away from the users it names.

Checking the live configuration

The admin page is not the only view, and it is not the one to trust when you are debugging. Ask the server:

curl -s -H 'Authorization: MediaBrowser Token="<admin-token>"' \
  'https://your-server/Plugins/9cab70ca0af34d3aadab6a0df2496a33/Configuration' \
  | python3 -m json.tool

Jellyfin 12 accepts only that header form. X-Emby-Token and ?api_key= both return 401, which from outside looks the same as the plugin being absent.

Two things to look at. Policies should contain your policy with the MaxHeight you expect, and Enabled true. UserPolicies should have an entry per capped user whose PolicyId matches a policy that is present in that same response. An assignment pointing at an id that is not in Policies is the gap described above.