Skip to content

feat(issues): admin action to mark issue as failed and redownload#2915

Open
babyhuey wants to merge 11 commits intoseerr-team:developfrom
babyhuey:feature-issue-mark-as-failed
Open

feat(issues): admin action to mark issue as failed and redownload#2915
babyhuey wants to merge 11 commits intoseerr-team:developfrom
babyhuey:feature-issue-mark-as-failed

Conversation

@babyhuey
Copy link
Copy Markdown

@babyhuey babyhuey commented Apr 20, 2026

Description

Adds an admin only action "Mark as failed and redownload" action on the "issue details" page. When an admin clicks it, seer asks radarr/sonarr to mark the currently grabbed release as failed, which blocklists that release and automatically triggers a new search. The issue is auto resolved and a system comment is appended recording the action. Issue creation and the redownload action are on separate permissions so that anyone can still file an issue, but only admins can trigger the redownload.
It now asks radarr/sonarr to delete the scoped file, then kicks off a search at the matching scope. then resolves the issue and appends a system commit logging what was done.

For TV the scope follows the issue's problemSeason and problemEpisodefields. If no grabbed history record can be found, falls back to a plain search.

Related open issues this touches:

How Has This Been Tested?

Unit tests with pnpm test. 35/35 pass including the 2 new tests
linting/formatting/i18n all clean

Screenshots / Logs (if applicable)

image image image

Checklist:

  • I have read and followed the contribution guidelines.

  • Disclosed any use of AI (see our policy)

  • I have updated the documentation accordingly.

  • All new and existing tests passed.

  • Successful build pnpm build

  • Translation keys pnpm i18n:extract

  • Database migration (if required)

    AI Disclosure: I used Claude Code to draft the implementation plan and generate the initial code. I reviewed every commit, ran pnpm test / pnpm lint / pnpm format:check locally, and performed all manual Radarr/Sonarr verification myself. PR description is my own words.

Summary by CodeRabbit

  • New Features

    • Added "Mark as Failed & Redownload" action to Issue Details for admins; triggers a fresh search and resolves the issue on success.
    • New authenticated POST endpoint to request a redownload for a specific issue.
  • Behavior / Fixes

    • Attempts to mark prior grabbed files as failed, remove on-disk files, and trigger remote rescan/search; maps specific error conditions to appropriate HTTP responses.
  • Tests

    • Integration tests covering permission checks, resolved-state handling, and successful redownload flow.
  • Localization

    • Added English UI strings and toasts for the redownload workflow.

@babyhuey babyhuey requested a review from a team as a code owner April 20, 2026 20:49
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

Adds an admin-only endpoint to mark grabbed media as failed and trigger Radarr/Sonarr rescans, plus server-side servarr helpers, core redownload logic, route + tests, and frontend UI with confirmation and localization strings.

Changes

Cohort / File(s) Summary
API Definition
seerr-api.yml
New authenticated POST /issue/{issueId}/redownload operation with issueId path param, returns updated Issue, and documents 400/403/404/502 error responses.
Servarr base & history
server/api/servarr/base.ts
Added HistoryRecord type, getHistory() to fetch history records, and markGrabAsFailed() to mark a history entry failed; errors wrapped with apiName context.
Radarr client
server/api/servarr/radarr.ts
Added deleteMovieFile(movieFileId: number) to delete a Radarr movie file and wrap/log errors.
Sonarr client
server/api/servarr/sonarr.ts
Added searchSeason(seriesId, seasonNumber), searchEpisodes(episodeIds), and deleteEpisodeFile(episodeFileId) with logging and error wrapping.
Core redownload logic
server/lib/issueRedownload.ts
New module exporting RedownloadError and triggerRedownload(issue). Implements validation, derives scope (series/season/episode), finds and marks appropriate servarr history entries as failed, deletes on-disk files, and triggers targeted Radarr/Sonarr searches.
Route & tests
server/routes/issue.ts, server/routes/issue.test.ts
Added admin-only POST /:issueId/redownload handler that calls triggerRedownload, maps RedownloadError statuses, appends comment, sets issue to RESOLVED; full integration tests for permission, conflict, and success flows (stubbing trigger).
Frontend UI & i18n
src/components/IssueDetails/index.tsx, src/i18n/locale/en.json
Added “Mark as Failed & Redownload” button (admin-only, visible for linked media), confirmation modal, request state and toasts, revalidation on success, and four new English localization keys.

Sequence Diagram

sequenceDiagram
    actor Admin
    participant Frontend as Frontend
    participant Backend as Backend Server
    participant DB as Database
    participant Servarr as Radarr/Sonarr

    Admin->>Frontend: Click "Mark as Failed & Redownload"
    Frontend->>Frontend: Show confirmation modal
    Admin->>Frontend: Confirm action
    Frontend->>Backend: POST /issue/{issueId}/redownload

    Backend->>DB: Load Issue + Media
    DB-->>Backend: Issue data

    alt Movie
        Backend->>Servarr: GET /history?movieId=...
        Servarr-->>Backend: History records
        Backend->>Servarr: POST /history/failed/{historyId}
        Servarr-->>Backend: OK
        Backend->>Servarr: GET /movieFile/{movieFileId}
        Servarr-->>Backend: File details
        Backend->>Servarr: DELETE /movieFile/{movieFileId}
        Servarr-->>Backend: OK
        Backend->>Servarr: POST /command (searchMovie)
        Servarr-->>Backend: Command queued
    else TV Series
        Backend->>Servarr: GET /episode?seriesId=...
        Servarr-->>Backend: Episodes
        Backend->>Servarr: GET /history?seriesId=...
        Servarr-->>Backend: History records
        Backend->>Servarr: POST /history/failed/{historyId}
        Servarr-->>Backend: OK
        Backend->>Servarr: DELETE /episodeFile/{episodeFileId}
        Servarr-->>Backend: OK (x N)
        Backend->>Servarr: POST /command (searchEpisodes/searchSeason/searchSeries)
        Servarr-->>Backend: Command queued
    end

    Backend->>DB: Add IssueComment, set Issue.status = RESOLVED, save
    DB-->>Backend: Persisted

    Backend-->>Frontend: 200 OK + Updated Issue
    Frontend->>Frontend: Revalidate data, show success toast
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • gauthier-th
  • M0NsTeRRR

Poem

🐰🌿 I twitched my nose and gave a hop,
A grabbed file failed — no need to stop.
Press the button, watch the search renew,
Old bits gone, fresh fetches come through.
Hooray — your shelves are whole anew! 🎬✨

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding an admin action to mark issues as failed and trigger redownloads, which is the primary feature introduced across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@babyhuey babyhuey changed the title Feature issue mark as failed feat(issues): admin action to mark issue as failed and redownload Apr 20, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
server/lib/issueRedownload.ts (2)

80-85: Consider isolating the "mark failed → delete file → search" steps so a mid-flow failure doesn’t leave a partially-applied state.

The three API calls are executed without per-step error handling. If markGrabAsFailed succeeds but deleteMovieFile / deleteEpisodeFile throws (e.g., file already removed out-of-band, transient *arr error), the whole function aborts — the release is blocklisted but the file is still present, so the subsequent manual retry will find no grabbed entry to blocklist and the *arr search will still be a no-op because the file remains on disk. A light try/catch around the delete (logging + continuing to the search) would make the flow more tolerant of benign 404s from *arr for files that no longer exist.

Non-blocking — feel free to defer, but worth considering for the retry experience.

Also applies to: 154-182

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/lib/issueRedownload.ts` around lines 80 - 85, The sequence that marks
a grab failed then deletes the file then triggers a search can leave the system
in a half-applied state if deleteMovieFile/deleteEpisodeFile throws; wrap the
delete call(s) (references: deleteMovieFile, deleteEpisodeFile and the
surrounding radarr.getMovie usage) in a try/catch that logs the error (including
404/not-found cases) and continues to call radarr.searchMovie so the search is
attempted even when the delete fails, and ensure markGrabAsFailed remains
unchanged; treat 404-like errors as non-fatal and only escalate unexpected
errors to the logger.

136-152: Season-scope grabbed-history matching will miss individual-episode grabs.

When the scope is season-only (no targetEpisodeId), the filter only matches history entries where h.data?.seasonNumber equals the target season. However, Sonarr populates data.seasonNumber only on season-pack grabs; individual-episode grabs include only a top-level episodeId. The current filter will therefore skip individual-episode grabs, causing the most recent grab for a season composed of single-episode releases to be missed — you'll fall through to the "no grabbed history found" fallback and rely purely on file deletion and re-search.

Since you already have allEpisodes, resolve each history record's episodeId to its season as a secondary match:

♻️ Proposed refactor
-    const inScope =
-      scope.seasonNumber !== undefined
-        ? history.filter(
-            (h) =>
-              h.eventType === 'grabbed' &&
-              // Sonarr history records include seasonNumber under data for
-              // season-pack grabs and under top-level for episode grabs.
-              String(h.data?.seasonNumber ?? '') === String(scope.seasonNumber)
-          )
-        : history.filter((h) => h.eventType === 'grabbed');
-    grabbedId = inScope[0]?.id;
+    const episodeSeasonById = new Map(
+      allEpisodes.map((e) => [e.id, e.seasonNumber])
+    );
+    const inScope =
+      scope.seasonNumber !== undefined
+        ? history.filter((h) => {
+            if (h.eventType !== 'grabbed') return false;
+            // Season-pack grabs: data.seasonNumber is populated.
+            if (
+              String(h.data?.seasonNumber ?? '') ===
+              String(scope.seasonNumber)
+            ) {
+              return true;
+            }
+            // Single-episode grabs: resolve seasonNumber via episodeId.
+            return (
+              h.episodeId !== undefined &&
+              episodeSeasonById.get(h.episodeId) === scope.seasonNumber
+            );
+          })
+        : history.filter((h) => h.eventType === 'grabbed');
+    grabbedId = inScope[0]?.id;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/lib/issueRedownload.ts` around lines 136 - 152, The season-scope
history filter currently only checks h.data?.seasonNumber and thus misses
individual-episode grabs; update the branch where scope.seasonNumber is defined
(the code building inScope and setting grabbedId) to also accept history entries
with a top-level h.episodeId whose episode's season (resolve via the existing
allEpisodes collection/map) equals scope.seasonNumber — i.e., when
scope.seasonNumber !== undefined, include entries where
String(h.data?.seasonNumber) === String(scope.seasonNumber) OR where h.episodeId
maps to an episode in allEpisodes whose seasonNumber matches; leave grabbedId =
inScope[0]?.id logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/api/servarr/sonarr.ts`:
- Around line 357-398: searchSeason and searchEpisodes currently catch errors
from this.runCommand('SeasonSearch'|'EpisodeSearch') and only log them, causing
calling code (e.g., triggerRedownload) to assume success; change these methods
to rethrow the caught error after logging (or remove the try/catch and let the
error bubble) so failures propagate to callers. Locate the methods searchSeason
and searchEpisodes and update their catch blocks to log the error (using
e.message) and then throw e (or omit the try/catch entirely) so callers can
detect and handle Sonarr command failures.

In `@server/routes/issue.ts`:
- Around line 325-331: Before calling triggerRedownload(issue) guard the route
by checking the issue status: after retrieving the issue with
issueRepository.findOneOrFail, verify issue.status === IssueStatus.OPEN and if
not respond with an error (e.g., 400/409 and a clear message) or throw an HTTP
error; only call triggerRedownload(issue) when the status is OPEN. Update any
imports/usages to reference IssueStatus enum and ensure the early return happens
before the try/catch that calls triggerRedownload.

---

Nitpick comments:
In `@server/lib/issueRedownload.ts`:
- Around line 80-85: The sequence that marks a grab failed then deletes the file
then triggers a search can leave the system in a half-applied state if
deleteMovieFile/deleteEpisodeFile throws; wrap the delete call(s) (references:
deleteMovieFile, deleteEpisodeFile and the surrounding radarr.getMovie usage) in
a try/catch that logs the error (including 404/not-found cases) and continues to
call radarr.searchMovie so the search is attempted even when the delete fails,
and ensure markGrabAsFailed remains unchanged; treat 404-like errors as
non-fatal and only escalate unexpected errors to the logger.
- Around line 136-152: The season-scope history filter currently only checks
h.data?.seasonNumber and thus misses individual-episode grabs; update the branch
where scope.seasonNumber is defined (the code building inScope and setting
grabbedId) to also accept history entries with a top-level h.episodeId whose
episode's season (resolve via the existing allEpisodes collection/map) equals
scope.seasonNumber — i.e., when scope.seasonNumber !== undefined, include
entries where String(h.data?.seasonNumber) === String(scope.seasonNumber) OR
where h.episodeId maps to an episode in allEpisodes whose seasonNumber matches;
leave grabbedId = inScope[0]?.id logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d383ca4d-b2da-4418-917c-52d3a62a9418

📥 Commits

Reviewing files that changed from the base of the PR and between 3e4bf1b and 3185706.

📒 Files selected for processing (9)
  • seerr-api.yml
  • server/api/servarr/base.ts
  • server/api/servarr/radarr.ts
  • server/api/servarr/sonarr.ts
  • server/lib/issueRedownload.ts
  • server/routes/issue.test.ts
  • server/routes/issue.ts
  • src/components/IssueDetails/index.tsx
  • src/i18n/locale/en.json

Comment thread server/api/servarr/sonarr.ts
Comment thread server/routes/issue.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
server/routes/issue.test.ts (1)

146-149: Assert the redownload service receives the target issue.

The mock currently ignores its argument, so this test would still pass if the route called triggerRedownload with the wrong issue. Please assert the mocked call’s first argument has the expected issue id.

Suggested assertion
     assert.strictEqual(res.status, 200);
     assert.strictEqual(triggerRedownloadMock.callCount(), 1);
+    assert.strictEqual(triggerRedownloadMock.calls[0].arguments[0].id, issue.id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/issue.test.ts` around lines 146 - 149, The test only checks
that triggerRedownloadMock was called once but doesn't verify the argument;
update the test to assert that the mock's first call received the expected issue
(check triggerRedownloadMock's first call args and assert the arg's id equals
issue.id). Locate the mock named triggerRedownloadMock used after
agent.post(`/issue/${issue.id}/redownload`) and add an assertion that the first
call's first argument has id equal to issue.id (in addition to the existing call
count and response status assertions).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/routes/issue.test.ts`:
- Around line 146-149: The test only checks that triggerRedownloadMock was
called once but doesn't verify the argument; update the test to assert that the
mock's first call received the expected issue (check triggerRedownloadMock's
first call args and assert the arg's id equals issue.id). Locate the mock named
triggerRedownloadMock used after agent.post(`/issue/${issue.id}/redownload`) and
add an assertion that the first call's first argument has id equal to issue.id
(in addition to the existing call count and response status assertions).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e51faca5-5c55-4a40-b30f-c97d313c0cbe

📥 Commits

Reviewing files that changed from the base of the PR and between 3185706 and 5f7e9f8.

📒 Files selected for processing (3)
  • server/api/servarr/sonarr.ts
  • server/routes/issue.test.ts
  • server/routes/issue.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/api/servarr/sonarr.ts
  • server/routes/issue.ts

@babyhuey
Copy link
Copy Markdown
Author

Hi folks, really sorry to nudge, and I completely understand you're busy. Just wanted to gently check in on this one in case it slipped off the radar. Totally happy to make any changes, break it up differently, or answer any questions if there's anything that would make review easier. No rush at all, and thank you so much for all the work you put into this project. I really appreciate it. 🙏

@poupounetjoyeux
Copy link
Copy Markdown

Hey!

Waiting for a feature like this
Is the button also available in the requests list page to be able to do massive retry ?

@babyhuey
Copy link
Copy Markdown
Author

Hey!

Waiting for a feature like this
Is the button also available in the requests list page to be able to do massive retry ?

Thanks for the kind words! For this PR I deliberately scoped it to the Issue Details page so it stays small and focused for review. A bulk retry action from the requests list is a great idea but really a separate feature (different permission boundary, different UX), so probably best filed as a follow up issue once this lands. I'm happy to help look at that next if maintainers are interested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants