Change the default audio and subtitle streams for items in Plex per user based on codec, language, keywords and more. Customizable with filters and groups. Can run on a schedule or for newly added items using a Tautulli webhook.
services:
defaulterr:
image: varthe/defaulterr:latest
container_name: defaulterr
hostname: defaulterr
ports:
- 3184:3184
volumes:
- /path/to/config:/config
- /path/to/logs:/logs
environment:
- TZ=Europe/London
- LOG_LEVEL=infoForks hosted on GitHub can use the included Build and publish Docker image
workflow (.github/workflows/docker-publish.yml) to automatically produce and
publish container images to the GitHub Container Registry (GHCR).
- Enable GitHub Actions for your fork (required for public forks) and ensure
the repository has permission to publish packages (Settings → Actions →
General → Workflow permissions → "Read and write" for the
GITHUB_TOKEN). - Optionally adjust the workflow's
IMAGE_NAMEenvironment variable if you prefer a different image name thandefaulterr. - Push to
main, create a tag (e.g.v1.2.3), or run the workflow manually from the Actions tab—builds on branches and tags are automatically pushed toghcr.io/<owner>/<image>with sensible tags (branch, tag, commit SHA, andlatestformain). - Update your deployment manifests to reference the published image (for
example
ghcr.io/<owner>/defaulterr:latest).
Pull requests still build the image but skip the push step, allowing verification without publishing.
Click here to download the Unraid template.
Your configuration is defined in config.yaml. Below is a breakdown of the required settings and optional configurations.
See config.yaml for an example of an implementation.
- plex_server_url: Your Plex server URL.
- plex_owner_name: Used to identify the owner, allowing them to be included in groups.
- plex_owner_token: The server owner's token.
- plex_client_identifier: Find this value using the instructions below.
- Go to
https://plex.tv/api/resources?X-Plex-Token={your_admin_token}(replace{your_admin_token}with your token). - Search for your server and find the
clientIdentifiervalue. This HAS TO be the server's identifier, not the owner's.
- dry_run: Set to
Trueto test filters. This mode won't update users and is recommended to verify that your filters work correctly. It overwrites other run settings. You can also enable it at runtime with--dry-runorDRY_RUN=1(CLI options override environment variables, which override config values). - partial_run_on_start: Set to
Trueto do a partial run on application start.- WARNING: The first run may take a LONG time to complete as it will update all existing media. Subsequent runs will only update any new items added since the last run.
- partial_run_cron_expression: Specify a cron expression (e.g.,
0 3 * * *for daily at 3:00 AM) to do a partial run on a schedule. You can create and check cron expressions at https://crontab.guru/. - clean_run_on_start: Set to
Trueto update all existing media on application start. Should only be used if you want to re-apply a new set of filters on your libraries. - skipInaccessibleItems: Set to
Trueto skip per-user updates that return HTTP 403 because the user can't access the item. When enabled the run will continue, log skip counts per user, and finish with exit code0. Defaults toFalse. You can also enable it via theSKIP_INACCESSIBLE_ITEMSenvironment variable.
Diagnostics are opt-in and disabled by default so an upgrade preserves historic logging behaviour. Enable only what you need for a run—the CLI flag always wins over an environment variable, which in turn overrides the configuration file.
| CLI flag | Environment variable | Default | Description |
|---|---|---|---|
| `--log-user-summary[=true | false]` | LOG_USER_SUMMARY |
false |
| `--log-json-user-summary[=true | false]` | LOG_JSON_USER_SUMMARY |
false |
--audit-dir=<path> |
AUDIT_DIR |
disabled | Write per-run audit reports to the given directory (JSON + CSV). Files are created as run-YYYYMMDD-HHMMSS.json / .csv. |
--dry-run |
DRY_RUN |
false |
Shortcut to enable dry-run mode without editing the config file. |
When summaries are enabled, two formats are available:
-
Human-readable summaries are compact and optimized for console review:
[INFO]: User updates (group=noLossless, part=23614, title='1917'): keltonsnyder: audio='AC3 5.1' (id=12345) [success] rossni6: [skipped: HTTP 403] -
JSON summaries are single-line payloads that parse cleanly with
jqor any JSON parser:[INFO]: {"event":"user_updates","group":"noLossless","partId":23614,"title":"1917","library":"Movies Home","users":[{"name":"keltonsnyder","audio":{"id":12345,"label":"AC3 5.1"},"status":"success"},{"name":"rossni6","status":"skipped","reason":"HTTP 403"}]}
The JSON mode includes every field from the human summary plus HTTP status codes when available, and the owner account is omitted unless explicitly placed in a group. Startup also emits a digest and owner-safety notice so operators can confirm membership ahead of the run:
[INFO]: Group digest (library='Movies Home'):
- noLossless: members=keltonsnyder,davidantillon,rossni6 (3)
resolvedTokens: 3/3 ok; restrictedAccess: 0 (updates skipped for missing tokens)
[INFO]: Owner 'Tristan' is not included in any group; no owner updates will be performed.
Providing --audit-dir (or setting AUDIT_DIR) enables structured per-run audit exports. Each run writes two files—run-YYYYMMDD-HHMMSS.json and run-YYYYMMDD-HHMMSS.csv—with one row per (group × user × item × actionType) attempt.
| Field | Description |
|---|---|
timestamp |
ISO timestamp when the attempt started. |
libraryName |
Library containing the media item. |
ratingKey / partId |
Plex identifiers for the item and part being updated. |
title |
Human-friendly title of the item. |
group / user |
Configured group and username being updated. |
actionType |
audio, subtitles, or audio+subtitles depending on the streams touched. |
fromStreamId / fromLabel |
Previous stream identifiers and labels (if known). |
toStreamId / toLabel |
Target stream identifiers and labels requested. |
status |
success, skipped, error, or dry_run. |
reason |
Short diagnostic (e.g. HTTP 403, no_token). |
httpStatus |
HTTP response code when available. |
durationMs |
Time spent on the attempt. |
The JSON export is a simple array of objects with these fields; the CSV uses the same header order for quick spreadsheet imports. Tokens are never written in full—any time a token surfaces in logs it is masked to ***…LAST6.
Managed accounts remain supported and keep the same configuration syntax as before. Diagnostics help confirm that Defaulterr updates only the accounts you intend, but Plex itself stores some preferences per underlying account. If multiple managed profiles reuse a single token, Plex may still apply changes across them. The audit trail and per-user headers make the script’s actions transparent so you can distinguish Defaulterr activity from Plex-side behaviour.
Groups define collections of users with shared filters:
- Usernames must match exactly as they appear in Plex, including capitalization and special characters.
- Managed accounts require additional setup. Read below.
- Optionally, use
$ALLin place of a username to include all users from your server.
Example:
groups:
serialTranscoders: # Can be named anything
- varthe
- UserWithCapitalLetters # EXACTLY like in Plex
- $ALL # Grabs all users from the server
subtitleEnjoyers: # Can be named anything
- varthe
deafPeople: # Can be named anything
- varthe
weebs: # Can be named anything
- vartheTo include managed accounts in groups you will need to supply their tokens manually. See this comment by Blind_Watchman on how to obtain their tokens. You have to do it like this because the regular tokens won't work.
Include them in the config file like below. Use the key (e.g user1) in groups.
managed_users:
user1: token
user2: tokenImportant: Plex keeps default audio/subtitle selections per Plex account, not per managed profile. If multiple entries share the same token (for example, the owner and the managed profiles underneath it), any update for one will affect all of them. Defaulterr logs a warning when it detects token reuse so you know shared preferences are in play. Some Plex clients may also display managed profile preferences as if they carry across profiles—double-check the audit trail if behaviour looks unexpected.
Filters define how audio and subtitle streams are updated based on specified criteria. The structure in config.yaml is as follows:
- Library Name: The filter applies to a specific Plex library.
- Group Name: Defines which group the filter targets.
- Stream Type: Can be
audioorsubtitles.- include: Fields that MUST appear in the stream AND include the specified value
- exclude: Fields that MUST NOT appear in the stream OR not be the specified value
- on_match: Specifies filters for the other stream type if a match is found. For example, disable subtitles if a Spanish audio track is matched. Otherwise find Spanish subtitles.
- Stream Type: Can be
- Group Name: Defines which group the filter targets.
Note: Any field (e.g.,
language,codec,extendedDisplayTitle) can either be a single value or an array of values. This allows flexibility in filtering criteria by matching multiple options when needed.
Multiple groups and filters can be defined per library, with the first matching filter being applied. If no filters match, the item remains unchanged in Plex. Filters can utilize any property in the stream object returned by Plex. See example.json for examples.
filters:
Movies: # Library name
serialTranscoders: # Group name
audio:
# Audio Filter 1 - First English audio track that's not TRUEHD/DTS and not a commentary
- include:
language: English # Needs to be in the original language, e.g Español for Spanish
# languageCode: eng # Alternative to the above, e.g. jpn for Japanese
exclude:
codec:
- truehd
- dts
extendedDisplayTitle: commentary
# Audio Filter 2 - Any English track (fallback if the above filter doesn't match)
- include:
language: English
subtitleEnjoyers:
subtitles:
# Subtitle Filter 1 - First English track that's not forced
- include:
language: English
exclude:
extendedDisplayTitle: forced
deafPeople:
subtitles:
# Subtitle Filter 1 - First English SDH track
- include:
language: English
hearingImpaired: true # SDH
Anime: # Library name
weebs: # Group name
audio:
# Audio Filter 1 - First English track with disabled subtitles
- include:
language: English
on_match:
subtitles: disabled # Set subtitles to "off" in Plex
# Audio Filter 2 - Japenese track with English subtitles
- include:
languageCode: jpn # Japenese
on_match:
subtitles:
# Full subtitles -> Dialogue subtitles -> Anything without the word "signs"
- include:
language: English
extendedDisplayTitle: full
- include:
language: English
extendedDisplayTitle: dialogue
- include:
language: English
exclude:
extendedDisplayTitle: signsThese diagnostics are considered production-ready when the following conditions are met:
- Upgrading without new flags preserves legacy logging output and runtime behaviour.
- Enabling the logging flags produces the documented human and JSON summaries with valid JSON payloads.
- Startup digests list the correct members, token resolution counts, and owner-safety notice, with missing tokens warned once per run.
- Audit exports create JSON and CSV files with one row per attempt and the schema shown above.
- All sensitive tokens are masked everywhere outside of configuration storage.
- Per-user HTTP requests always include the intended token and never reuse a default/owner credential for another account.
- Automated tests cover success, dry-run, missing token, HTTP 403, JSON validation, audit I/O, and per-request header isolation.
Operators who prefer a hands-on validation can follow this checklist after deploying an update:
- Baseline run – Execute a small partial run with diagnostics flags left at their defaults to confirm logs match the pre-upgrade format.
- Human summaries – Re-run with
--log-user-summaryto confirm the concise per-item summaries list the expected users and statuses. - JSON summaries – Add
--log-json-user-summaryand pipe the log throughjq '.'to validate that each JSON line parses cleanly and contains the documented keys. - Audit exports – Enable
--audit-dir=/tmp/audit-test, process a few items, and spot-check that the CSV/JSON row counts match the attempted updates and that tokens appear masked in any warnings. - Session isolation – Temporarily supply an invalid token for a single user and confirm only that user is skipped (with
HTTP 403orno_token), while other users continue succeeding.
To automate filter applications for newly added items:
- Go to Settings -> Notifications & Newsletters in Tautulli.
- Set Recently Added Notification Delay to 60 (increase if notifications are firing too early).
- Navigate to Settings -> Notification Agents.
- Add a new notification agent and select Webhook.
- Use the Defaulterr URL:
http://defaulterr:3184/webhook. - Choose POST for the Webhook Method.
- Enable the Recently Added trigger.
- Paste the following JSON data into JSON Data:
<movie>
{
"type": "movie",
"libraryId": "{section_id}",
"mediaId": "{rating_key}"
}
</movie>
<show>
{
"type": "show",
"libraryId": "{section_id}",
"mediaId": "{rating_key}"
}
</show>
<season>
{
"type": "season",
"libraryId": "{section_id}",
"mediaId": "{rating_key}"
}
</season>
<episode>
{
"type": "episode",
"libraryId": "{section_id}",
"mediaId": "{rating_key}"
}
</episode>