Skip to content

feat(settings): add configurable download progress refresh interval#2647

Open
alchemyyy wants to merge 2 commits intoseerr-team:developfrom
alchemyyy:download-refresh-interval
Open

feat(settings): add configurable download progress refresh interval#2647
alchemyyy wants to merge 2 commits intoseerr-team:developfrom
alchemyyy:download-refresh-interval

Conversation

@alchemyyy
Copy link

@alchemyyy alchemyyy commented Mar 6, 2026

Description

Moves the hardcoded 15-second refresh interval for download progress to a user-configurable value under General Settings, tagged as Advanced. Users can now define the download sync job refresh interval in seconds directly from the UI — previously this was only achievable by manually editing the config file.

The lambda change across the SWR calls ensures the interval is recalculated on every poll cycle; once a download finishes, polling stops immediately.

I've been using this for quite a while and it works great, since Seerr already runs the proper API request to Radarr/Sonarr to force a progress refresh when the download sync job runs.

How Has This Been Tested?

  • Verified the new setting appears under Settings > General with "Advanced" badge
  • Confirmed changing the value persists after save and page reload
  • Checked that download progress bars poll at the configured interval
  • Validated input constraints (min 1s, max 300s)
  • Confirmed the job schedule seconds editor accepts numeric input correctly

Screenshots / Logs (if applicable)

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy) — Assisted by Claude Opus
  • 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)

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a configurable download refresh interval setting (1–300 seconds) that controls how often the app updates download progress information across collections, movies, TV series, and requests.
    • Enhanced job scheduler settings with a numeric input field for more precise seconds control (1–59 range).

@alchemyyy alchemyyy requested a review from a team as a code owner March 6, 2026 18:19
@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

A new configurable downloadRefreshInterval setting (milliseconds) was added to backend and frontend settings, initialized with a default. Multiple UI components switched from a hardcoded 15000ms SWR refreshInterval to dynamic functions that compute intervals from latest data and the user-configured setting. A new settings form field and i18n keys were added.

Changes

Cohort / File(s) Summary
Backend Settings Interfaces & Logic
server/interfaces/api/settingsInterfaces.ts, server/lib/settings/index.ts
Added downloadRefreshInterval: number to public and main settings types, introduced defaults and normalization, and expose normalized value in public settings.
Frontend Component SWR Refresh Intervals
src/components/CollectionDetails/index.tsx, src/components/MovieDetails/index.tsx, src/components/TvDetails/index.tsx, src/components/RequestCard/index.tsx, src/components/RequestList/RequestItem/index.tsx
Replaced fixed 15000ms SWR refreshInterval with functions that compute intervals using latestData, refreshIntervalHelper, and settings.currentSettings.downloadRefreshInterval. Added useSettings imports where needed.
Settings UI: Main
src/components/Settings/SettingsMain/index.tsx
Added a form field for downloadRefreshInterval (UI in seconds) with Formik/Yup validation, conversion between seconds and stored milliseconds, label, tip, and validation messages.
Settings UI: Jobs Cache
src/components/Settings/SettingsJobsCache/index.tsx
Replaced seconds select with a numeric input constrained to 1–59, dispatches clamped values, and shows inline value label; layout adjusted for horizontal alignment.
Default Settings & Context
src/context/SettingsContext.tsx, src/pages/_app.tsx
Added downloadRefreshInterval: 15000 to default/current settings to ensure the new value is present in context and initial props.
Internationalization
src/i18n/locale/en.json
Added i18n keys and messages for the new downloadRefreshInterval label, tip, and validation strings.

Sequence Diagram(s)

sequenceDiagram
  participant Component as UI Component
  participant SettingsCtx as SettingsContext
  participant Helper as refreshIntervalHelper
  participant API as Server API

  Component->>SettingsCtx: read settings.currentSettings.downloadRefreshInterval
  Component->>API: useSWR fetch (returns latestData)
  API-->>Component: latestData
  Component->>Helper: refreshIntervalHelper(returnCollectionDownloadItems(latestData), downloadRefreshInterval)
  Helper-->>Component: computedInterval (ms)
  Component->>Component: schedule next revalidation with computedInterval
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • 0xSysR3ll
  • gauthier-th

Poem

🐰 A twitch, a tick, the downloads hum along,
I swapped the hardcode for a setting — clever and strong.
Now each refresh listens to data and choice,
I nibble carrots and cheer with a happy voice.
thump-thump

🚥 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 'feat(settings): add configurable download progress refresh interval' accurately summarizes the main change: introducing a user-configurable setting for the download progress refresh interval.

✏️ 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.

Copy link

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/RequestList/RequestItem/index.tsx (1)

313-327: ⚠️ Potential issue | 🟠 Major

Pause request polling for offscreen rows.

This hook still refreshes /api/v1/request/${request.id} for every row, even before the row enters the viewport. With the new configurable interval going as low as 1 second, long request lists can generate a lot of hidden per-item traffic. Use the existing inView signal to return 0 until the item is visible.

♻️ Suggested change
   const { data: requestData, mutate: revalidate } = useSWR<
     NonFunctionProperties<MediaRequest>
   >(`/api/v1/request/${request.id}`, {
     fallbackData: request,
-    refreshInterval: (
-      latestData: NonFunctionProperties<MediaRequest> | undefined
-    ) =>
-      refreshIntervalHelper(
-        {
-          downloadStatus: latestData?.media.downloadStatus,
-          downloadStatus4k: latestData?.media.downloadStatus4k,
-        },
-        settings.currentSettings.downloadRefreshInterval
-      ),
+    refreshInterval: inView
+      ? (latestData: NonFunctionProperties<MediaRequest> | undefined) =>
+          refreshIntervalHelper(
+            {
+              downloadStatus: latestData?.media.downloadStatus,
+              downloadStatus4k: latestData?.media.downloadStatus4k,
+            },
+            settings.currentSettings.downloadRefreshInterval
+          )
+      : 0,
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/RequestList/RequestItem/index.tsx` around lines 313 - 327, The
per-row useSWR polling should be paused when the item is offscreen: update the
refreshInterval option in the useSWR call inside RequestItem to check the
existing inView signal and return 0 while inView is false (so no polling occurs
until the row is visible); keep calling refreshIntervalHelper with the same
downloadStatus payload when inView is true, and reference the useSWR invocation,
the refreshInterval option, refreshIntervalHelper, and the inView variable when
making the change.
🤖 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/lib/settings/index.ts`:
- Line 412: Clamp and normalize downloadRefreshInterval before
persisting/exposing: when merging req.body into settings.main (see
server/routes/settings/index.ts handling around merge at lines like 76-83)
validate and normalize the incoming downloadRefreshInterval to the supported
range (1–300 seconds) using the same logic as src/utils/refreshIntervalHelper.ts
(or call a shared clamp/normalize helper), convert units consistently (ms ↔ s)
if needed, store the normalized value on settings.main.downloadRefreshInterval,
and ensure the value emitted from server/lib/settings/index.ts
(downloadRefreshInterval) uses that normalized/clamped value; apply the same
normalization to the other occurrence mentioned (around 709).

In `@src/components/RequestCard/index.tsx`:
- Around line 247-256: The refresh callback in RequestCard (refreshInterval)
watches both latestData?.media.downloadStatus and downloadStatus4k, causing
polling to continue if the other variant is still downloading; instead scope the
watched status to the card's own variant using requestData.is4k. Update the
refreshInterval lambda in RequestCard to pass only the relevant status to
refreshIntervalHelper (e.g., pass { downloadStatus: requestData.is4k ?
latestData?.media.downloadStatus4k : latestData?.media.downloadStatus }) so
refreshIntervalHelper only sees the variant this card renders; ensure you
reference the existing refreshIntervalHelper and requestData.is4k symbols and
keep the callback signature the same.

In `@src/components/Settings/SettingsJobsCache/index.tsx`:
- Around line 283-293: The code currently rounds jobModalState.scheduleSeconds
to whole minutes when building jobScheduleCron (mutating jobScheduleCron[1])
which causes a mismatch with the UI text that still prints the raw seconds;
update the logic to compute an effective schedule value and use that both for
cron generation and display: e.g., derive const effectiveSeconds =
jobModalState.scheduleSeconds < 60 ? jobModalState.scheduleSeconds :
Math.round(jobModalState.scheduleSeconds / 60) * 60 (or compute minutes =
Math.ceil(...) if you prefer nearest/upward rounding), then set jobScheduleCron
appropriately (use `*/${minutes}` when minutes >= 1) and change the UI display
to use effectiveSeconds or a humanized string like “Every X minute(s)” when
effectiveSeconds % 60 === 0 so the displayed text matches the actual schedule;
update references to jobModalState.scheduleSeconds in the cron construction and
in the render where the "Every ... seconds" text is produced.

In `@src/components/Settings/SettingsMain/index.tsx`:
- Around line 125-127: Replace the hardcoded English strings in the Yup
validation for downloadRefreshInterval (the .min and .max calls in
SettingsMain/index.tsx) with localized messages: add new defineMessages entries
(e.g., downloadRefreshIntervalMin and downloadRefreshIntervalMax) and use
intl.formatMessage to pass the localized text into Yup.number().min(...) and
.max(...), mirroring how other fields in this form are localized.

---

Outside diff comments:
In `@src/components/RequestList/RequestItem/index.tsx`:
- Around line 313-327: The per-row useSWR polling should be paused when the item
is offscreen: update the refreshInterval option in the useSWR call inside
RequestItem to check the existing inView signal and return 0 while inView is
false (so no polling occurs until the row is visible); keep calling
refreshIntervalHelper with the same downloadStatus payload when inView is true,
and reference the useSWR invocation, the refreshInterval option,
refreshIntervalHelper, and the inView variable when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 875ca4a9-ce98-41f7-bf33-786d658adcd7

📥 Commits

Reviewing files that changed from the base of the PR and between ff4ab21 and fb5d191.

📒 Files selected for processing (12)
  • server/interfaces/api/settingsInterfaces.ts
  • server/lib/settings/index.ts
  • src/components/CollectionDetails/index.tsx
  • src/components/MovieDetails/index.tsx
  • src/components/RequestCard/index.tsx
  • src/components/RequestList/RequestItem/index.tsx
  • src/components/Settings/SettingsJobsCache/index.tsx
  • src/components/Settings/SettingsMain/index.tsx
  • src/components/TvDetails/index.tsx
  • src/context/SettingsContext.tsx
  • src/i18n/locale/en.json
  • src/pages/_app.tsx

Copy link
Member

@gauthier-th gauthier-th left a comment

Choose a reason for hiding this comment

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

Please edit the PR description according to the PR template in https://github.com/seerr-team/seerr/blob/develop/.github/PULL_REQUEST_TEMPLATE.md

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a user-configurable “download progress refresh interval” setting (previously hardcoded at 15s) and wires it through settings + polling logic so download progress polling can be tuned and stops promptly when downloads complete.

Changes:

  • Introduces downloadRefreshInterval as a persisted/public setting (stored in ms; edited in UI as seconds).
  • Updates multiple SWR polling sites to compute refreshInterval dynamically using the latest polled data + the configured interval.
  • Reworks the Jobs “seconds” schedule editor from a small dropdown to a numeric seconds input and adjusts default seconds value.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/pages/_app.tsx Adds downloadRefreshInterval to initial public settings bootstrap defaults.
src/context/SettingsContext.tsx Adds downloadRefreshInterval to client-side default settings context.
src/i18n/locale/en.json Adds English strings for the new General Settings field/tooltip.
src/components/Settings/SettingsMain/index.tsx Adds the new setting to the General Settings form (validation, load/save conversion sec↔ms, UI field).
src/components/MovieDetails/index.tsx Uses settings-driven polling interval via SWR refreshInterval callback.
src/components/TvDetails/index.tsx Uses settings-driven polling interval via SWR refreshInterval callback.
src/components/CollectionDetails/index.tsx Uses settings-driven polling interval via SWR refreshInterval callback.
src/components/RequestCard/index.tsx Uses settings-driven polling interval for request polling.
src/components/RequestList/RequestItem/index.tsx Uses settings-driven polling interval for request polling.
src/components/Settings/SettingsJobsCache/index.tsx Changes seconds scheduling UI to numeric input and updates cron generation logic/default.
server/lib/settings/index.ts Adds downloadRefreshInterval to main settings defaults and exposes it via fullPublicSettings.
server/interfaces/api/settingsInterfaces.ts Extends PublicSettingsResponse with downloadRefreshInterval.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +565 to +568
type="text"
inputMode="numeric"
className="short"
placeholder={15}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This field is meant to be numeric seconds (with 1–300 constraints), but it’s rendered as type="text". That makes it easier to enter non-integer/invalid values and relies on Yup casting. Prefer type="number" with min, max, and step={1} (and/or a custom onChange that stores a number) so the value is consistently numeric before conversion to ms on submit.

Suggested change
type="text"
inputMode="numeric"
className="short"
placeholder={15}
type="number"
min={1}
max={300}
step={1}
className="short"
placeholder={15}
onChange={(e) => {
const value = e.target.value;
setFieldValue(
'downloadRefreshInterval',
value === '' ? '' : Number(value)
);
}}

Copilot uses AI. Check for mistakes.
intl.formatMessage(messages.validationUrlTrailingSlash),
(value) => !value || !value.endsWith('/')
),
downloadRefreshInterval: Yup.number()
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The schema currently allows non-integer seconds (e.g., 0.5, 15.2) and will also show Yup’s default “must be a number” message if the field is blank/NaN. If the intent is an integer seconds interval, consider adding .integer() and a .typeError(...) message (and possibly .required()) to keep validation consistent and user-friendly.

Suggested change
downloadRefreshInterval: Yup.number()
downloadRefreshInterval: Yup.number()
.typeError('Must be a whole number of seconds.')
.integer('Must be a whole number of seconds.')

Copilot uses AI. Check for mistakes.
Comment on lines +283 to +292
if (jobModalState.scheduleSeconds < 60) {
jobScheduleCron.splice(
0,
2,
`*/${jobModalState.scheduleSeconds}`,
'*'
);
} else {
const minutes = Math.round(jobModalState.scheduleSeconds / 60);
jobScheduleCron[1] = `*/${minutes}`;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The “seconds” scheduler UI allows values up to 300, but cron expressions can’t represent arbitrary intervals > 60s precisely. The current logic rounds seconds to minutes (Math.round(seconds / 60)), which can schedule at a different frequency than the user-entered value (e.g., 89s -> 1m, 91s -> 2m) while the UI still says “Every X seconds”. Consider either (a) restricting seconds input to <= 60 (or <= 59) for second-based jobs, or (b) changing the UI/label to minutes once >= 60 and using a deterministic conversion (e.g., require multiples of 60) instead of rounding.

Suggested change
if (jobModalState.scheduleSeconds < 60) {
jobScheduleCron.splice(
0,
2,
`*/${jobModalState.scheduleSeconds}`,
'*'
);
} else {
const minutes = Math.round(jobModalState.scheduleSeconds / 60);
jobScheduleCron[1] = `*/${minutes}`;
const seconds = jobModalState.scheduleSeconds;
if (seconds < 60) {
jobScheduleCron.splice(0, 2, `*/${seconds}`, '*');
} else if (seconds % 60 === 0) {
const minutes = seconds / 60;
jobScheduleCron[1] = `*/${minutes}`;
} else {
// Cannot represent arbitrary second intervals > 60s precisely in cron.
addToast(intl.formatMessage(messages.jobScheduleEditFailed), {
appearance: 'error',
autoDismiss: true,
});
return;

Copilot uses AI. Check for mistakes.
Comment on lines 200 to 204
scheduleDays: 1,
scheduleHours: 1,
scheduleMinutes: 5,
scheduleSeconds: 30,
scheduleSeconds: 10,
});
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

scheduleSeconds default was changed to 10 in the reducer’s initial state, but opening the modal resets scheduleSeconds back to 30 (in the open action). This makes the new default ineffective in the main workflow. If the intention is to change the default, update the open action’s scheduleSeconds value too (or derive it from the job’s current cron schedule).

Copilot uses AI. Check for mistakes.
- Add server-side normalization to clamp downloadRefreshInterval to
  1000-300000ms, handling invalid/out-of-range values from config or API
- Scope SWR polling in RequestCard and RequestItem to the request's own
  variant (4k vs non-4k) so polling stops when that variant finishes
- Cap job scheduler seconds input to 59 and remove lossy minutes
  rounding, since cron seconds field only supports 0-59
- Revert scheduleSeconds default to 30s (consistent across initial
  state and open action)
- Change download refresh interval field to type="number" with
  min/max/step attributes
- Add .integer() and .typeError() to Yup validation to reject
  decimals and non-numeric input
- Localize Yup validation messages for downloadRefreshInterval via
  intl.formatMessage and new i18n keys

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@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/lib/settings/index.ts (1)

638-640: Consider normalizing in the setter for write-path consistency.

The main setter assigns raw data, so values set via the POST /main route bypass normalization until re-read through fullPublicSettings. Since the frontend validates and the public getter normalizes, this isn't critical, but normalizing in the setter would ensure consistency in the stored data and API responses.

🔧 Optional: Normalize in setter
 set main(data: MainSettings) {
-  this.data.main = data;
+  this.data.main = {
+    ...data,
+    downloadRefreshInterval: Settings.normalizeDownloadRefreshInterval(
+      data.downloadRefreshInterval
+    ),
+  };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/lib/settings/index.ts` around lines 638 - 640, The main setter
currently assigns raw MainSettings into this.data.main causing write-path
inconsistency; update the set main(data: MainSettings) to normalize the incoming
data before assignment (use the same normalization logic used by
fullPublicSettings or a shared normalizeMainSettings helper) so that
this.data.main always stores normalized values and API responses remain
consistent.
🤖 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/lib/settings/index.ts`:
- Around line 638-640: The main setter currently assigns raw MainSettings into
this.data.main causing write-path inconsistency; update the set main(data:
MainSettings) to normalize the incoming data before assignment (use the same
normalization logic used by fullPublicSettings or a shared normalizeMainSettings
helper) so that this.data.main always stores normalized values and API responses
remain consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 15ec11bc-f8c0-4733-be6e-bc0a643bf045

📥 Commits

Reviewing files that changed from the base of the PR and between fb5d191 and 3590b02.

📒 Files selected for processing (6)
  • server/lib/settings/index.ts
  • src/components/RequestCard/index.tsx
  • src/components/RequestList/RequestItem/index.tsx
  • src/components/Settings/SettingsJobsCache/index.tsx
  • src/components/Settings/SettingsMain/index.tsx
  • src/i18n/locale/en.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/components/RequestList/RequestItem/index.tsx
  • src/i18n/locale/en.json
  • src/components/Settings/SettingsJobsCache/index.tsx
  • src/components/RequestCard/index.tsx

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.

3 participants