Files
profilarr/docs/backend/drift.md
2026-05-07 13:03:06 +09:30

14 KiB

Drift Detection

Source: src/lib/server/jobs/handlers/arrDrift.ts, src/lib/server/jobs/handlers/arrSync.ts, src/lib/server/db/queries/arrDriftSettings.ts, src/lib/server/db/queries/arrDriftStatus.ts, src/lib/server/drift/customFormats.ts, src/lib/server/drift/qualityProfiles.ts, src/lib/server/drift/delayProfiles.ts, src/lib/server/drift/mediaManagement.ts, src/lib/server/drift/display.ts, src/routes/arr/[id]/drift/+page.svelte, src/routes/arr/[id]/drift/+page.server.ts, src/routes/arr/[id]/drift/components/DriftFieldDiffTable.svelte, src/routes/arr/[id]/sync/+page.server.ts, src/routes/arr/[id]/sync/components/QualityProfiles.svelte, src/routes/arr/[id]/sync/components/DelayProfiles.svelte

Drift detection checks whether an Arr instance still matches the configuration Profilarr would sync now. It is observational: it does not write to Arr, repair config, delete stale items, or replace cleanup.

Drift detection currently covers custom formats, quality profiles, the default delay profile, and media management media settings, naming, and quality definitions.

Job

The scheduled job type is arr.drift with payload { instanceId }.

Scheduling is per Arr instance and uses arr_drift_settings:

Field Purpose
enabled Master switch for drift detection
cron Cron expression for scheduled checks
next_run_at Next scheduled run stored as UTC text

The job queue uses dedupe key arr.drift:{instanceId} so each instance has at most one scheduled drift job.

Current handler behavior:

  • invalid instance ids fail
  • missing instances fail
  • missing or disabled settings cancel the job
  • unsupported Arr types are skipped
  • enabled jobs compare custom formats, quality profiles, delay profiles, media management media settings, media management naming, and media management quality definitions, store the latest result, and return success
  • drift-detected and failed runs notify subscribed services when the current drift or error hash has not already been notified
  • scheduled jobs calculate and store the next run before returning

Latest Status

Drift stores only the latest result per Arr instance in arr_drift_status. Job run history remains the operational history.

Field Purpose
status never_checked, clean, drift_detected, failed
last_checked_at Last completed check time
counts_json Count summary by drift section
diff_json Structured latest drift result
diff_hash Stable hash of the structured drift result
last_notified_hash Last drift hash sent as a notification
last_notified_at Last drift notification time
last_error Latest failure detail
error_hash Stable hash of the latest failure detail
last_notified_error_hash Last failure hash sent as a notification

Notifications

Drift emits notification events through the shared notification manager:

Event Trigger Severity
arr.drift.detected Latest drift hash has not been sent warning
arr.drift.failed Latest failure hash has not been sent error

Detected notifications emit one section block per drift category (Custom Format, Quality Profile, Delay Profile, Media Management) listing up to 15 displayable drift entities total. If more entities exist, an additional More block summarises the remainder as +N more. Discord renders each section as a code-block field. Webhook receives the full payload. Summary-tier services such as Ntfy and Telegram show only the title because section blocks are omitted.

Failed notifications use the first error line as the message and include the full error text in an Error section.

Custom Formats

Custom format drift compares Profilarr-managed custom formats referenced by synced quality profile selections.

Comparison rules:

  • build expected custom formats with the same transformer used by sync
  • fetch actual custom formats from Arr
  • match by custom format name
  • ignore Arr ids
  • compare includeCustomFormatWhenRenaming
  • compare normalized specifications and fields
  • ignore unmanaged extra Arr custom formats

Quality Profiles

Quality profile drift compares selected Profilarr-managed profiles by name. Expected profiles are built through the same quality profile transformer used by sync, with actual Arr custom format ids used to resolve scoring rows.

Comparison rules:

  • report selected quality profiles missing from Arr
  • ignore Arr profile ids
  • compare upgradeAllowed, cutoff, minFormatScore, cutoffFormatScore, and minUpgradeFormatScore
  • compare Radarr language; ignore language for Sonarr
  • compare normalized quality item order, grouping, and allowed flags
  • compare custom format scores for custom formats managed by that profile, including explicit zero scores
  • report an expected managed custom format as missing from scoring if the custom format is missing from Arr
  • ignore unmanaged custom format scoring rows with score 0
  • report unmanaged custom format scoring rows with nonzero scores because they affect profile behavior

Delay Profiles

Delay profile drift compares the selected Profilarr delay profile against Arr's default delay profile (id=1). This mirrors sync, which always overwrites the default Arr profile instead of creating or matching by name.

Comparison rules:

  • build expected delay profile with the same transformer used by sync
  • no selected delay profile is clean
  • report the selected delay profile as missing if Arr does not return id=1
  • ignore non-default extra Arr delay profiles
  • compare id, derived protocol, delays, bypass fields, minimum custom format score, order, and tags

Media Management

Media management drift compares configured media settings, naming, and quality definition selections against Arr's media management, naming, and quality definition configs.

Comparison rules:

  • build expected media settings, naming, and quality definitions with the same transformers used by sync
  • no selected media settings, naming, or quality definitions config is clean
  • missing selected PCD cache or config fails the drift check
  • compare downloadPropersAndRepacks and enableMediaInfo
  • compare Radarr naming fields: rename, illegal-character replacement, colon replacement, movie format, and movie folder format
  • compare Sonarr naming fields: rename, illegal-character replacement, colon replacement, custom colon replacement, multi-episode style, episode formats, series folder format, and season folder format
  • normalize Sonarr Arr enum integers back to semantic strings before comparing
  • compare quality definition minSize, maxSize, and preferredSize
  • normalize PCD quality definition 0 size values to Arr null for unlimited maximum and preferred sizes
  • ignore unmapped PCD quality definitions, mapped definitions missing in Arr, extra Arr quality definitions, and unmanaged Arr fields

Display Formatter

src/lib/server/drift/display.ts maps the raw diff_json stored in arr_drift_status into a typed list of DriftDisplayEntity objects consumed by the page. One entity is one drifted managed item (e.g. one custom format). Each entity carries:

  • section and sectionLabel (e.g. custom_formats / Custom Format)
  • state and stateLabel (missing / modified / extra)
  • tone for badge color signaling
  • summary (one-line description, e.g. 3 changes detected)
  • changes[]: per-field DriftDisplayChange rows with label, optional detail, and expected / actual DriftDisplayValues. Values carry text, optional mono, and optional tone.

For custom formats the formatter resolves Arr enum ids back to friendly names (sources, resolutions, indexer flags, languages, release types, quality modifiers), formats sizes in human-readable bytes, and turns specification paths into labeled changes (negate / required / per-field changes / missing condition / extra condition).

For quality profiles the formatter turns settings, language, qualities, and custom format score paths into labeled changes. Quality profile score rows show expected and actual score values, missing custom formats, missing score rows, and unmanaged nonzero score rows.

For delay profiles the formatter turns the derived protocol, delays, bypass flags, minimum score, order, and tags into friendly field rows.

For media management the formatter turns media settings, naming, and quality definition changes into friendly field rows.

Display types live in src/lib/shared/drift.ts.

Arr Drift Page

Route: /arr/[id]/drift. Source: src/routes/arr/[id]/drift/+page.svelte and +page.server.ts.

Layout:

  • Sticky header with Run Now (queues a manual arr.drift job) and Save (persists the schedule and enabled state).
  • Settings bar (borderless, full-width, with a bottom rule): Detection toggle, Schedule CronInput, and timing pills aligned right (Paused / Ready / Next ... / Last ...).
  • Entities section: an ExpandableTable with Name / Entity / State columns, mirroring the dev changes-page diff idiom. Each drifted entity is one row; expanding shows a DriftFieldDiffTable with Field / Profilarr / <Arr type> - <Arr name> columns rendering the entity's changes[].

State rendering inside the entities section:

Latest status Rendered as
Detection disabled Empty ExpandableTable chrome with a disabled message in the empty row
never_checked Neutral message box
clean Success message box
failed Error message box with last_error
drift_detected, displayable items Populated ExpandableTable
drift_detected, no displayable items Amber message box (formatter produced nothing for the stored diff)

The page is read-only for drift results. It never writes to Arr, repairs configuration, or triggers sync.

Sync Page Progress

Route: /arr/[id]/sync. Source: src/routes/arr/[id]/sync/+page.server.ts, src/routes/arr/[id]/sync/components/QualityProfiles.svelte, src/routes/arr/[id]/sync/components/DelayProfiles.svelte, src/routes/arr/[id]/sync/components/MediaManagement.svelte.

Per-section drift progress is rendered as ProgressIndicator chips in each sync section header (right-aligned on tablet+, stacked below the title on mobile). The Quality Profiles header carries two chips: one for QPs themselves and one for the custom formats referenced by those QPs. The Delay Profiles header carries one chip. The Media Management header carries up to three chips — one each for Naming, Quality Definitions, and Media Settings — corresponding to the three sub-configs the user selects in that section.

Each chip shows current / total where:

  • total: managed items Profilarr would sync. Selected QP count for the QP chip, expected CF count from buildExpectedCustomFormats for the CF chip, 0 or 1 for the delay profile chip and each of the three Media Management chips (Naming, Quality Definitions, Media Settings).
  • current: total - drifted. For the QP chip, drifted counts only QPs the drift comparison flagged directly. QPs that are only "transitively" affected (their scoring rows reference a CF that has been deleted from Arr) are not subtracted from the QP chip; the user-visible impact is surfaced via the CF chip's tooltip instead.

Chips render with colorMode="completion": green check when met, yellow bar otherwise. Anything below 100% is yellow regardless of how close to completion, because for drift "almost in sync" is still actionable.

Each chip has a tooltip listing up to three affected entity names with a +N more suffix when the list overflows. Examples:

  • "HD Movies, 4K Movies drifted."
  • "HD Movies affected by drifted custom formats."
  • "Streaming Tier drifted, used in HD Movies, TV."
  • "Standard Delay drifted."

Chips are hidden entirely when any of these conditions hold:

  • FEATURES.drift is off
  • per-instance arr_drift_settings.enabled is false
  • drift status is never_checked or failed (or no row exists)
  • the section's denominator is zero (e.g. no selected QPs)

Failures (status === 'failed') surface only on the dedicated drift page.

Post-sync drift refresh

The arr sync handler (src/lib/server/jobs/handlers/arrSync.ts) enqueues an arr.drift job after a successful sync that touched Arr (any section ran). This keeps the sync page chips fresh after the user fixes drift by syncing, instead of waiting for the next scheduled drift run. The chain is gated on FEATURES.drift and per-instance arr_drift_settings.enabled.

The sync page subscribes to job-finished events via jobStatus.onJobFinished and calls invalidateAll() whenever an arr.drift job completes. The raw hook is used (not the store's state machine) because the state machine drops finished events for jobs it is not actively tracking, including a drift job chained right after a sync's completion holdoff window. SSE is opened on demand when the user triggers a sync and auto-closes after the post-completion idle window, so this does not hold a persistent connection.