Skip to content

Import Doctor

Implementation contracts and behavior for import doctor.

One shared classifier (internal/arr/doctor.go) explains stuck queue items in plain English: sample files, un-extracted archives, unconfirmed TheXEM mappings, “not an upgrade”, unparseable/invalid files, remote-path-mapping or download-client problems, stalled torrents, permissions: and maps each to ordered one-click fixes: process monitored downloads, manual/force import (candidates shown first, quality/languages blobs round-tripped verbatim), remove, blocklist + re-search, blocklist without re-search, change category (hand-off to e.g. Unpackerr), rescan. blocklist_search removes and blocklists, leaving replacement searches to the service’s failed-download settings; Cantinarr adds no search of its own. blocklist_only additionally suppresses that replacement search. It is appropriate for an unrequested upgrade (a known library file plus an RSS grab), or a TV episode whose live Sonarr air time is still in the future. TV queue remediation reads the exact instance’s queue, download-scoped grab history, and live episode records before mutation; if any episode in the download is unaired, it sends skipRedownload=true, including for existing blocklist-and-search proposals. New proposals are narrowed before review, and the effective action is saved in approved_params before dispatch without rewriting the original proposal. Explicit blocklist-only approvals never become searches after air. Failed or inconsistent scope/date reads stop execution; a missing air date remains unknown and does not itself establish that an episode is unaired. Diagnosis suggestions carry the same rule when the queue supplies a verified episode air time. The search guard uses the exact air instant and does not change the 12-hour tolerance used to detect suspicious imported files. Episode monitoring and existing library files are unchanged. For ordinary upgrades, missing library-file or grab-provenance evidence retains the service’s replacement policy. The same catalog backs the app UI (Sonarr, Radarr, and Chaptarr), the AI assistant, the remediation agent, and the MCP tools; diagnose_queue over MCP prints the exact next tool call per item.

An automatic TV queue incident can close with resolution_kind: removed_waiting_for_air: the server successfully dispatched blocklist-only, the exact download is absent after the settle window, the incident had no library file and still has none, and the live episode has not aired. Pack siblings are checked too; an aired or unknown-date gap prevents this ending. Observation and agent resume share the proof, and successful cleanup counts as success for a standing approval rule. The resolution says that the bad download was removed while the episode is still waiting to air, never that the media became available. Missing or incomplete evidence does not prove cleanup. User-reported content problems retain administrator review. Remediation proposals and execution also refuse a follow-up automatic search that includes an unaired episode; manual searches outside remediation keep their existing behavior.

Every problem label the classifier produces is an exported constant (arr.Problem…) rather than a literal inside a rule table. Those labels are persisted verbatim as issues.problem_kind and become agent_approval_rules keys, so renaming one silently orphans every standing rule armed on it and every stored row that carries it: a hazard that looked exactly like editing copy while each label lived as a string in its own rule. The rule tables, the prevention catalog below, and any future consumer now name the same bytes through the same identifier, so a label and the advice keyed on it cannot drift apart.

A truncated import reports itself. The failure class every queue-shaped detector structurally misses: a sample clip or truncated encode that IMPORTS leaves a healthy-looking queue and a full-looking library, and sits there until a human presses play. The Sonarr Download webhook now hands every completed import to a sentinel that judges the file against the arr’s own analysis: a file running under 40% of the show’s own per-episode runtime is not a short episode; it is not the episode. The evidence is Sonarr’s ffprobe runtime; a file the arr has not analyzed yet gets NO verdict (blindness is never treated as evidence), a show without a stored runtime gets none either, and the conservative threshold keeps specials and short episodes clear. Advisory-first by design: the notice opens at needs_admin with the SAMPLE problem label; one cause, one label, so the prevention rollup and its minimum-size advice already speak for the pattern; and no agent run. Running the agent on an episode-scoped, queue-less auto issue needs its own recovery proof (the same design the pre-air class earned); that upgrade is deliberate follow-up work, not a bolt-on.

Pre-air season fill is a second detector beside the Doctor (internal/arr/season.go), for the failure the Doctor structurally cannot see: by the time anyone notices the wrong episodes, the download has finished, imported, and left the queue: on a season grabbed ahead of its air dates, weeks earlier. BuildSeasonTimeline is therefore a pure function over typed episode facts rather than a queue classifier, and it turns on one rule: a file the service imported before its episode aired cannot be that episode, and a season already holding files for episodes that have not aired is content that has not been released. Aired is decided by the air date and only the air date: deliberately not sonarr.SeriesCompletion‘s definition, which counts an episode as aired once it merely holds a file and would fold every impossible file straight into the aired count. An episode carrying no air date is neither aired nor unaired and is excluded from every bucket rather than guessed at. The finding needs two unaired episodes holding files, not one: a single one is an everyday air-date slip and the file is probably genuine, while a batch is content no air-date error explains. get_episode_timeline renders the season episode by episode and, when the finding trips, prints the exact repair as a SINGLE call: delete_media_files over the episodes whose files predate their own air time: and says not to follow it with a search. That one action deletes, blocklists, and then searches the episodes that have already aired, leaving the rest of the season for the service to grab as it comes out. The same kind now repairs a WRONG BOOK: scoped to the issue’s own durable Chaptarr record id, it deletes the record’s file(s), marks the delivering grabs failed through the same history/failed route, and leaves the replacement to Chaptarr’s failed-download handling when that is on (books have no air dates, so there is no aired-only half; a file that arrived after the fix was proposed is spared). The one thing that calls its search off is the service having been triggered to look itself: marking a grab failed is what starts the service’s own failed-download handling, so when that setting is on the repair leaves the replacement to it and says so, and only searches when the service will not. That is the same boundary PR #363 drew: never duplicate or overrule the administrator’s own replacement policy: without making them approve a single repair twice. Deletion runs before blocklisting, and POST /api/v3/history/failed/{id}: the services’ own “Mark as Failed” button: is the only route to blocklist a release that already imported, since neither Sonarr nor Radarr exposes an add-to-blocklist endpoint. Nothing scans the library, and nothing in this server ever does: but the detector no longer waits to be asked.

Caught at import, not at complaint. Sonarr’s Download callback fires on every import and upgrade, and the managed webhook already subscribes to both (onDownload, onUpgrade), so the same detector runs the moment an impossible file lands: no report, no queue row, nobody having sat down to watch the wrong thing first. Cantinarr now parses episodes[] off that payload (id, episodeNumber, seasonNumber, airDateUtc), the fields it discarded until this, because an episode’s own air date is the only thing that can say a file claiming to be it cannot possibly be it. The gate is deliberately free: an airDateUtc still in the future is one comparison against the clock and no network at all, and the check stops at the first one it finds, so an ordinary import costs a few comparisons and only the impossible case ever pays for a library read. The handler decides nothing beyond that: one early file is an everyday air-date slip, and the two-file threshold belongs to the detector: so it reports the (series, season) once and the witness re-reads the whole live season before judging it.

What the finding opens is ONE source=auto issue for the season. A pack that imports as nine episodes is one problem; keying per episode would open nine issues, run the agent nine times, and ask an admin to approve one decision nine times: the same one-problem-one-decision rule the repair itself follows, applied at intake instead. The dedupe key is season-scoped (pre-air: + instance + TVDB identity, TMDB only for a series Sonarr holds without one, + season number) and namespaced away from the queue observer’s incident keys, so the open-issue unique index turns a webhook retry, or the second and ninth file of the same batch, into a refreshed detail and an incremented occurrences rather than a second row. It opens at open, not needs_admin, because the agent is what turns a finding into a proposal and only open/investigating issues are ever enqueued: which is exactly why no source=system issue ever gets a run: the health sinks (shared-AI health, push delivery, remediation-provider, the auto-dispatch breaker) and the recurrence notice below all open at needs_admin, and the book-import stall opens at waiting (equally never enqueued; the worker allowlists open/investigating). problem_kind is set to the persisted label Content that has not aired yet; every one of those system issues leaves it NULL and can therefore never match a standing auto-approval rule, while this one can, so an admin who has reviewed this repair once may arm one for it. It is written unread, so it counts in the admin Issues badge from the moment it exists: unlike a queue observation, which starts silent inside a tracking window and only marks itself unread if it survives to promotion, this finding is terminal as soon as it is made: those files exist and cannot become genuine. “Not aired” carries a twelve-hour margin floor everywhere the verdict is computed (detector, webhook gate, season proof): TheTVDB models a several-episodes-at-once premiere as a linear schedule staggered one runtime apart, so a release-day import can sit minutes “before air” on the calendar while being exactly the episode it claims: Reacher S4’s three-at-once drop (2026-08-12) proposed deleting two legitimate files that way, while the finding’s real disease arrives days early. The page still rides the ordinary 3-minute issue_created hold-down rather than firing immediately, because a batch import delivers a webhook per file within minutes and none of it is urgent. No issue_observations row is written: this is not a queue incident, and the queue sweeper must not adopt it.

Instant updates install automatically when an instance is added, but the install can fail (an unreachable callback) or predate that default, so an instance whose webhook is not actually configured would otherwise be uncovered. A fallback pass rides the observation sweeper’s existing goroutine, every 15th one-minute tick, rather than a timer of its own: every arr library read in the service already happens on that worker, and a second timer would race it for the one DB connection. It is still not a scan. One history page per Sonarr instance per pass names only the seasons something actually imported into (downloadFolderImported, the same event vocabulary the content-alert catch-up reads), filtered by an in-memory per-instance watermark that starts 6 hours back on the first pass after a restart: long enough to catch a batch that landed while Cantinarr was down, short enough that a cold start is not a sweep of everything: and at most 8 distinct seasons are examined per pass, logged when that cap bites rather than silently truncated. A quiet instance therefore costs exactly one arr call per quarter hour and reads nothing else, and repeats are free because the dedupe index makes a second open a no-op. The watermark advances only after a pass completes, so a failed read re-examines its window instead of stepping over it.

A season-scoped issue could close through neither existing recovery proof. exactRecoveryProven and upgradeAbandonProven both read issue_observations, which this class deliberately does not have, and both go through exactIssueFileState, which fails closed on episode_number = 0: correct for a queue incident, where which exact file is the whole question, and simply the wrong question to ask about a season. So this class carries its own proof, in two typed, server-computed halves that never rest on the model’s reading of its own tool output. get_episode_timeline, when scoped to a single season, emits a season_clean verification whose target_present is the live finding, and the runner sets the same “target cleared” flag from it that get_queue’s queue_target already sets, so the conclusion gate opens for this issue class without being loosened for any other; a season Sonarr does not hold gets no verdict at all, so an absent season can never read as a repaired one. Then, before the issue actually closes, the server re-reads the live season and requires that nothing unaired still holds a file: not merely that the finding stopped tripping, which, the threshold being two, would let a single impossible file left behind pass as repaired. Both intake paths are TV and Sonarr only: Radarr has no equivalent, the fallback reads Sonarr history alone, and neither ever looks at a season nothing recently imported into.

View the maintained source for this page.