feat(settings): add configurable download progress refresh interval#2647
feat(settings): add configurable download progress refresh interval#2647alchemyyy wants to merge 2 commits intoseerr-team:developfrom
Conversation
📝 WalkthroughWalkthroughA 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ 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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorPause 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 existinginViewsignal to return0until 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
📒 Files selected for processing (12)
server/interfaces/api/settingsInterfaces.tsserver/lib/settings/index.tssrc/components/CollectionDetails/index.tsxsrc/components/MovieDetails/index.tsxsrc/components/RequestCard/index.tsxsrc/components/RequestList/RequestItem/index.tsxsrc/components/Settings/SettingsJobsCache/index.tsxsrc/components/Settings/SettingsMain/index.tsxsrc/components/TvDetails/index.tsxsrc/context/SettingsContext.tsxsrc/i18n/locale/en.jsonsrc/pages/_app.tsx
gauthier-th
left a comment
There was a problem hiding this comment.
Please edit the PR description according to the PR template in https://github.com/seerr-team/seerr/blob/develop/.github/PULL_REQUEST_TEMPLATE.md
There was a problem hiding this comment.
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
downloadRefreshIntervalas a persisted/public setting (stored in ms; edited in UI as seconds). - Updates multiple SWR polling sites to compute
refreshIntervaldynamically 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.
| type="text" | ||
| inputMode="numeric" | ||
| className="short" | ||
| placeholder={15} |
There was a problem hiding this comment.
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.
| 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) | |
| ); | |
| }} |
| intl.formatMessage(messages.validationUrlTrailingSlash), | ||
| (value) => !value || !value.endsWith('/') | ||
| ), | ||
| downloadRefreshInterval: Yup.number() |
There was a problem hiding this comment.
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.
| downloadRefreshInterval: Yup.number() | |
| downloadRefreshInterval: Yup.number() | |
| .typeError('Must be a whole number of seconds.') | |
| .integer('Must be a whole number of seconds.') |
| if (jobModalState.scheduleSeconds < 60) { | ||
| jobScheduleCron.splice( | ||
| 0, | ||
| 2, | ||
| `*/${jobModalState.scheduleSeconds}`, | ||
| '*' | ||
| ); | ||
| } else { | ||
| const minutes = Math.round(jobModalState.scheduleSeconds / 60); | ||
| jobScheduleCron[1] = `*/${minutes}`; |
There was a problem hiding this comment.
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.
| 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; |
| scheduleDays: 1, | ||
| scheduleHours: 1, | ||
| scheduleMinutes: 5, | ||
| scheduleSeconds: 30, | ||
| scheduleSeconds: 10, | ||
| }); |
There was a problem hiding this comment.
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).
- 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>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
server/lib/settings/index.ts (1)
638-640: Consider normalizing in the setter for write-path consistency.The
mainsetter assigns raw data, so values set via the POST/mainroute bypass normalization until re-read throughfullPublicSettings. 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
📒 Files selected for processing (6)
server/lib/settings/index.tssrc/components/RequestCard/index.tsxsrc/components/RequestList/RequestItem/index.tsxsrc/components/Settings/SettingsJobsCache/index.tsxsrc/components/Settings/SettingsMain/index.tsxsrc/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
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?
Screenshots / Logs (if applicable)
Checklist:
pnpm buildpnpm i18n:extractSummary by CodeRabbit
Release Notes