plex2jellyfin
Reference

Configuration

Complete config.toml reference for local and container deployments.

Config file: ~/.config/plex2jellyfin/config.toml on bare metal, /config/.config/plex2jellyfin/config.toml in Docker (see Docker).

A starter template ships as config.toml.example. For a file that matches the current schema (including defaults), generate:

plex2jellyfin config init

This page documents every key config init / Config.ToTOML() emits, plus a few loadable extras noted where they matter. Defaults below are from DefaultConfig unless stated otherwise.

[setup]

Wizards write this block; you normally do not edit it.

KeyTypeDefaultDescription
versionint0 until a wizard runsSetup schema version.
completedboolfalsetrue after CLI/web/TUI setup finishes. Web then skips guided setup (password creation only).

Completing setup on any surface completes it for all. The web server adopts older configured installs that predate this block. To re-run web guided setup, set completed = false and restart plex2jellyfin-web.

[watch]

KeyTypeDescription
moviesstring[]Download drop dirs for movies.
tvstring[]Download drop dirs for TV.
[watch]
movies = ["/path/to/downloads/movies"]
tv = ["/path/to/downloads/tv"]

[libraries]

KeyTypeDescription
moviesstring[]Destination movie library roots.
tvstring[]Destination TV library roots.
[libraries]
movies = ["/path/to/jellyfin/Movies"]
tv = ["/path/to/jellyfin/TV Shows"]

Both [watch] and [libraries] accept multiple paths for multi-drive setups.

[sonarr] / [radarr]

KeyTypeDefaultDescription
enabledboolfalseEnable *arr API notifications.
urlstring""Base URL (e.g. http://localhost:8989).
api_keystring""From Settings → General → API Key.
notify_on_importbooltrueTell *arr about files the organizer moved.
[sonarr]
enabled = false
url = "http://localhost:8989"
api_key = ""
notify_on_import = true

[radarr]
enabled = false
url = "http://localhost:7878"
api_key = ""
notify_on_import = true

[jellyfin]

KeyTypeDefaultDescription
enabledboolfalseEnable Jellyfin API / feedback loop.
urlstring""Jellyfin base URL.
api_keystring""From Dashboard → API Keys.
notify_on_importbooltrueTrigger library refresh after organizes.
playback_safetybooltrueBlock moves while media is actively streamed.
verify_after_refreshboolfalseQuery Jellyfin after refresh to verify identification.
webhook_secretstring""Shared secret; webhook requests must send X-Plex2Jellyfin-Webhook-Secret.
plugin_enabledboolfalseCompanion plugin expected / configured.
plugin_shared_secretstring""Usually the same value as webhook_secret.
plugin_auto_scanbooltrueAsk Jellyfin to scan after organizing.
plugin_verify_on_startupboolfalseRun plugin verification when the daemon starts.
plugin_verify_intervalint0Hours between automatic verifications (0 = disabled).
plugin_daemon_urlstring""Base URL the plugin calls back to (appends /api/v1/webhooks/jellyfin). Must be reachable from Jellyfin; never localhost when either side is containerized.
[jellyfin]
enabled = true
url = "http://localhost:8096"
api_key = "..."
notify_on_import = true
playback_safety = true
verify_after_refresh = false
webhook_secret = "..."
plugin_enabled = true
plugin_shared_secret = "..."
plugin_auto_scan = true
plugin_verify_on_startup = false
plugin_verify_interval = 0
plugin_daemon_url = "http://192.168.1.10:5522"

[[jellyfin.path_mappings]]

KeyTypeDescription
jellyfinstringPath prefix as Jellyfin reports it (often container-internal).
daemonstringSame root as Plex2Jellyfin sees it (host / [libraries] path).
[[jellyfin.path_mappings]]
jellyfin = "/tv5"
daemon   = "/mnt/STORAGE5/TVSHOWS"

[[jellyfin.path_mappings]]
jellyfin = "/movies"
daemon   = "/mnt/STORAGE2/MOVIES"

Longest-prefix match. Without mappings when paths diverge: organizes still succeed, but webhooks never match parse_decisions, jellyfin_item_id stays empty, PASS/DRIFT/FAIL never runs, and the sweeper eventually marks uncorrelated rows FAIL. Full guide: Path mappings.

[metadata_recovery]

Passive recovery checks Jellyfin for metadata that arrives after import. Active repair asks Jellyfin to refresh items (off by default).

KeyTypeDefaultDescription
passive_enabledbooltruePeriodically reconcile missing metadata from Jellyfin.
repair_enabledboolfalseRun MetadataReconciler.RunRepair on a timer (parse-decision active repair). Compatibility: before 2026-08 this gated unknown-season refreshes.
unknown_season_repair_enabledboolfalsePeriodically refresh Jellyfin series for strict Season Unknown candidates (every null-index episode has SxxEyy evidence). Default off; dogfood via CLI dry-run / --execute --max-refresh 1 first.
passive_interval_minutesint60Minutes between passive passes (also used by repair timers).
passive_batch_sizeint25Max items per passive pass.
repair_batch_sizeint5Max items / series per repair pass.
repair_cooldown_hoursint6Hours before re-repairing the same parse-decision item or unknown-season series.
needs_review_afterint4Failures before an item is marked needs-review.

[daemon]

KeyTypeDefaultDescription
enabledboolfalseWhether the daemon’s watcher / periodic scan are considered enabled in config (wizards set true on activate).
scan_frequencystring"5m"Convergence re-scan interval (Go duration, e.g. "5m", "1h").
health_addrstring":8686"Daemon health HTTP bind address.

[options]

KeyTypeDefaultDescription
dry_runboolfalsePreview flag for some CLI flows. The daemon always organizes for real and forces this off at startup.
verify_checksumsboolfalseVerify integrity after transfer before deleting the source.
delete_sourcebooltrueDelete source after a successful move (false = copy).

Loadable but not always emitted by config init: transfer_concurrency_per_volume (int; caps parallel transfers to the same destination mount; <=0 disables; unset uses the daemon default of 2).

[ai]

KeyTypeDefaultDescription
enabledboolfalseGlobal AI naming assist.
ollama_endpointstring"http://localhost:11434"Ollama URL (local or cloud).
modelstring"qwen2.5vl:7b"Primary model.
fallback_modelstring""Used if primary errors / circuit-breaks.
confidence_thresholdfloat0.8Cutoff for audit --generate candidates.
auto_trigger_thresholdfloat0.6Daemon queues AI only when confidence is below this. Deterministic movie/TV identity bypasses AI even when low.
timeout_secondsint30Per-request timeout.
cache_enabledbooltrueCache AI responses.
cloud_modelstring"nemotron-3-nano:30b-cloud"Optional cloud model name.
auto_resolve_riskyboolfalseAuto-apply suggestions flagged risky.
max_retriesint3Retries before fallback.
hourly_limitint10Max AI calls per hour.
daily_limitint50Max AI calls per day.
enhancement_interval_secondsint30Spacing between daemon AI enhancement workers.

Loadable nested tables (not emitted by ToTOML today; defaults apply if omitted):

[ai.circuit_breaker]
failure_threshold = 5
failure_window_seconds = 120
cooldown_seconds = 30

[ai.keepalive]
enabled = true
interval_seconds = 300
idle_timeout_seconds = 1800

retry_delay (duration, default 500ms) is also loadable.

[logging]

KeyTypeDefaultDescription
levelstring"info"Log level.
filestring""Log file path ("" = default under the config dir).
max_size_mbint10Rotate after this many MB.
max_backupsint5Rotated files to keep.
max_age_daysint0Delete rotated files older than this (0 = age pruning off).
compressboolfalseGzip rotated backups.

[api]

KeyTypeDefaultDescription
allowed_originsstring[]["http://localhost:3000"]CORS origins for the web UI. Same-origin production usually needs no change; reverse proxies / external dev servers do.

[permissions]

Optional. Emitted by ToTOML only when ownership/mode fields are set. Sets ownership and mode on files the daemon moves (bare metal).

KeyTypeDescription
userstringUsername or numeric UID (empty = leave owner).
groupstringGroup name or GID.
file_modestringOctal file mode (e.g. "0664").
dir_modestringOctal directory mode (e.g. "0775").

Requires root on bare metal; unavailable in Docker. The systemd unit runs the daemon as root with CAP_CHOWN / CAP_FOWNER / CAP_DAC_OVERRIDE. Inside Docker, use PUID/PGID instead — see Docker.

[jellystat]

Optional. Emitted only when enabled or a URL is set.

KeyTypeDescription
enabledboolEnable Jellystat integration.
urlstringJellystat base URL.
api_keystringJellystat API key.

Auth (top-level)

Managed by the web UI. Emitted near the top of the file when set:

KeyTypeDescription
password_hashstringbcrypt hash of the admin password. Delete to reset.
secure_cookiesboolPrefer secure cookie flags (HTTPS deployments).

Legacy plaintext password is still read and migrated to password_hash on load; new writes should not persist plaintext.

Loadable extras (not in ToTOML)

Section / keyNotes
[tmdb] enabled, api_keyOptional TMDB fallback for housekeeping verification.
[options].transfer_concurrency_per_volumeSee [options] above.
[ai].circuit_breaker / keepalive / retry_delaySee [ai] above.

Full example

[setup]
version = 1
completed = true

[watch]
movies = ["/downloads/movies"]
tv     = ["/downloads/tv"]

[libraries]
movies = ["/media/Movies"]
tv     = ["/media/TV Shows"]

[daemon]
enabled        = true
scan_frequency = "5m"
health_addr    = ":8686"

[options]
dry_run          = false
verify_checksums = false
delete_source    = true

[ai]
enabled                      = true
ollama_endpoint              = "http://localhost:11434"
model                        = "qwen2.5vl:7b"
fallback_model               = ""
cloud_model                  = "nemotron-3-nano:30b-cloud"
confidence_threshold         = 0.8
auto_trigger_threshold       = 0.6
timeout_seconds              = 30
cache_enabled                = true
auto_resolve_risky           = false
max_retries                  = 3
hourly_limit                 = 10
daily_limit                  = 50
enhancement_interval_seconds = 30

[sonarr]
enabled          = true
url              = "http://localhost:8989"
api_key          = "..."
notify_on_import = true

[radarr]
enabled          = true
url              = "http://localhost:7878"
api_key          = "..."
notify_on_import = true

[jellyfin]
enabled                   = true
url                       = "http://localhost:8096"
api_key                   = "..."
notify_on_import          = true
playback_safety           = true
verify_after_refresh      = false
webhook_secret            = "..."
plugin_enabled            = true
plugin_shared_secret      = "..."
plugin_auto_scan          = true
plugin_verify_on_startup  = false
plugin_verify_interval    = 0
plugin_daemon_url         = "http://192.168.1.10:5522"

[[jellyfin.path_mappings]]
jellyfin = "/tv"
daemon   = "/mnt/storage1/TVSHOWS"

[metadata_recovery]
passive_enabled               = true
repair_enabled                = false
unknown_season_repair_enabled = false
passive_interval_minutes      = 60
passive_batch_size            = 25
repair_batch_size             = 5
repair_cooldown_hours         = 6
needs_review_after            = 4

[logging]
level        = "info"
file         = ""
max_size_mb  = 10
max_backups  = 5
max_age_days = 0
compress     = false

[api]
allowed_origins = ["http://localhost:3000"]

[permissions]
user      = "jellyfin"
group     = "media"
file_mode = "0664"
dir_mode  = "0775"

Validate with:

plex2jellyfin config show
plex2jellyfin config test

On this page