Files
profilarr/docs/backend/upgrades.md
2026-05-27 13:27:50 +09:30

11 KiB

Upgrades

Source: src/lib/server/upgrades/ (processor, cooldown, normalize, logger) Shared: src/lib/shared/upgrades/ (filters, selectors)

The upgrade system is a scheduled job that proactively searches Arr for better releases. It fills the gap that RSS alone can't cover: new quality profiles, profile score updates, and indexer downtime all leave existing library items with suboptimal files that RSS will never revisit. Upgrades work through the library methodically, one filter at a time, searching for improvements and letting Arr's own upgrade logic decide whether to grab them.

Table of Contents

Pipeline

Each run processes a single filter from the config's filter list. The processor normalizes Radarr/Sonarr responses into a unified UpgradeItem so all downstream logic is app-agnostic.

flowchart TD
    FETCH[Fetch library + normalize] --> PICK[Pick filter]
    PICK --> EVAL[Evaluate filter rules]
    EVAL --> COOL[Exclude cooldown-tagged items]
    COOL --> DRY{Dry run?}

    DRY -->|Yes| EXCL[Exclude previous dry-run picks]
    DRY -->|No| SEL

    EXCL --> SEL[Apply selector]
    SEL --> SEARCH{Dry run?}

    SEARCH -->|Yes| RELEASES[Fetch releases + compare scores]
    SEARCH -->|No| BATCH[Batch search + apply cooldown tag]

    RELEASES --> LOG[Log + notify]
    BATCH --> LOG

Live searches watch the Arr queue as soon as the search command is created. Queue entries are accumulated every 3 seconds while the command runs. After the command completes, the processor parses Arr's downloaded report count from the command message and keeps watching for up to 3 minutes if fewer queue entries were observed. If Arr reports zero downloads, the run finishes immediately after command completion.

Status

Status Meaning
success Searches triggered, no errors
partial Some searches succeeded, some failed
failed All searches failed or a fatal error occurred
skipped No items to search (filtered to zero)

Filters

Filters use a nested group/rule structure with AND/OR logic. A config can have multiple filters; each run processes exactly one (see Scheduling).

A rule is a field + operator + value triple:

monitored  is  true
year       gte 2020
status     gte released

A group wraps rules (or nested groups) with a match mode:

  • all -- every child must match (AND)
  • any -- at least one child must match (OR)

Fields are typed by category:

Category Operators Examples
Boolean is, is_not monitored, cutoff_met
Text contains, not_contains, starts/ends_with, eq title, quality_profile, genres, tags
Number eq, neq, gt, gte, lt, lte year, rating, size_on_disk, popularity
Date before, after, in_last, not_in_last date_added, digital_release, first_aired
Ordinal eq, neq, gte, lte, gt, lt status, minimum_availability
Custom Format includes, does_not_include, is_only, has_any, has_none custom_format

Ordinal fields have a defined progression (e.g. tba -> announced -> inCinemas -> released for Radarr) so operators like gte mean "has reached this stage or later." Radarr and Sonarr each have app-specific fields; the full list is in src/lib/shared/upgrades/filters.ts.

The Radarr-only custom_format field checks the custom formats currently matched by the movie file. includes, does_not_include, and is_only take a custom format name. has_any and has_none are value-less operators. Sonarr is intentionally unsupported because a series does not have one series-level current custom format set.

Filter imports are app-scoped. Shared fields can move between Radarr and Sonarr, but an import is blocked if any rule uses a field unavailable for the current app type. This prevents a pasted filter from silently changing meaning by dropping incompatible rules.

Each filter also carries a cutoff (0-100%), a percentage of the quality profile's cutoff score. Items whose current score meets or exceeds the threshold are considered "cutoff met" and can be filtered out.

Dynamic Filter Values

Some text fields use Arr-derived dropdown values instead of free text. These options are loaded by dynamicOptions.ts and streamed from the upgrades page load, so the page shell renders before heavier library/file metadata finishes loading.

Most dynamic fields use exact string operators only: eq and neq (shown as "is" / "is not" in the UI). custom_format is dynamic but uses set operators.

Scope Fields Source
Shared quality_profile, tags, original_language, genres Profiles, tags, library
Radarr release_group Movie file metadata
Radarr custom_format Arr custom formats
Sonarr network, certification Series library metadata

If an Arr source fails while loading dynamic options, the affected fields fall back to empty option lists and existing saved values remain visible.

Selectors

After filtering, a selector picks which items to search. Each filter specifies a selector strategy and a count (items per run).

Selector Strategy
random Shuffle, take N
oldest Oldest by date added
newest Newest by date added
lowest_score Lowest custom format score first
most_popular Highest popularity first
least_popular Lowest popularity first
alphabetical_asc A-Z by title
alphabetical_desc Z-A by title

Selector definitions live in src/lib/shared/upgrades/selectors.ts.

Scheduling

Each Arr instance has a single upgrade config with a global cron schedule. Each run picks one filter from the enabled list:

  • Round robin -- cycles through filters in order. currentFilterIndex increments after each non-failed run and wraps around.
  • Random -- shuffles the enabled filters and cycles through all of them before reshuffling.

The job system manages dispatch via nextRunAt / lastRunAt. After each run, the handler calculates the next cron occurrence and updates nextRunAt.

Arr instance records are de-duplicated by normalized type + url during creation and settings edits. This prevents accidental duplicate records for the same Arr target from bypassing per-instance upgrade cooldowns while still allowing cloned 4K instances that reuse the same API key on different targets. This is a guardrail, not an anti-abuse boundary. A user who intentionally routes the same Arr instance through multiple aliases or reverse proxies can still bypass this check, but that setup has no legitimate operational purpose inside Profilarr. Duplicate aliases are treated as intentional misuse rather than a case the app tries to fully prevent.

Cooldown

The cooldown system prevents the same item from being searched repeatedly across runs. It works through Arr tags:

  1. When items are searched in a live run, the processor tags them in Arr with profilarr-{filter-name} (slugified, max 50 chars). Filters can also specify a custom tag to override the auto-generated one.
  2. On the next run, filterByFilterTag() excludes items that already carry the filter's tag.
  3. When every matched item has been tagged (the filter is "exhausted"), resetFilterCooldown() removes the tag from all items and a new cycle begins.

This means the system works through the entire filtered pool before revisiting any item. Multiple filters can share a tag to enforce a shared cooldown across them.

Dry Run

Dry-run mode fetches available releases for each selected item and compares scores without triggering actual searches. This lets users preview what the system would do. For Sonarr, dry runs only query monitored seasons that already have files; selected series without an eligible season are shown without a previewed upgrade.

An in-memory exclusion cache (1-hour TTL, keyed by instance ID) tracks items selected in previous dry runs so the same items aren't re-picked on repeated manual runs. The cache can be cleared from the UI.

In dry-run mode, cooldown tags are not applied to items.

Logging

Every run produces an UpgradeJobLog that captures the full funnel:

  • Config snapshot -- cron, filterMode, selected filter name, dryRun flag
  • Library stats -- total items, fetch duration
  • Filter stats -- matched count, after cooldown, dry-run excluded
  • Selection stats -- method, requested count, actual count, per-item details (title, current score/formats, upgrade releases with scores)
  • Results -- searches triggered, successful, failed, errors

If Arr reports more downloaded releases than Profilarr observed in the queue, the run still records the upgrades it saw and logs a warning with the reported and observed counts.

Core logging functions in logger.ts:

  • logUpgradeRun(log) -- persists the full log to the upgrade_runs table and writes a summary to the logger with source UpgradeJob. Log level is INFO for success, WARN for partial, ERROR for failed.
  • logUpgradeSkipped(instanceId, name, reason) -- DEBUG-level.
  • logUpgradeError(instanceId, name, error) -- ERROR-level.
  • logUpgradeQueueDetectionMismatch(details) -- WARN-level diagnostic when Arr reports more downloads than the queue monitor observed.

Notifications

Upgrade runs emit one of four notification types via the notification system:

Type Severity When
upgrade.success success Searches completed, no errors
upgrade.partial warning Some searches, some errors
upgrade.failed error All searches failed
upgrade.skipped success Nothing to search

Notifications are not sent for dry runs. Each notification includes the filter name, selector method, funnel breakdown (total -> filtered -> cooldown -> selected), and per-item score/format comparisons with poster images. Sonarr items are flattened by season.