feat(rss): add native rss support - #801
Conversation
WalkthroughAdds end-to-end RSS support: backend REST handlers and SSE, SyncManager RSS proxies and qBittorrent capability flag; OpenAPI schemas; frontend types, API client, hooks and SSE client; a new authenticated /rss route, navigation entries, and a full RSS management UI. No public API removals. Changes
sequenceDiagram
participant User
participant UI as React UI (RSSPage)
participant Hooks as React Query Hooks
participant SSE as RSSEventSource (client)
participant API as ApiClient (/api/instances/{id}/rss)
participant Server as Backend (RSSHandler / RSSSSEHandler)
participant SM as SyncManager
participant QBT as qBittorrent Client
User->>UI: Open RSS page
UI->>Hooks: useRSSFeeds(instanceId)
Hooks->>SSE: connect /instances/{id}/rss/events
SSE-->>Hooks: feeds_update
Hooks-->>UI: update cache -> re-render
User->>UI: Add Feed (with optional folder)
UI->>Hooks: mutate addRSSFeed(data)
Hooks->>API: POST /instances/{id}/rss/feeds
API->>Server: HTTP request
Server->>SM: AddRSSFeed(ctx, id, url, path)
SM->>QBT: client.AddRSSFeed(...)
QBT-->>SM: success
alt folder specified
Server->>SM: MoveRSSItem(ctx, id, feedPath, destPath)
SM->>QBT: client.MoveRSSItem(...)
QBT-->>SM: moved
end
SM-->>Server: result (may include warning)
Server-->>API: JSON (may include WarningResponse)
API-->>Hooks: mutation success -> invalidate feeds
Hooks->>API: GET /instances/{id}/rss/items
API-->>Hooks: updated items
Hooks-->>UI: updated feed tree
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (7)
web/src/routes/_authenticated/rss.tsx (1)
1-56: URL‑driven RSS route state looks solidThe zod search schema,
Route.useSearch/useNavigatewiring, and the wayRSSPageprops are derived from search params are all consistent with the rest of the router setup and give nice deep‑linkable state. If you ever see confusion from stale selections, you could optionally clearruleNamewhen switching to the feeds tab (and vice versa), but it’s not required for correctness.web/src/hooks/useRSS.ts (2)
22-29: IncludewithDatain the feeds query key to avoid cache collisions
useRSSFeedspasseswithDataintoapi.getRSSItems(instanceId, withData)butrssKeys.feeds(instanceId)only keys oninstanceId. If you ever calluseRSSFeedswith differentwithDatavalues for the same instance, they’ll share a cache entry and you can get stale/mismatched data.You can future‑proof this by keying on the effective flag:
-export const rssKeys = { - all: ["rss"] as const, - feeds: (instanceId: number) => [...rssKeys.all, "feeds", instanceId] as const, +export const rssKeys = { + all: ["rss"] as const, + feeds: (instanceId: number, withData: boolean) => + [...rssKeys.all, "feeds", instanceId, withData] as const, rules: (instanceId: number) => [...rssKeys.all, "rules", instanceId] as const, matching: (instanceId: number, ruleName: string) => [...rssKeys.all, "matching", instanceId, ruleName] as const, } @@ -export function useRSSFeeds(instanceId: number, options?: { enabled?: boolean; withData?: boolean }) { +export function useRSSFeeds( + instanceId: number, + options?: { enabled?: boolean; withData?: boolean }, +) { const shouldEnable = (options?.enabled ?? true) && instanceId > 0 const withData = options?.withData ?? true return useQuery({ - queryKey: rssKeys.feeds(instanceId), + queryKey: rssKeys.feeds(instanceId, withData), queryFn: () => api.getRSSItems(instanceId, withData),Also applies to: 35-45
177-221: Consider invalidating matching‑articles queries when rules change
useSetRSSRule,useRenameRSSRule,useRemoveRSSRule, anduseReprocessRSSRulesonly invalidaterssKeys.rules(instanceId)(or feeds), but notrssKeys.matching(instanceId, ruleName). If the UI keeps a rule preview panel open that usesuseRSSMatchingArticles, it may continue showing stale matches after rule edits/renames/removals or a reprocess.Not strictly required today, but you could defensively clear those caches when you have the rule name available, e.g.:
export function useSetRSSRule(instanceId: number) { const queryClient = useQueryClient() return useMutation({ mutationFn: (data: SetRSSRuleRequest) => api.setRSSRule(instanceId, data), onSuccess: (_result, variables) => { queryClient.invalidateQueries({ queryKey: rssKeys.rules(instanceId) }) + if (variables?.name) { + queryClient.invalidateQueries({ + queryKey: rssKeys.matching(instanceId, variables.name), + }) + } }, }) }and similarly for rename/remove, using the old/new names as appropriate.
web/src/pages/RSSPage.tsx (3)
495-507: Hardcoded delay for query invalidation is fragile.The 2000ms timeout is arbitrary and may not align with actual refresh completion time. If the server refresh takes longer, users see stale data; if shorter, there's unnecessary delay.
Consider invalidating immediately after the mutation succeeds, or using
onSettledcallback if you want to delay:const handleRefreshFeed = async (path: string) => { try { await refreshFeed.mutateAsync({ itemPath: path }) toast.success("Feed refresh triggered") - // Invalidate to pick up changes - setTimeout(() => { - queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) }) - }, 2000) + // Invalidate immediately - the server has triggered the refresh + queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) }) } catch (err) {
1518-1521: Consider consistent parseInt radix usage.While
parseInt(e.target.value) || 0works, explicitly specifying radix 10 is more defensive and consistent with other usages in this file:- onChange={(e) => setIgnoreDays(parseInt(e.target.value) || 0)} + onChange={(e) => setIgnoreDays(parseInt(e.target.value, 10) || 0)}Same applies to line 1810 in EditRuleDialog.
1286-1295: Consider extracting shared form logic between Add and Edit rule dialogs.
AddRuleDialogandEditRuleDialogshare approximately 90% identical code (form fields, state management, validation). Consider extracting a sharedRuleFormcomponent or custom hook to reduce duplication:// Example shared hook function useRuleFormState(initialRule?: RSSAutoDownloadRule) { const [mustContain, setMustContain] = useState(initialRule?.mustContain ?? "") // ... other state return { mustContain, setMustContain, /* ... */ } }This would make future maintenance easier when adding new rule fields.
internal/api/handlers/rss.go (1)
176-263: AddFeed folder workaround has potential race condition but handles failures gracefully.The logic to work around qBittorrent's API limitation (path param is full path, not parent folder) is sound. The theoretical race condition (concurrent feed additions) is mitigated by:
- Warning responses for partial failures
- Low likelihood in typical usage
The implementation is pragmatic. Consider adding a comment explaining why this approach is necessary:
+ // qBittorrent's AddFeed API uses 'path' as the full item path, not the parent folder. + // To add a feed to a specific folder, we must: + // 1. Add the feed to root + // 2. Find the newly created feed by comparing before/after state + // 3. Move it to the target folder if targetFolder != "" {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (12)
go.mod(1 hunks)internal/api/handlers/helpers.go(1 hunks)internal/api/handlers/rss.go(1 hunks)internal/api/server.go(2 hunks)internal/qbittorrent/sync_manager.go(1 hunks)web/src/components/layout/Sidebar.tsx(1 hunks)web/src/hooks/useRSS.ts(1 hunks)web/src/lib/api.ts(2 hunks)web/src/pages/RSSPage.tsx(1 hunks)web/src/routeTree.gen.ts(11 hunks)web/src/routes/_authenticated/rss.tsx(1 hunks)web/src/types/index.ts(2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-11-06T12:11:04.963Z
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: internal/services/crossseed/service.go:1045-1082
Timestamp: 2025-11-06T12:11:04.963Z
Learning: The autobrr/qui project uses a custom go-qbittorrent client library (github.com/autobrr/go-qbittorrent) that supports both "paused" and "stopped" parameters when adding torrents via the options map. Both parameters should be set together when controlling torrent start state, as seen in internal/services/crossseed/service.go and throughout the codebase.
Applied to files:
go.mod
📚 Learning: 2025-11-25T22:46:03.762Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 632
File: internal/backups/service.go:1401-1404
Timestamp: 2025-11-25T22:46:03.762Z
Learning: In qui's backup service (internal/backups/service.go), background torrent downloads initiated during manifest import intentionally use a fire-and-forget pattern with the shared service context (s.ctx). Per-run cancellation is not needed, as orphaned downloads completing after run deletion are considered harmless and acceptable. This design prioritizes simplicity over per-run lifecycle management for background downloads.
Applied to files:
go.mod
📚 Learning: 2025-12-11T08:40:01.329Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:40:01.329Z
Learning: In autobrr/qui's internal/services/reannounce/service.go, the hasHealthyTracker, getProblematicTrackers, and getHealthyTrackers functions intentionally match qbrr's lenient tracker health logic (skip unregistered trackers and check if any other tracker is healthy) rather than go-qbittorrent's strict isTrackerStatusOK logic (which treats unregistered as an immediate failure). For multi-tracker torrents, if one tracker is working, reannouncing won't help. The duplication of the health check logic across these three functions is acceptable as it's a simple one-liner, and extracting it would add unnecessary complexity.
Applied to files:
go.mod
📚 Learning: 2025-11-28T20:32:30.126Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go:209-212
Timestamp: 2025-11-28T20:32:30.126Z
Learning: Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go
Learning: The cross-seed recheck-resume worker intentionally runs for the process lifetime and keys pending entries by hash only. This is acceptable under the current constraint that background seeded-search runs operate on a single instance at a time; graceful shutdown and instanceID|hash keying are deferred by design.
Applied to files:
internal/api/server.go
🧬 Code graph analysis (6)
internal/api/server.go (2)
internal/api/handlers/rss.go (1)
NewRSSHandler(25-29)web/src/routes/_authenticated/rss.tsx (1)
Route(16-19)
web/src/hooks/useRSS.ts (2)
web/src/lib/api.ts (1)
api(1823-1823)web/src/types/index.ts (10)
AddRSSFeedRequest(1647-1651)AddRSSFolderRequest(1643-1645)RemoveRSSItemRequest(1668-1670)MoveRSSItemRequest(1663-1666)RefreshRSSItemRequest(1672-1674)SetRSSFeedURLRequest(1653-1656)SetRSSFeedRefreshIntervalRequest(1658-1661)MarkRSSAsReadRequest(1676-1679)SetRSSRuleRequest(1681-1684)RenameRSSRuleRequest(1686-1688)
web/src/routes/_authenticated/rss.tsx (1)
web/src/pages/RSSPage.tsx (1)
RSSPage(109-436)
internal/qbittorrent/sync_manager.go (1)
web/src/types/index.ts (4)
RSSItems(1588-1590)RSSRules(1638-1638)RSSAutoDownloadRule(1618-1636)RSSMatchingArticles(1640-1640)
web/src/types/index.ts (1)
internal/api/handlers/helpers.go (1)
WarningResponse(22-24)
internal/api/handlers/rss.go (1)
internal/api/handlers/helpers.go (3)
RespondError(39-43)RespondJSON(27-36)WarningResponse(22-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (19)
go.mod (1)
13-13: Prerelease version requires verification before merge.The go-qbittorrent dependency is pinned to v1.15.0-rc1, a prerelease version. The previous stable release is v1.14.0, and no stable v1.15.0 release currently exists. Confirm that:
- Using a prerelease is acceptable for this codebase's stability requirements
- The RSS API methods referenced in this PR are available in v1.15.0-rc1
- No dependency conflicts exist with other direct/indirect dependencies
internal/api/handlers/helpers.go (1)
21-24: Shared WarningResponse type looks appropriateDefining a simple exported
WarningResponsealigned with the JSON contract (warning/ omitempty) is fine and consistent with the existingErrorResponse.web/src/components/layout/Sidebar.tsx (1)
71-75: RSS sidebar entry wiring is consistentThe new "RSS" nav item reuses the existing navigation pattern (href, icon, active state) and matches the
/rssauthenticated route, so this integration looks good.web/src/types/index.ts (2)
19-22: Frontend WarningResponse matches backend contractThe generic
WarningResponsewith optionalwarningaligns with the GoWarningResponsestruct and gives you a reusable way to type non-fatal warning payloads.
1563-1689: RSS domain model and request types are coherentThe RSS article/feed tree, rule/torrent param models, and the various request DTOs (add/move/remove/refresh, rules, matching) line up cleanly with qBittorrent’s RSS concepts and should be sufficient for the hooks and API client.
internal/api/server.go (1)
274-275: RSS handler wiring under instance routes is consistentCreating
rssHandlerfroms.syncManagerand mounting it under/instances/{instanceID}/rssinside the authenticated group matches the existing pattern for instance-scoped features and keeps routing coherent.Also applies to: 444-447
web/src/routeTree.gen.ts (1)
19-20: Generated route tree entries for RSS are consistentThe added
AuthenticatedRssRoutewiring (imports,updatecall,/rssfullPath/toentries, and/_authenticated/rssid + children) matches how other authenticated child routes are modeled in this generated file. No manual edits needed.Also applies to: 61-65, 107-113, 121-127, 138-144, 155-161, 169-175, 185-191, 251-257, 325-337
web/src/pages/RSSPage.tsx (6)
1-97: LGTM - Imports and type definitions are well-structured.The imports are organized logically, separating UI components, hooks, and types. The type guard
isRSSFeedimport from types is a good pattern for type narrowing.
109-156: LGTM - Instance selection and query setup are well-implemented.Good use of
enabled: instanceId > 0to prevent queries when no instance is selected. The auto-selection logic properly prioritizes connected instances.
585-588: LGTM - Path separator is correctly hardcoded for qBittorrent API.The backslash separator (
\\) is correct as qBittorrent's RSS API uses this format regardless of the server's operating system.
1085-1091: Using array index as key is acceptable here.Since the matching articles are simple strings without unique identifiers and this is a read-only preview list, using the index as key is acceptable. If performance issues arise with large lists, consider using a hash of
feedUrl + title + idx.
1617-1634: LGTM - Form initialization effect is correctly implemented.The useEffect properly initializes form state when the rule prop changes, with all field mappings including fallbacks for legacy field names (
savePath,assignedCategory).
1879-1931: LGTM - Helper functions are well-implemented.The recursive helper functions correctly handle the nested RSS items structure with proper type guards using
isRSSFeed().internal/api/handlers/rss.go (3)
31-53: LGTM - Routes are well-organized following RESTful conventions.The route structure cleanly separates feed/folder management, article management, and auto-download rules. Good use of appropriate HTTP methods.
106-126: LGTM - Consistent error handling pattern.The handler follows the established pattern: parse instance ID, check disabled state, log errors with context, and return appropriate HTTP status codes.
617-625: LGTM - Instance ID parsing is robust.The helper correctly validates the instance ID and returns appropriate errors for invalid input.
web/src/lib/api.ts (3)
76-91: LGTM - RSS types are properly imported.The new RSS-related types are imported consistently with existing patterns.
1799-1814: LGTM - Proper URL encoding for rule names.Using
encodeURIComponent(ruleName)correctly handles rule names with special characters in the URL path.
1724-1820: LGTM - RSS API methods are consistently implemented.All new RSS methods follow the established patterns in the API client:
- Proper HTTP methods for each operation
- JSON body serialization
- Type-safe return types
- URL encoding where needed
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
web/src/pages/RSSPage.tsx (2)
498-510: Consider using mutation'sonSuccesscallback for cache invalidation instead of arbitrary timeout.The 2-second timeout for cache invalidation is fragile - the refresh operation may complete faster or slower than expected. Consider leveraging React Query's built-in invalidation mechanisms or the mutation's
onSettledcallback for more reliable cache updates.const handleRefreshFeed = async (path: string) => { try { await refreshFeed.mutateAsync({ itemPath: path }) toast.success("Feed refresh triggered") - // Invalidate to pick up changes - setTimeout(() => { - queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) }) - }, 2000) + // The mutation hook should handle invalidation via onSuccess/onSettled } catch (err) { const message = err instanceof Error ? err.message : "Failed to refresh feed" toast.error(message) } }Alternatively, if the backend refresh is asynchronous and doesn't return updated data, consider polling or using a more sophisticated approach with
refetchIntervaltemporarily enabled.
1676-1692: Missing dependency warning in useEffect.The useEffect only has
[rule]as a dependency but doesn't need additional ones since the setters are stable. However, this pattern could cause issues if the dialog is opened with the same rule twice after external changes. Consider addingopento dependencies or resetting form when dialog opens.The current implementation works correctly because the
ruleprop changes when editing different rules. The React linter might warn about this, but the behavior is intentional since the setters fromuseStateare stable.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
internal/web/swagger/openapi.yaml(3 hunks)web/src/pages/RSSPage.tsx(1 hunks)
🔇 Additional comments (21)
internal/web/swagger/openapi.yaml (7)
3209-3265: LGTM! RSS items endpoints are well-defined.The RSS items endpoints follow established patterns with proper request/response schemas, appropriate HTTP methods, and consistent error handling (400, 409, 500 responses).
3267-3328: LGTM! Move and refresh endpoints follow REST conventions.The move and refresh operations correctly use POST methods for action-oriented operations with appropriate request body schemas.
3330-3400: LGTM! Folder and feed creation endpoints are properly defined.The folder and feed creation endpoints correctly return 201 status codes. The feed endpoint appropriately includes the WarningResponse schema for partial success scenarios.
3402-3503: LGTM! Feed URL, interval, and article read endpoints are well-structured.These endpoints properly handle feed modifications with appropriate PUT methods for updates and POST for marking articles as read.
3505-3669: LGTM! RSS rules management endpoints are comprehensive.The rules endpoints provide full CRUD operations plus reprocess, rename, and preview capabilities. The response schema using
additionalPropertiesfor the rules map is appropriate for the key-value structure.
4892-5030: LGTM! RSS schemas are well-defined with appropriate types.The schema definitions properly model the RSS data structures:
RSSItemsuses recursiveoneOffor nested feeds/foldersRSSFeedandRSSArticlecapture all relevant feed metadataRSSAutoDownloadRuleincludes both newtorrentParamsand legacy fields for backward compatibilityRSSRuleTorrentParamscomprehensively covers qBittorrent's torrent parameters
5053-5054: LGTM! RSS tag added to the tags section.The new RSS tag is properly added with a descriptive description for grouping the RSS-related endpoints.
web/src/pages/RSSPage.tsx (14)
1-104: LGTM! Imports and type definitions are well-organized.The imports are logically grouped (React, UI libraries, custom hooks, components, types) and the
RSSPagePropsinterface is properly typed with appropriate callback signatures.
126-135: LGTM! Auto-selection effect is properly implemented.The effect correctly auto-selects the first connected instance (or falls back to the first instance) when no selection exists. Dependencies are correctly specified.
150-160: LGTM! Query hooks are properly configured with conditional enabling.The queries are correctly disabled when
instanceIdis 0, preventing unnecessary API calls before an instance is selected.
248-266: LGTM! Main layout structure is clean and responsive.The header layout with title, instance selector, and settings popover is well-structured with appropriate responsive behavior.
602-730: LGTM! FeedTree component handles recursive structure well.The recursive component properly distinguishes between feeds and folders using
isRSSFeed, maintains expansion state locally, and renders appropriate UI with actions for each item type.
742-826: LGTM! ArticlesPanel and ArticleRow provide clean article display.The components handle empty states, date formatting, read/unread visual distinction, and external link actions appropriately. The
rel="noopener noreferrer"attributes on external links are correctly applied for security.
843-950: LGTM! RulesTab component properly manages rule operations.The component handles loading states, empty states, rule toggling, editing, and removal with appropriate toast feedback. The preview sheet integration for matching articles is well-implemented.
966-1105: LGTM! RuleCard provides excellent visual representation of rules.The component effectively displays rule configuration with:
- Color-coded badges for match/exclude patterns
- Status badges for regex, smart filter, and enabled state
- Metadata display with appropriate icons
- Clear visual feedback for selected state
1118-1164: LGTM! RulePreviewSheet shows matching articles effectively.The component properly handles loading, empty states, and displays matching articles grouped by feed URL with appropriate scrolling behavior.
1179-1273: LGTM! AddFeedDialog handles form submission correctly.The dialog properly validates required fields, resets form state on success, and appropriately distinguishes between success and warning responses from the API.
1285-1338: LGTM! AddFolderDialog is simple and functional.The dialog provides clear guidance about folder path format and handles form submission correctly.
1353-1631: LGTM! AddRuleDialog provides comprehensive rule configuration.The dialog covers all necessary rule configuration options with proper form state management, validation, and reset functionality. The use of sentinel values for empty/default selections is appropriate.
1937-2001: LGTM! Helper functions correctly traverse recursive RSS structure.The helper functions properly use the
isRSSFeedtype guard and handle the recursive feed/folder structure. The backslash path separator matches qBittorrent's convention.
2007-2091: LGTM! RssSettingsPopover provides convenient settings access.The popover offers quick access to RSS settings with appropriate input controls and disabled states during updates.
… state - Remove empty ItemPath rejection in RefreshItem handler to allow refreshing all feeds with empty path - Preserve original RSS auto-downloading preference in ReprocessRSSRules by fetching state before toggle and restoring if it was disabled - Fix addRSSFeed return type to include undefined for empty responses
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
internal/api/handlers/rss.go(1 hunks)internal/qbittorrent/sync_manager.go(1 hunks)web/src/lib/api.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
internal/api/handlers/rss.go (2)
internal/qbittorrent/sync_manager.go (1)
SyncManager(159-182)internal/api/handlers/helpers.go (3)
RespondError(39-43)RespondJSON(27-36)WarningResponse(22-24)
internal/qbittorrent/sync_manager.go (1)
web/src/types/index.ts (4)
RSSItems(1588-1590)RSSRules(1638-1638)RSSAutoDownloadRule(1618-1636)RSSMatchingArticles(1640-1640)
🔇 Additional comments (11)
internal/qbittorrent/sync_manager.go (2)
4803-4941: RSS proxy methods are consistent, minimal, and correctly layeredThe new RSS methods correctly follow the existing SyncManager pattern: acquire client once, add useful error context, and delegate to the qBittorrent client APIs without extra state. No issues spotted here.
4943-4977: ReprocessRSSRules now preserves original auto-download settingThis implementation fixes the prior behavior by reading
AppPreferences, forcing afalse -> truetransition to trigger processing, and restoring the original disabled state when appropriate. Error handling is straightforward and avoids silently changing user preferences.internal/api/handlers/rss.go (7)
32-54: RSS routes wiring is clear and aligned with SyncManager surfaceRoute registration cleanly groups item, article, and rule operations under
/rssand matches the SyncManager methods one-for-one, which should keep server/client wiring straightforward.
107-157: GetItems/AddFolder handlers follow existing patterns and look correctInstance ID parsing, JSON decoding, validation, and error handling for
GetItemsandAddFolderare consistent with other handlers in this package. No issues spotted.
279-337: Feed URL and refresh-interval handlers are straightforward
SetFeedURLandSetFeedRefreshIntervalvalidate required fields, delegate to the SyncManager, and surface errors consistently. No additional validation seems necessary at this layer.
339-397: MoveItem/RemoveItem validation and delegation look correctBoth handlers enforce required identifiers (
itemPath/path), forward to the SyncManager, and userespondIfInstanceDisabledfor capability/instance errors. Behavior is in line with the rest of the API.
399-456: RefreshItem now correctly supports “refresh all” semanticsAllowing an empty, trimmed
itemPathand passing it through toRefreshRSSItemenables the “Refresh All” UI flow without returning 400, while still validating JSON shape and logging failures with context. This addresses the previous backend/frontend mismatch cleanly.
458-613: RSS rules and reprocess handlers are consistent and well-scopedRule CRUD, preview, and
ReprocessRulesall reuse common instance parsing, validate path params/body as needed, and delegate to the corresponding SyncManager APIs. The comments aroundReprocessRulesmatch the SyncManager semantics (toggling auto-downloading while preserving original state).
617-625: parseRSSInstanceID helper is simple and robustCentralizing instance ID parsing and validation here (including rejecting non-positive IDs) keeps the individual handlers focused and avoids repeated parsing logic.
web/src/lib/api.ts (2)
75-91: RSS types are correctly surfaced from shared type definitionsImporting
RSSItems, rules/matching types, request payloads, andWarningResponsehere keeps the API client aligned with the shared frontend types module and avoids ad-hoc shapes.
1723-1819: RSS API client methods align with backend routes and handle empty responsesThe new RSS methods mirror the server routes (
/instances/{id}/rss/...) and use the appropriate request/response types. In particular,addRSSFeedreturningPromise<WarningResponse | undefined>matches therequest()helper’s behavior for empty bodies, so callers can safely handle the optional warning payload.
When adding a feed to a folder, if the initial GetRSSItems call fails, the before/after diff would treat all feeds as "new" and potentially move a random existing feed. Now we track whether we have a valid baseline and skip the diff comparison entirely if not, falling through to the "could not identify feed" warning path.
- Remove duplicate parseRSSInstanceID, use existing parseInstanceID - Use SetRSSAutoDownloadingEnabledCtx for proper context propagation - Add error handling callbacks to enable RSS/auto-download buttons - Update go-qbittorrent dependency with new Ctx variants
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
internal/api/handlers/rss.go (1)
401-428: RefreshItem: emptyitemPathnow correctly supports “refresh all”Allowing an empty, trimmed
itemPathto flow through toRefreshRSSItemaligns with the frontend’s{ itemPath: "" }“Refresh All” behavior. Trimming whitespace but not rejecting the empty string is a good balance between validation and flexibility.This resolves the earlier 400‑on‑empty issue while keeping the handler simple.
🧹 Nitpick comments (7)
web/src/pages/RSSPage.tsx (6)
105-147: Instance selection: avoid using0as a synthetic selected value in the UI controlRight now
instanceIdfalls back to0, and that value is fed into the<Select>even when no instance is actually selected. This means the select value can be"0"while there is no matchingSelectItem, which is a small mismatch between internal state and the control.You can keep using
0as the sentinel for query enabling, but drive the select fromselectedInstanceIdinstead so the UI reflects “no selection” as an empty value:- const instanceId = selectedInstanceId ?? 0 + const instanceId = selectedInstanceId ?? 0 … - const renderInstanceSelector = () => ( - <Select value={instanceId?.toString() ?? ""} onValueChange={handleInstanceSelection}> + const renderInstanceSelector = () => ( + <Select value={selectedInstanceId?.toString() ?? ""} onValueChange={handleInstanceSelection}>This keeps the UX cleaner without affecting the query logic.
319-403: Refresh / reprocess actions are wired cleanly, but consider centralizing toasts and invalidationThe feed refresh and rules reprocess buttons correctly call their respective mutations, reflect pending state, and show user‑friendly toasts. One small follow‑up for maintainability would be to move the common
mutate+ toast pattern into thin helpers/hooks (e.g.useRefreshAllFeedsAction,useReprocessRulesAction) to avoid duplicating onSuccess/onError blocks as usage grows, but the current implementation is functionally fine.
966-1115: RuleCard: rich summary UI with sensible parsingParsing
mustContain/mustNotContaininto individual terms, rendering them as color‑coded pills, and surfacing status badges (regex, smart filter, active/disabled) all make the card very readable. Category/save‑path metadata and last‑match timestamp round it out nicely.As an optional improvement, you could use the shared
useDateTimeFormattershook forlastMatchto keep date formatting consistent with the rest of the app, but the currenttoLocaleDateString()is perfectly serviceable.
1180-1283: AddFeedDialog: consider slightly more defensive number parsing for refresh intervalThe dialog validates URL, resets state on success, and supports optional folder selection from
getFolderPaths. ForrefreshInterval, you currently do:onChange={(e) => setRefreshInterval(e.target.value ? parseInt(e.target.value, 10) : undefined) }This works in practice, but if the browser ever surfaces a transient non‑numeric string from the number input,
parseIntcould yieldNaNand propagate into state.You can make this more robust with a small tweak:
- value={refreshInterval ?? ""} - onChange={(e) => - setRefreshInterval(e.target.value ? parseInt(e.target.value, 10) : undefined) - } + value={refreshInterval ?? ""} + onChange={(e) => { + const raw = e.target.value.trim() + if (raw === "") { + setRefreshInterval(undefined) + return + } + const parsed = Number(raw) + setRefreshInterval(Number.isNaN(parsed) ? undefined : parsed) + }}Not critical, but it makes the controlled input more resilient.
1354-1641: AddRuleDialog: new torrent params (tags, ignoreDays, contentLayout, addStopped) are wired correctlyThe dialog captures all the core rule fields plus the extended torrent params, builds a
qbt.RSSAutoDownloadRule‑shaped object, and normalizes optional values toundefinedwhen empty—important for keeping the JSON minimal and letting qBittorrent defaults apply.A couple of minor polish ideas:
ignoreDayshandling (parseInt(...) || 0) forces0whenever the field is cleared; if you ever want “unset” semantics distinct from 0, you may want to distinguish empty vs numeric explicitly (similar to therefreshIntervalsuggestion).- A large number of inputs live in one component; extracting a few logical sub‑sections (e.g. “Match criteria”, “Feeds”, “Torrent options”) into small presentational components would reduce cognitive load.
Both are non‑blocking.
2017-2100: RssSettingsPopover: live preference editing works, but fires a mutation on every keystrokeThe popover gives a nice compact control surface for RSS refresh/max‑articles and auto‑download flags, and it reuses
updatePreferencesconsistently.One thing to keep in mind is that both numeric inputs call
updatePreferenceson everyonChange, which will trigger a network request per keystroke. If this proves noisy in practice, you might consider switching toonBluror adding a small debounce so preference updates happen less frequently while typing. Behavior is otherwise correct.internal/api/handlers/rss.go (1)
597-615: ReprocessRules: handler matches new SyncManager behavior; consider updating the doc commentThe handler now simply calls
h.syncManager.ReprocessRSSRules, which itself toggles auto‑downloading to reprocess unread articles while restoring the original auto‑download preference.The function comment still says “It does this by toggling auto‑downloading off then on.” To avoid confusion, you might update it to reflect that the original state is restored afterwards, e.g.:
-// It does this by toggling auto-downloading off then on. +// It does this by forcing a false→true toggle to trigger processing, +// while preserving the user's original auto-downloading setting.Behavior itself is correct.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (4)
go.mod(1 hunks)internal/api/handlers/rss.go(1 hunks)internal/qbittorrent/sync_manager.go(1 hunks)web/src/pages/RSSPage.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- go.mod
🧰 Additional context used
🧬 Code graph analysis (2)
web/src/pages/RSSPage.tsx (25)
web/src/hooks/usePersistedInstanceSelection.ts (1)
usePersistedInstanceSelection(8-35)web/src/hooks/useRSS.ts (10)
useRSSFeeds(35-45)useRSSRules(47-56)useRefreshRSSFeed(121-133)useRemoveRSSItem(99-108)useMarkRSSAsRead(162-171)useSetRSSRule(177-186)useRemoveRSSRule(200-209)useRSSMatchingArticles(58-71)useAddRSSFeed(77-86)useAddRSSFolder(88-97)web/src/hooks/useInstancePreferences.ts (1)
useInstancePreferences(15-81)web/src/hooks/useInstanceMetadata.ts (1)
useInstanceMetadata(21-43)web/src/components/ui/select.tsx (5)
Select(180-180)SelectTrigger(188-188)SelectValue(189-189)SelectContent(181-181)SelectItem(183-183)web/src/components/ui/card.tsx (5)
Card(90-90)CardHeader(91-91)CardTitle(93-93)CardDescription(95-95)CardContent(96-96)web/src/components/ui/alert.tsx (2)
Alert(64-64)AlertDescription(64-64)web/src/components/ui/button.tsx (1)
Button(64-64)web/src/components/ui/tabs.tsx (4)
Tabs(69-69)TabsList(69-69)TabsTrigger(69-69)TabsContent(69-69)web/src/components/ui/file-tree.tsx (1)
Folder(431-431)web/src/components/ui/tooltip.tsx (3)
Tooltip(184-184)TooltipTrigger(184-184)TooltipContent(184-184)web/src/types/index.ts (6)
RSSItems(1588-1590)isRSSFeed(1593-1595)RSSFeed(1576-1585)RSSArticle(1565-1574)RSSAutoDownloadRule(1618-1636)Category(434-437)web/src/components/ui/dropdown-menu.tsx (5)
DropdownMenu(245-245)DropdownMenuTrigger(247-247)DropdownMenuContent(248-248)DropdownMenuItem(251-251)DropdownMenuSeparator(255-255)web/src/hooks/useDateTimeFormatters.ts (1)
useDateTimeFormatters(13-52)web/src/components/ui/switch.tsx (1)
Switch(34-34)web/src/components/ui/badge.tsx (1)
Badge(51-51)web/src/components/ui/sheet.tsx (5)
Sheet(138-138)SheetContent(141-141)SheetHeader(142-142)SheetTitle(144-144)SheetDescription(145-145)web/src/components/ui/scroll-area.tsx (1)
ScrollArea(61-61)web/src/components/ui/dialog.tsx (6)
Dialog(136-136)DialogContent(138-138)DialogHeader(141-141)DialogTitle(144-144)DialogDescription(139-139)DialogFooter(140-140)web/src/components/ui/label.tsx (1)
Label(29-29)web/src/components/ui/input.tsx (1)
Input(26-26)web/src/components/ui/separator.tsx (1)
Separator(33-33)web/src/lib/category-utils.ts (2)
buildCategorySelectOptions(9-33)buildTagSelectOptions(36-42)web/src/components/ui/multi-select.tsx (1)
MultiSelect(26-170)web/src/components/ui/popover.tsx (3)
Popover(34-34)PopoverTrigger(34-34)PopoverContent(34-34)
internal/qbittorrent/sync_manager.go (1)
web/src/types/index.ts (4)
RSSItems(1588-1590)RSSRules(1638-1638)RSSAutoDownloadRule(1618-1636)RSSMatchingArticles(1640-1640)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (19)
web/src/pages/RSSPage.tsx (9)
268-317: RSS preferences banners and mutation usage look solidThe processing / auto‑download banners correctly derive state from
preferences, optimistically enable viaupdatePreferences, and surface feedback with toasts. Usingenabled: instanceId > 0onuseInstancePreferencesavoids unnecessary calls when no instance is selected.No changes needed here.
454-597: FeedsTab: behavior and edge cases are handled well
FeedsTabcleanly handles loading, empty‑state, and normal rendering. Selection is safely derived withfindFeedByPath, unread counts are computed from the tree, and “mark all as read” defers to the backend. ThequeryClient.invalidateQueriesafter a short delay on refresh is a pragmatic way to pick up feed changes without over‑eager polling.No issues here.
603-740: FeedTree: recursive rendering and item actions are correct and robustThe recursive
FeedTreecorrectly constructs item paths with\\, distinguishes feeds vs folders viaisRSSFeed, tracks expanded folders with aSet, and protects row clicks from being hijacked by the dropdown buttons.The “Remove” and “Refresh” callbacks are passed down consistently, and unread counts per feed/folder are computed via helpers. This looks good.
746-836: ArticlesPanel / ArticleRow: mark‑as‑read and link handling are sensible
ArticlesPanelcorrectly short‑circuits on empty feeds, passes per‑article handlers, and wraps themarkAsReadmutation in error handling.ArticleRowavoids rendering the external link when it equals the torrent URL and guards the date formatting.Given React 19’s handling of
javascript:URLs, the direct<a href={...}>usage is acceptable here. No changes needed.
842-960: RulesTab: editing and preview orchestration is cleanRules are sorted for deterministic ordering, toggling uses
SetRSSRulewith a full rule clone, and edit/delete actions correctly maintainselectedRuleNameandeditingRulestate while keeping dialogs and sheets in sync.This is a solid wiring of the matching‑articles sheet and edit dialog around the underlying rules data.
1121-1173: RulePreviewSheet: conditional fetching and empty‑state UX are solid
useRSSMatchingArticlesis correctly enabled only whenruleNameis truthy, and the sheet handles loading, non‑empty, and empty states cleanly with a scrollable list per feed URL.No issues here.
1289-1347: AddFolderDialog: UX and validation are straightforwardFolder creation validates non‑empty path, gives clear feedback via toasts, and resets/closing behavior is correct. The guidance text about using backslashes for nesting matches how paths are built elsewhere.
No changes needed.
1658-1941: EditRuleDialog: initialization from existing rule and submission shape look goodThe
useEffectcorrectly hydrates local state from the providedrule, including legacy fields (savePath,assignedCategory) and the newertorrentParams. On submit you spread the original rule and carefully override only the edited fields, which avoids accidentally dropping backend‑only properties.The feeds, category, tags, ignoreDays, contentLayout, and addStopped controls mirror
AddRuleDialogas expected; any future validation tweaks can likely be shared between the two.No changes needed here.
1947-2011: Helper functions: path traversal and folder/url extraction align with the RSS tree model
findFeedByPath,countFeeds,countUnreadArticles,getFolderPaths, andgetFeedUrlsall honor theRSSItemsrecursive shape and the\separator convention. Early returns on missing paths and checks viaisRSSFeedkeep them safe against malformed trees.Looks good.
internal/qbittorrent/sync_manager.go (2)
4801-4941: RSS proxy methods: consistent error handling and context usageAll the new RSS methods (
GetRSSItems,AddRSSFolder,AddRSSFeed, etc.) consistently obtain the client viaGetClient(ctx, instanceID), wrap errors with context, and delegate to the underlying*Ctxmethods. They don’t unnecessarily touch the sync manager or cache, which is appropriate since these calls are already relatively cheap and stateful.This looks correct and aligned with the rest of the SyncManager API surface.
4943-4977: ReprocessRSSRules: preserves original auto‑download stateThe implementation correctly:
- Reads the current
AppPreferencesfrom the go-qbittorrent client.- Forces a
false → truetransition to trigger qBittorrent's internal RSS processing.- Restores the original auto‑download flag if it was previously disabled.
The field name
RssAutoDownloadingEnabledand methodSetRSSAutoDownloadingEnabledCtxare correct per the go-qbittorrent library (v1.15.0-rc1.0.20251216201705-279083efefc3).internal/api/handlers/rss.go (8)
18-52: RSSHandler wiring and route surface are coherentThe
RSSHandlerstruct plusNewRSSHandlerandRoutesmethod give a clear, centered place for all RSS endpoints. Route shapes (/items,/feeds,/folders,/rules/...) are consistent and map one‑to‑one onto the SyncManager methods, which should make it straightforward to keep the OpenAPI spec and frontend client in sync.No changes needed here.
56-102: Request types are well‑shaped and closely match the underlying qBittorrent APIThe various request DTOs (
AddFolderRequest,AddFeedRequest,SetFeedURLRequest,MoveItemRequest, etc.) are minimal and typed in a way that mirrors the qBittorrent contract (e.g.RefreshIntervalasint64,Ruleasqbt.RSSAutoDownloadRule). This should keep marshalling straightforward and prevent impedance mismatches between layers.Looks good.
157-279: AddFeed: baseline/diff+move logic is careful; baseline failure is handled safelyThe sequence:
- Optionally fetch a baseline
GetRSSItems(withhasBaselineguard).- Add the feed to root (empty path).
- Fetch items again and diff against
existingNamesto locate a newnamewhose JSON has aurl.- Move that feed into the requested folder, or return a 201 with a warning if it can’t be found or moved,
is a reasonable workaround for qBittorrent’s “full path” semantics on RSS paths.
The earlier edge case where a failed baseline could cause any “new” root item to be moved is now avoided by only doing diff‑based detection when
hasBaselineis true; on failure, you fall back to adding to root with a clear warning. Nice fix.
281-399: SetFeedURL / SetFeedRefreshInterval / MoveItem / RemoveItem have consistent validation and error pathsThese handlers:
- Parse
instanceIDuniformly viaparseInstanceID.- Validate required fields (
Path,ItemPath) and return 400s on missing data.- Delegate to the appropriate SyncManager method and use
respondIfInstanceDisabledbefore logging and returning a 500.The patterns are consistent and will be easy to maintain.
430-458: MarkAsRead: validates required path and cleanly delegates per‑article vs all‑articles
MarkAsReadenforces a non‑emptyItemPathand passes bothitemPathand (optional)articleIdthrough to the SyncManager. This matches the intended semantics of “mark all articles in feed as read” whenArticleIDis empty and “mark a specific article” when it’s present, assuming the underlying client follows qBittorrent’s API.No changes needed.
460-508: SetRule: rule creation/update API shape looks correctThe handler ensures
Nameis non‑empty, then forwards theqbt.RSSAutoDownloadRuleto the SyncManager. Returning 201 on success is appropriate for both create and upsert semantics here, since from a client perspective the effect is “rule now exists with this configuration.”Looks good.
510-569: RenameRule and RemoveRule: parameter validation and logging are straightforwardBoth endpoints:
- Require a
ruleNamepath parameter, with clear 400s when missing.- Decode payload (for rename) and validate new name.
- Delegate to SyncManager with consistent
respondIfInstanceDisabledhandling and structured logging.No issues here.
571-595: GetMatchingArticles: preview semantics are simple and correctly scopedThe handler pulls
ruleNamefrom the path, validates it, and returns theRSSMatchingArticlesmap from SyncManager. Using a GET here is appropriate since the endpoint is read‑only and purely for preview.Looks good.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
internal/api/handlers/rss_sse.go (3)
155-170: Nested lock acquisition may cause issues if ordering changes.
removeClientholdsh.mu(Lock) then callsstopPollerLockedwhich acquiresh.pollerMu. While this currently works, the namingstopPollerLockedsuggests it expects the caller to already hold a lock, which is misleading—it actually acquires its own lock.Consider renaming to
stopPollerfor clarity, or restructuring to avoid nested lock acquisition patterns that could become deadlock-prone if the code evolves.
68-85: SSE headers set after potential error response.If
parseInstanceIDreturns an error, it likely writes an error response tow. The subsequentw.Header().Set()calls at lines 75-78 would then have no effect (headers already sent). While this doesn't cause a crash, the code flow is misleading.The current structure is fine since
returnexits early, but consider adding a comment clarifying thatparseInstanceIDhandles the error response internally.
212-233: Consider making poll interval configurable.The 5-second poll interval is hardcoded. For production deployments with varying network conditions or backend load, a configurable interval would provide operational flexibility.
+const defaultRSSPollInterval = 5 * time.Second + func (h *RSSSSEHandler) pollLoop(ctx context.Context, instanceID int) { log.Debug().Int("instanceID", instanceID).Msg("RSS SSE poll loop started") var lastItems []byte // Initial poll lastItems = h.pollAndBroadcast(ctx, instanceID, lastItems) - ticker := time.NewTicker(5 * time.Second) + ticker := time.NewTicker(defaultRSSPollInterval) defer ticker.Stop()web/src/lib/rss-events.ts (1)
111-132: Consider notifyingonDisconnectedwhen connection fails before reconnect.When
scheduleReconnectcloses the existing connection, it doesn't callonDisconnected. This means the UI won't know the connection is temporarily down during the reconnection window.private scheduleReconnect(): void { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error("RSS SSE max reconnection attempts reached") + this.handlers.onDisconnected?.() return } // Close existing connection if any if (this.eventSource) { this.eventSource.close() this.eventSource = null }web/src/hooks/useRSS.ts (1)
44-53: RedundantinvalidateQueriesaftersetQueryData.
setQueryDataalready updates the cache. The subsequentinvalidateQuerieswithrefetchType: "none"marks the query as stale but doesn't trigger a refetch. SincestaleTime: Infinityis set and SSE handles updates, this call appears unnecessary.const handleFeedsUpdate = useCallback( (data: FeedsUpdatePayload) => { if (data.instanceId === instanceId) { - // Set the data and invalidate to ensure re-render queryClient.setQueryData(rssKeys.feeds(instanceId), data.items) - queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId), refetchType: "none" }) } }, [instanceId, queryClient] )If you need to notify observers of the update,
setQueryDataalready triggers re-renders for subscribed components.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
internal/api/handlers/rss_sse.go(1 hunks)internal/api/server.go(2 hunks)web/src/hooks/useRSS.ts(1 hunks)web/src/lib/rss-events.ts(1 hunks)web/src/pages/RSSPage.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/api/server.go
- web/src/pages/RSSPage.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
internal/api/handlers/rss_sse.go (3)
internal/qbittorrent/sync_manager.go (1)
SyncManager(159-182)internal/api/handlers/helpers.go (1)
RespondError(39-43)web/src/lib/rss-events.ts (1)
FeedsUpdatePayload(22-26)
web/src/lib/rss-events.ts (3)
internal/api/handlers/rss_sse.go (1)
FeedsUpdatePayload(52-56)web/src/types/index.ts (1)
RSSItems(1588-1590)web/src/lib/base-url.ts (1)
getApiBaseUrl(17-21)
web/src/hooks/useRSS.ts (2)
web/src/lib/rss-events.ts (2)
RSSEventSource(35-137)FeedsUpdatePayload(22-26)web/src/types/index.ts (10)
AddRSSFeedRequest(1647-1651)AddRSSFolderRequest(1643-1645)RemoveRSSItemRequest(1668-1670)MoveRSSItemRequest(1663-1666)RefreshRSSItemRequest(1672-1674)SetRSSFeedURLRequest(1653-1656)SetRSSFeedRefreshIntervalRequest(1658-1661)MarkRSSAsReadRequest(1676-1679)SetRSSRuleRequest(1681-1684)RenameRSSRuleRequest(1686-1688)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (6)
internal/api/handlers/rss_sse.go (1)
235-266: LGTM: Change detection via byte comparison is efficient.The approach of serializing RSS items to bytes and comparing with the previous payload is a clean way to detect changes without complex diffing logic. The
json.RawMessagetype preserves the original JSON for the payload.web/src/lib/rss-events.ts (2)
50-92: LGTM: Robust connection management with proper cleanup.The connection logic correctly:
- Disconnects any existing connection before creating a new one
- Resets
isIntentionalCloseflag- Resets reconnect attempts on successful connection (line 62)
- Handles JSON parsing errors gracefully
94-109: LGTM: Clean disconnect implementation.The disconnect method properly:
- Sets intentional close flag first to prevent reconnection
- Clears any pending reconnect timer
- Closes the EventSource and notifies handlers
- Resets reconnect attempts
web/src/hooks/useRSS.ts (3)
55-79: LGTM: Proper SSE lifecycle management in useEffect.The effect correctly:
- Creates the EventSource only when enabled
- Connects immediately after creation
- Stores reference for potential future use
- Cleans up on unmount or dependency change
The dependency array is complete with
instanceId,shouldEnable, andhandleFeedsUpdate.
81-89: LGTM: Smart caching strategy with SSE.Setting
staleTime: Infinityis the correct approach when SSE handles data freshness. The initial fetch populates the cache, and subsequent updates come through the event stream.
24-31: LGTM: Well-structured query keys.The hierarchical query key pattern enables granular cache invalidation and follows React Query best practices.
Document the /api/instances/{instanceID}/rss/events endpoint for
Server-Sent Events streaming of real-time RSS feed updates.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (6)
internal/web/swagger/openapi.yaml (6)
3210-3231: Consider documenting SSE event format.The SSE endpoint definition is functional, but the schema description is generic. Consider adding a description of the event types and payload structure that clients can expect from this stream (e.g., feed updates, article additions, rule matches) to improve API discoverability.
3425-3493: Consider using PATCH instead of PUT for partial updates.The endpoints at
/rss/feeds/urland/rss/feeds/intervaluse PUT but only update a single field. Per REST conventions, PUT should replace the entire resource while PATCH should be used for partial updates. Since these operations modify only one property (url or refreshInterval), PATCH would be more semantically appropriate.Apply this pattern for both endpoints:
- /api/instances/{instanceID}/rss/feeds/url: - put: + /api/instances/{instanceID}/rss/feeds/url: + patch:- /api/instances/{instanceID}/rss/feeds/interval: - put: + /api/instances/{instanceID}/rss/feeds/interval: + patch:
3552-3582: Clarify response behavior for create vs update operations.The POST
/rss/rulesendpoint description states it can "Create a new rule or update an existing one" but always returns201 Created. This is inconsistent with HTTP semantics where:
- 201 (Created) should be returned when a new resource is created
- 200 (OK) should be returned when an existing resource is updated
Consider either:
- Splitting this into separate POST (create) and PUT (update) endpoints
- Returning 200 when updating an existing rule and 201 when creating a new one
- Documenting that 201 will always be returned regardless of create/update
4922-4968: Consider marking required fields in RSS schemas.The
RSSFeedandRSSArticleschemas don't specify required fields, making all properties implicitly optional. Consider addingrequiredarrays to indicate which fields are guaranteed to be present. For example:
- RSSFeed:
urlis likely always required- RSSArticle:
id,title, anddateare likely always requiredThis improves type safety and helps client implementations understand which fields they can rely on.
Example:
RSSFeed: type: object + required: + - url properties: url:RSSArticle: type: object + required: + - id + - title + - date properties:
5001-5010: Use OpenAPI deprecation markers for legacy fields.The legacy fields (
savePath,assignedCategory,addPaused) are documented with description text, but OpenAPI 3.1 provides adeprecated: trueproperty that tools can use to warn developers. This provides better machine-readable deprecation information.Apply this pattern:
savePath: type: string + deprecated: true description: Legacy field assignedCategory: type: string + deprecated: true description: Legacy field addPaused: type: boolean nullable: true + deprecated: true description: Legacy field
5012-5054: Consider adding validation constraints to numeric fields.The
RSSRuleTorrentParamsschema has several numeric fields (upload_limit,download_limit,seeding_time_limit, etc.) without validation constraints. Addingminimum: 0orminimum: -1(if -1 means "unlimited") would help catch invalid values early and improve API documentation.Example:
upload_limit: type: integer + minimum: -1 + description: Upload limit in bytes/sec (-1 for unlimited) download_limit: type: integer + minimum: -1 + description: Download limit in bytes/sec (-1 for unlimited)
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
internal/web/swagger/openapi.yaml(3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (1)
internal/web/swagger/openapi.yaml (1)
4915-4921: LGTM!The
WarningResponseschema appropriately handles partial success scenarios where an operation completes but with warnings. This is a good pattern for operations like feed addition where non-critical issues shouldn't block the operation.
- Add sync.Once to rssSSEClient to ensure done channel is closed only once - Copy clients slice while holding lock in broadcast to prevent race during iteration after lock release
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
internal/api/handlers/rss_sse.go (1)
253-257: Log marshaling error for debuggability.If
json.Marshalfails when serializing the RSS items for comparison, the error is silently ignored. While unlikely for internal data structures, logging this would aid debugging if serialization issues occur.Apply this diff:
// Serialize for comparison currentItems, err := json.Marshal(items) if err != nil { + log.Debug().Err(err).Int("instanceID", instanceID).Msg("RSS SSE failed to marshal items") return lastItems }
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
internal/api/handlers/rss_sse.go(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/api/handlers/rss_sse.go (2)
internal/api/handlers/helpers.go (1)
RespondError(39-43)web/src/lib/rss-events.ts (1)
FeedsUpdatePayload(22-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
…terval feature - Introduced `SupportsSetRSSFeedURL` capability in the API response. - Updated the RSS feed handling to allow setting the feed URL without a refresh interval. - Removed the refresh interval parameter from the AddRSSFeed API and related frontend hooks. - Updated OpenAPI documentation to reflect the changes in the RSS feed endpoints.
- Add search field to filter articles by title/description - Add collapsible descriptions with clickable URLs - Route download button through AddTorrentDialog - Auto-expand parent folders when feed is selected on page load - Add Save button to RSS settings (instead of auto-save on input) - Strip HTML tags and decode entities in descriptions - Use CSS grid for proper text truncation with action buttons
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
web/src/hooks/useRSS.ts (1)
164-176: Clean up and de‑duplicate delayed invalidation inuseRefreshRSSFeed
useRefreshRSSFeedschedules asetTimeouton every successful refresh without clearing it on unmount, andFeedsTabadds a second delayedinvalidateQuerieson top. That means each click schedules two refetches and keeps timers alive even if the UI is torn down.You can centralize the delay in the hook and ensure it’s cleaned up on unmount:
export function useRefreshRSSFeed(instanceId: number) { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: (data: RefreshRSSItemRequest) => api.refreshRSSItem(instanceId, data), - onSuccess: () => { - // Invalidate after a short delay to allow qBittorrent to process the refresh - setTimeout(() => { - queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) }) - }, 1000) - }, - }) + const queryClient = useQueryClient() + const timeoutRef = useRef<number | null>(null) + + useEffect(() => { + return () => { + if (timeoutRef.current != null) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + } + }, []) + + return useMutation({ + mutationFn: (data: RefreshRSSItemRequest) => api.refreshRSSItem(instanceId, data), + onSuccess: () => { + if (timeoutRef.current != null) { + clearTimeout(timeoutRef.current) + } + timeoutRef.current = window.setTimeout(() => { + queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) }) + timeoutRef.current = null + }, 1000) + }, + }) }Combined with removing the extra timeout in
FeedsTab(see comment there), this keeps refresh behavior while avoiding redundant timers and refetches.web/src/pages/RSSPage.tsx (1)
543-551: Avoid a second delayed invalidate after refresh
handleRefreshFeedcallsrefreshFeed.mutateAsyncand then schedules its own 2ssetTimeouttoinvalidateQueries, whileuseRefreshRSSFeedalready does a delayed invalidate on success. With the hook updated to own the delay, this extra timeout just duplicates work and adds more timers:const handleRefreshFeed = async (path: string) => { try { await refreshFeed.mutateAsync({ itemPath: path }) toast.success("Feed refresh triggered") - // Invalidate to pick up changes - setTimeout(() => { - queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) }) - }, 2000) } catch (err) { const message = err instanceof Error ? err.message : "Failed to refresh feed" toast.error(message) } }This keeps the behavior but makes refresh semantics single-sourced in the hook.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (10)
go.mod(1 hunks)internal/api/handlers/capabilities.go(2 hunks)internal/api/handlers/rss.go(1 hunks)internal/qbittorrent/client.go(4 hunks)internal/qbittorrent/sync_manager.go(1 hunks)internal/web/swagger/openapi.yaml(3 hunks)web/src/hooks/useRSS.ts(1 hunks)web/src/lib/api.ts(2 hunks)web/src/pages/RSSPage.tsx(1 hunks)web/src/types/index.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- go.mod
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-06T12:11:04.963Z
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: internal/services/crossseed/service.go:1045-1082
Timestamp: 2025-11-06T12:11:04.963Z
Learning: The autobrr/qui project uses a custom go-qbittorrent client library (github.com/autobrr/go-qbittorrent) that supports both "paused" and "stopped" parameters when adding torrents via the options map. Both parameters should be set together when controlling torrent start state, as seen in internal/services/crossseed/service.go and throughout the codebase.
Applied to files:
internal/qbittorrent/client.go
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.
Applied to files:
web/src/pages/RSSPage.tsx
🧬 Code graph analysis (4)
web/src/types/index.ts (1)
internal/api/handlers/helpers.go (1)
WarningResponse(22-24)
internal/api/handlers/rss.go (3)
internal/qbittorrent/sync_manager.go (1)
SyncManager(159-182)web/src/types/index.ts (2)
RSSAutoDownloadRule(1619-1637)WarningResponse(20-22)internal/api/handlers/helpers.go (3)
RespondError(39-43)RespondJSON(27-36)WarningResponse(22-24)
internal/qbittorrent/sync_manager.go (1)
web/src/types/index.ts (4)
RSSItems(1589-1591)RSSRules(1639-1639)RSSAutoDownloadRule(1619-1637)RSSMatchingArticles(1641-1641)
web/src/lib/api.ts (2)
web/src/types/index.ts (12)
AddRSSFolderRequest(1644-1646)AddRSSFeedRequest(1648-1651)WarningResponse(20-22)SetRSSFeedURLRequest(1653-1656)MoveRSSItemRequest(1658-1661)RemoveRSSItemRequest(1663-1665)RefreshRSSItemRequest(1667-1669)MarkRSSAsReadRequest(1671-1674)RSSRules(1639-1639)SetRSSRuleRequest(1676-1679)RenameRSSRuleRequest(1681-1683)RSSMatchingArticles(1641-1641)internal/api/handlers/helpers.go (1)
WarningResponse(22-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (12)
internal/web/swagger/openapi.yaml (1)
3209-5015: LGTM! Comprehensive RSS API surface.The RSS endpoints and schemas are well-structured and consistent. The API design includes:
- Proper SSE streaming for real-time updates
- RESTful CRUD operations for feeds, folders, and rules
- Consistent error handling (409 for disabled instances)
- Comprehensive schemas with recursive structures for hierarchical feed organization
- Legacy field handling in RSSAutoDownloadRule
web/src/types/index.ts (1)
1594-1596: LGTM! Type guard implementation.The
isRSSFeedtype guard correctly distinguishes betweenRSSFeedandRSSItemsby checking for theurlproperty, which is unique to feeds.internal/api/handlers/capabilities.go (1)
24-24: LGTM! Capability flag properly wired.The new
SupportsSetRSSFeedURLfield follows the established pattern for capability flags and is correctly initialized from the client's capability method.Also applies to: 43-43
internal/qbittorrent/client.go (3)
50-50: LGTM! Thread-safe capability field and getter.The
supportsSetRSSFeedURLfield and its getter method follow the established pattern used by all other capability flags, with proper read-lock protection for thread safety.Also applies to: 421-425
258-258: LGTM! Capability computation follows established pattern.The RSS feed URL capability is computed using the same version comparison logic as all other capability flags in this method.
32-32: Version 2.9.1 is correct for RSS setFeedURL support.The RSS setFeedURL endpoint was introduced with qBittorrent v4.6.0 (Web API v2.9.1), confirming the minimum version gate is accurate.
internal/qbittorrent/sync_manager.go (1)
4801-4967: RSS proxy surface and ReprocessRSSRules logic look solidThe new RSS methods correctly delegate to the underlying client with consistent error handling, and
ReprocessRSSRulesnow preserves the originalRssAutoDownloadingEnabledpreference while forcing the required false→true transition to trigger reprocessing. This aligns with typical SyncManager patterns and avoids surprising permanent config changes.internal/api/handlers/rss.go (1)
150-272: Robust AddFeed folder handling and RefreshItem behaviorThe
AddFeedflow (baseline snapshot, diff, type-checking feeds viaurl, and clear warning fallbacks when baseline or post-add fetch fails) is carefully done and avoids moving the wrong item whenGetRSSItemserrors. Likewise,RefreshItemnow correctly trims whitespace and allows an emptyitemPathto support “Refresh All” while still delegating directly toRefreshRSSItem, which fixes the previous 400-on-refresh-all issue.Also applies to: 364-391
web/src/pages/RSSPage.tsx (1)
710-717: AddTorrentDialog integration respects per-torrent semanticsWiring
AddTorrentDialogwith adropPayloadcontaining only the article’s torrent URL (and not mutating instance-level preferences) keeps torrent-add behavior scoped to the individual RSS item, which matches the intended per-torrent semantics from earlier work on that dialog.Based on learnings, this avoids accidentally treating RSS-triggered adds as global preference changes.
web/src/lib/api.ts (3)
76-89: LGTM: RSS type imports are comprehensive and properly structured.All imported types are used in the RSS methods below and align with the backend API schema.
1723-1776: LGTM: RSS feed management methods are well-implemented.The methods follow existing patterns in this file, use appropriate HTTP verbs, and properly handle request/response types. The
addRSSFeedmethod correctly returnsPromise<WarningResponse | undefined>to reflect the backend's conditional response behavior.
1778-1812: LGTM: RSS rules methods properly handle URL encoding and follow RESTful patterns.Good attention to detail encoding
ruleNamewithencodeURIComponentat lines 1792, 1799, and 1805, which prevents issues with special characters in user-defined rule names.
- Add proper URL validation requiring http/https scheme and host
- Fix silent failures in SSE handler by adding error logging
- Change SSE poll failure log level from Debug to Warn (with context
cancellation check to avoid noise during shutdown)
- Check and handle connected event send errors in SSE handler
- Rename stopPollerLocked to stopPoller for clarity
- Update interface{} to any for modern Go
- Make destPath optional in MoveRSSItemRequest TypeScript type
- Extract shared RuleFormFields component to eliminate ~150 lines of
duplicated form code between Add and Edit rule dialogs
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
web/src/types/index.ts (1)
1581-1590: Verify RSSFeed.uid field alignment with backend.The
uidfield doesn't appear in the OpenAPI schema as noted in a previous review. Please verify whether this is intentionally client-only (for React keys or local tracking) or if it should be removed to match the backend contract.internal/api/handlers/rss_sse.go (1)
165-180: Critical: Inconsistent lock ordering creates deadlock risk.The lock acquisition order differs between
removeClientandShutdown:
removeClient(line 166): acquiresh.mu, then callsstopPoller(line 175) which acquiresh.pollerMu(line 216)Shutdown(line 290): acquiresh.pollerMufirst, thenh.mu(line 297)This classic ABBA deadlock pattern can cause the application to hang during shutdown if a client disconnects simultaneously.
🔎 Proposed fix: call stopPoller outside the mu lock
func (h *RSSSSEHandler) removeClient(instanceID int, client *rssSSEClient) { + var shouldStopPoller bool + h.mu.Lock() - defer h.mu.Unlock() - client.closeDone() if h.clients[instanceID] != nil { delete(h.clients[instanceID], client) if len(h.clients[instanceID]) == 0 { delete(h.clients, instanceID) - // Stop poller if no more clients - h.stopPoller(instanceID) + shouldStopPoller = true } } + h.mu.Unlock() + + if shouldStopPoller { + h.stopPoller(instanceID) + } log.Debug().Int("instanceID", instanceID).Msg("RSS SSE client disconnected") }Also applies to: 215-224, 289-307
🧹 Nitpick comments (2)
internal/api/handlers/rss_sse.go (1)
249-286: Consider hash-based change detection for reduced allocations.The current approach serializes the entire RSS items tree every 5 seconds for byte comparison. While correct, this creates allocation overhead on each poll cycle.
For feeds with many articles, consider caching a hash of the serialized data instead of the full byte slice for comparison.
web/src/pages/RSSPage.tsx (1)
543-555: Consider removing manual invalidation with SSE in place.The 2-second timeout before invalidating queries may be unnecessary since SSE (configured with
staleTime: Infinity) handles real-time updates. This could cause double-updates or race conditions where SSE data arrives before the timeout fires.const handleRefreshFeed = async (path: string) => { try { await refreshFeed.mutateAsync({ itemPath: path }) toast.success("Feed refresh triggered") - // Invalidate to pick up changes - setTimeout(() => { - queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) }) - }, 2000) + // SSE will push updates when feed data changes } catch (err) { const message = err instanceof Error ? err.message : "Failed to refresh feed" toast.error(message) } }
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
internal/api/handlers/rss.go(1 hunks)internal/api/handlers/rss_sse.go(1 hunks)web/src/pages/RSSPage.tsx(1 hunks)web/src/types/index.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/api/handlers/rss.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-28T20:32:30.126Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go:209-212
Timestamp: 2025-11-28T20:32:30.126Z
Learning: Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go
Learning: The cross-seed recheck-resume worker intentionally runs for the process lifetime and keys pending entries by hash only. This is acceptable under the current constraint that background seeded-search runs operate on a single instance at a time; graceful shutdown and instanceID|hash keying are deferred by design.
Applied to files:
internal/api/handlers/rss_sse.go
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.
Applied to files:
web/src/pages/RSSPage.tsx
🧬 Code graph analysis (3)
internal/api/handlers/rss_sse.go (3)
internal/qbittorrent/sync_manager.go (1)
SyncManager(170-197)internal/api/handlers/helpers.go (1)
RespondError(39-43)web/src/lib/rss-events.ts (1)
FeedsUpdatePayload(22-26)
web/src/types/index.ts (1)
internal/api/handlers/helpers.go (1)
WarningResponse(22-24)
web/src/pages/RSSPage.tsx (3)
web/src/hooks/useRSS.ts (3)
useRSSFeeds(36-88)useRSSRules(90-99)rssKeys(24-30)web/src/types/index.ts (4)
RSSItems(1593-1595)isRSSFeed(1598-1600)RSSFeed(1581-1590)RSSArticle(1570-1579)web/src/lib/linkUtils.tsx (1)
renderTextWithLinks(17-53)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (10)
internal/api/handlers/rss_sse.go (1)
59-66: LGTM!The constructor properly initializes maps to avoid nil map panics, and the overall handler structure is well-organized with clear separation of concerns.
web/src/types/index.ts (2)
1597-1600: Well-implemented type guard.The
isRSSFeedtype guard correctly distinguishes feeds from folders using theurlproperty check, enabling type-safe traversal of the hierarchical RSS structure.
19-22: LGTM!The
WarningResponsetype correctly mirrors the backendWarningResponsestruct, and the RSS request types are well-structured with appropriate optional fields matching the API contract.Also applies to: 1647-1687
web/src/pages/RSSPage.tsx (7)
124-169: LGTM!The component properly handles instance selection with auto-selection of the first connected instance, and the queries correctly guard against invalid instance IDs with
enabled: instanceId > 0.
767-929: LGTM!The recursive
FeedTreecomponent correctly handles the hierarchical structure with proper auto-expansion of parent folders and appropriate qBittorrent-specific path separators (backslash).
1017-1031: Basic HTML sanitization is acceptable for display.The
stripHtmlfunction handles common HTML entities and tags adequately for RSS content display. For stricter security requirements, consider using a library like DOMPurify, but for trusted RSS feed content this approach is reasonable.
1119-1226: LGTM!The
RulesTabcomponent provides good UX with proper state management for editing, toggling, and removing rules. The integration withRulePreviewSheetfor matching articles preview is well-designed.
1600-1835: Good code reuse with shared form fields.The
RuleFormFieldscomponent effectively eliminates duplication between Add and Edit rule dialogs while maintaining flexibility through theidPrefixprop for unique form element IDs.
2083-2147: LGTM!Helper functions correctly traverse the hierarchical RSS structure with proper use of the
isRSSFeedtype guard. The recursive implementations are clean and handle edge cases appropriately.
2153-2244: LGTM!The settings popover properly synchronizes local state with preferences and handles input validation with sensible defaults. The save-all-at-once pattern provides a clear user experience.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
web/src/pages/RSSPage.tsx (3)
585-597: Consider cleanup for the setTimeout to prevent potential issues.The
setTimeoutwith query invalidation could execute after component unmount. While React Query handles this gracefully, consider storing the timeout ID and clearing it in a cleanup function for defensive coding.🔎 Suggested improvement
const handleRefreshFeed = async (path: string) => { try { await refreshFeed.mutateAsync({ itemPath: path }) toast.success("Feed refresh triggered") // Invalidate to pick up changes - setTimeout(() => { + const timeoutId = setTimeout(() => { queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) }) }, 2000) + return () => clearTimeout(timeoutId) } catch (err) { const message = err instanceof Error ? err.message : "Failed to refresh feed" toast.error(message) } }Note: If using the cleanup approach, you'd need to refactor this into a useEffect or similar pattern.
1702-1729: Consider accessibility enhancements for the feed selection list.The affected feeds checkbox list could benefit from:
- An accessible label or
role="group"witharia-labelfor the container- A "Select All/None" quick action for better UX when there are many feeds
- Visual scrollbar styling for consistency
🔎 Suggested enhancement
<div className="space-y-2"> <Label>Affected Feeds</Label> + <div role="group" aria-label="Select RSS feeds for this rule"> <div className="grid grid-cols-1 gap-2 max-h-32 overflow-y-auto"> {feedUrls.map((feedUrl) => ( <label key={feedUrl} className="flex items-center gap-2 text-sm"> <input type="checkbox" checked={state.affectedFeeds.includes(feedUrl)} onChange={(e) => { if (e.target.checked) { onChange("affectedFeeds", [...state.affectedFeeds, feedUrl]) } else { onChange( "affectedFeeds", state.affectedFeeds.filter((f) => f !== feedUrl) ) } }} className="rounded" /> <span className="truncate">{feedUrl}</span> </label> ))} {feedUrls.length === 0 && ( <p className="text-sm text-muted-foreground">No feeds available</p> )} </div> + </div> </div>
1-2242: Consider splitting this file for better maintainability.At 2242 lines, this file could benefit from being split into smaller, focused modules. While the code is well-organized with clear section markers, separating components into individual files would improve:
- Code navigation and searchability
- Test isolation
- Bundle splitting and lazy loading opportunities
- Team collaboration (fewer merge conflicts)
Suggested structure:
RSSPage.tsx(main component)components/FeedsTab/(FeedsTab, FeedTree, ArticlesPanel)components/RulesTab/(RulesTab, RuleCard, RulePreviewSheet)components/dialogs/(RSS-specific dialogs)lib/rss-utils.ts(helper functions)components/RssSettingsPopover.tsx
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
web/src/pages/RSSPage.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/pages/RSSPage.tsx (7)
web/src/hooks/useRSS.ts (12)
useRSSFeeds(36-116)useRSSRules(118-127)useReprocessRSSRules(270-280)useRefreshRSSFeed(192-204)useRemoveRSSItem(170-179)useMarkRSSAsRead(221-230)useMoveRSSItem(181-190)useSetRSSFeedURL(206-215)rssKeys(24-30)useSetRSSRule(236-245)useRemoveRSSRule(259-268)useRSSMatchingArticles(129-142)web/src/hooks/useInstanceMetadata.ts (1)
useInstanceMetadata(21-44)web/src/types/index.ts (4)
RSSItems(1594-1596)RSSFeed(1583-1591)RSSArticle(1572-1581)Category(435-438)web/src/components/torrents/AddTorrentDialog.tsx (2)
AddTorrentDropPayload(104-106)AddTorrentDialog(164-1671)web/src/hooks/useDateTimeFormatters.ts (1)
useDateTimeFormatters(13-52)web/src/lib/linkUtils.tsx (1)
renderTextWithLinks(17-53)web/src/lib/category-utils.ts (2)
buildCategorySelectOptions(9-33)buildTagSelectOptions(36-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Build Docker images (linux/amd64/v2)
- GitHub Check: Build Docker images (linux/arm/v6)
- GitHub Check: Build Docker images (linux/amd64)
- GitHub Check: Build Docker images (linux/arm64)
- GitHub Check: Build Docker images (linux/arm/v7)
- GitHub Check: Build Docker images (linux/amd64/v3)
- GitHub Check: Build distribution binaries
🔇 Additional comments (5)
web/src/pages/RSSPage.tsx (5)
179-189: LGTM: SSE disconnect notification logic is sound.The use of a ref to prevent duplicate toast notifications when SSE disconnects is a good pattern. The logic correctly resets the flag when reconnected and only shows the toast on permanent disconnection.
1456-1479: LGTM: Dialog submission handlers are well-implemented.Both
AddFeedDialogandAddRuleDialoghave robust submission logic with:
- Proper validation
- Error handling with user feedback
- State cleanup on success
- Appropriate closing behavior
Also applies to: 1862-1899
2077-2141: LGTM: Helper functions are well-implemented.The helper functions correctly handle the recursive RSS feed/folder structure with:
- Proper use of the
isRSSFeedtype guard- Safe type assertions
- Appropriate recursion for tree traversal
- Clear, focused responsibilities
2147-2242: LGTM: RSS settings popover implements good form state management.The component correctly:
- Maintains local form state separate from server state
- Syncs from preferences when they change (Line 2161-2165)
- Provides clear save action with feedback
- Uses appropriate input types and validation
1060-1073: Regex-based HTML stripping is acceptable here, but consider alternatives for edge cases.The
stripHtmlfunction uses regex to strip HTML tags and decode entities, which is a reasonable approach for extracting plain text from RSS descriptions. Since the output is rendered as text via React's default JSX binding (notdangerouslySetInnerHTML), React's automatic escaping of dynamic content provides built-in XSS protection. The main drawback is that regex parsing can miss edge cases, particularly in malformed HTML. Consider testing with malformed entity sequences or nested tags from real-world feeds to ensure robustness.
Change text-yellow-700 to text-yellow-800 in light mode for better contrast ratio against bg-yellow-500/10 background.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
web/src/pages/RSSPage.tsx (1)
585-597: Consider eliminating the setTimeout workaround.The 2-second delay before invalidating queries is a code smell. Ideally, the backend would provide completion feedback or the mutation would handle cache invalidation automatically.
💡 Potential improvement
If the backend API returns updated data or a completion signal, you could:
- Return the updated feed data from the refresh endpoint
- Update the cache directly in the mutation's
onSuccesscallback- Use optimistic updates to reflect the loading state immediately
Example pattern:
const handleRefreshFeed = async (path: string) => { try { await refreshFeed.mutateAsync({ itemPath: path }) toast.success("Feed refresh triggered") - // Invalidate to pick up changes - setTimeout(() => { - queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) }) - }, 2000) } catch (err) { const message = err instanceof Error ? err.message : "Failed to refresh feed" toast.error(message) } }Then handle invalidation in the mutation hook itself:
export function useRefreshRSSFeed(instanceId: number) { const queryClient = useQueryClient() return useMutation({ mutationFn: (data: RefreshRSSFeedRequest) => api.refreshRSSFeed(instanceId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: rssKeys.feeds(instanceId) }) }, }) }
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
web/src/components/layout/Header.tsxweb/src/components/layout/MobileFooterNav.tsxweb/src/components/ui/alert.tsxweb/src/pages/RSSPage.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
web/src/components/layout/Header.tsx (2)
web/src/components/ui/dropdown-menu.tsx (1)
DropdownMenuItem(251-251)pkg/gojackett/domain.go (1)
Rss(95-143)
web/src/pages/RSSPage.tsx (11)
web/src/hooks/useRSS.ts (4)
rssKeys(24-30)useRSSMatchingArticles(129-142)useAddRSSFeed(148-157)useAddRSSFolder(159-168)web/src/hooks/useInstanceMetadata.ts (1)
useInstanceMetadata(21-44)web/src/components/ui/alert.tsx (1)
Alert(66-66)web/src/components/ui/badge.tsx (1)
Badge(51-51)web/src/types/index.ts (5)
RSSItems(1594-1596)isRSSFeed(1599-1601)RSSFeed(1583-1591)RSSAutoDownloadRule(1624-1642)Category(435-438)web/src/hooks/useInstanceCapabilities.ts (1)
useInstanceCapabilities(15-34)web/src/components/ui/input.tsx (1)
Input(26-26)web/src/hooks/useDateTimeFormatters.ts (1)
useDateTimeFormatters(13-52)web/src/lib/linkUtils.tsx (1)
renderTextWithLinks(17-53)web/src/lib/category-utils.ts (2)
buildCategorySelectOptions(9-33)buildTagSelectOptions(36-42)web/src/components/ui/multi-select.tsx (1)
MultiSelect(26-170)
🔇 Additional comments (10)
web/src/components/layout/MobileFooterNav.tsx (1)
410-418: LGTM - RSS menu item integrated correctly.The RSS navigation item follows the established pattern and is properly positioned in the Settings dropdown menu.
web/src/pages/RSSPage.tsx (6)
121-499: Well-structured main component with proper state management.The RSSPage component correctly handles:
- Instance selection with persistence
- Auto-selection of first connected instance
- SSE status monitoring with duplicate toast prevention
- Conditional rendering for no-instance states
- Clear warning banners with action buttons
809-971: Excellent recursive tree implementation with auto-expand.The FeedTree component provides a solid navigation experience with:
- Auto-expansion of parent folders when a deep feed is selected
- Efficient state management using Set for expanded folders
- Clear visual indicators for loading, errors, and unread counts
- Proper keyboard and mouse interaction
984-1146: Articles display correctly implemented.The ArticlesPanel and ArticleRow components properly handle:
- Date formatting using the
useDateTimeFormattershook- HTML content sanitization with entity decoding
- Collapsible article details with correct Tailwind data attribute selectors
- Link rendering with the
renderTextWithLinksutility- Read/unread state management
1163-1434: Rules management UI is well-designed.The rules components provide a complete management interface with:
- Toggle, edit, and remove actions with proper state updates
- Preview functionality showing matching articles per feed
- Compact, informative rule cards with filter summaries
- Proper error handling and loading states
1440-2077: Dialog components demonstrate excellent code reuse.The dialog implementations show good practices:
- Shared
RuleFormFieldscomponent eliminates duplication between Add and Edit dialogs- Proper form state initialization from existing data
- Date formatting correctly uses the
useDateTimeFormattershook- Consistent validation and error handling patterns
- Clear user feedback via toast notifications
2083-2248: Helper functions and settings popover are well-implemented.The utility functions and settings component provide:
- Clean, pure helper functions for tree traversal and data extraction
- Settings popover with proper state synchronization
- Good UX with local state that syncs to preferences on save
- Clear feedback for settings changes
web/src/components/layout/Header.tsx (1)
542-550: LGTM - RSS navigation item added correctly.The RSS menu item follows the established pattern for dropdown items and is appropriately positioned in the navigation hierarchy.
web/src/components/ui/alert.tsx (2)
18-18: LGTM: Destructive variant styling is well-structured.The destructive variant uses semantic color tokens and follows best practices with appropriate opacity modifiers for background and border. The dark mode variant ensures proper visibility across themes.
19-20: Change meets WCAG AA accessibility standards.The contrast ratio for
text-yellow-800onbg-yellow-500/10is 6.39:1, exceeding the required 4.5:1 for WCAG AA compliance. The fix is sufficient.
✅ Deploy Preview for getqui ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (5)
internal/qbittorrent/sync_manager.go (1)
5514-5548: ReprocessRSSRules correctly preserves user setting, with a small edge-case caveatThe new implementation fixes the previous behaviour by:
- Reading
RssAutoDownloadingEnabledfrom preferences,- Forcing a
false -> truetransition to trigger processing,- Restoring the original setting when it was disabled.
The only remaining edge case is if the final “restore to false” call fails: in that (rare) case a user who had auto‑downloading disabled will end up with it enabled. Since reprocessing has already run successfully at that point, you may want to treat the restore failure as a logged warning instead of a hard error, to avoid surprising persistent state changes on transient failures.
If you agree this is worth tightening, you could log the restore error and return
nilso callers still treat the reprocess as successful while keeping telemetry about the failed restore.web/src/types/index.ts (3)
397-412: InstanceCapabilities: ensure supportsSetRSSFeedURL is always defined client‑sideAdding
supportsSetRSSFeedURL: booleanis a good way to feature‑gate the “edit feed URL” UI. Just make sure the backend always includes this flag (defaulting tofalsefor older qBittorrent versions) so the frontend never has to deal withundefinedhere.
1922-1999: RSS models and helpers are well‑structured for the UI’s tree + rulesThe
RSSArticle,RSSFeed,RSSItems, andisRSSFeedtype guard form a clean, recursive model for the feed/folder tree and per‑feed metadata. A couple of small observations:
isRSSFeedusing"url" in item && typeof item.url === "string"is a pragmatic discriminator and should work fine given how you construct items.RSSRuleTorrentParams/RSSAutoDownloadRulecapture the modern rule shape while still preserving legacy fields (e.g.addPaused,savePath) so existing rules can round‑trip safely.Overall this gives the UI enough structure for editing, previewing matches, and surfacing rule metadata without over‑specifying qBittorrent internals.
2000-2040: RSS request payload types line up with the server API surfaceThe request types (
AddRSSFolderRequest,AddRSSFeedRequest,SetRSSFeedURLRequest,MoveRSSItemRequest,RemoveRSSItemRequest,RefreshRSSItemRequest,MarkRSSAsReadRequest,SetRSSRuleRequest,RenameRSSRuleRequest) are minimal and map cleanly onto the operations exposed by the new RSS handlers.One minor detail to keep in mind:
MoveRSSItemRequest.destPathbeing optional (withundefinedmeaning “move to root”) relies on the handler normalisingundefinedto the empty string or equivalent. As long as the handler does that consistently, these shapes look solid.internal/web/swagger/openapi.yaml (1)
3771-4050: RSS path surface looks coherent and consistent with the rest of the API.The new RSS endpoints (events, items, folders/feeds, articles, and rules) follow existing naming/verb patterns (
/instances/{instanceID}/…), status codes, and error conventions and line up with the TS client calls (paths and payload field names all match). No functional issues from the spec side.One minor improvement you might consider later: for
POST /api/instances/{instanceID}/rss/feedsthe 201 response is always described asWarningResponse, but the backend can legitimately return an empty body on full success (frontend already types this asWarningResponse | undefined). If you rely on codegen clients, it may be worth clarifying in the spec (e.g. documenting that the body may be empty / warning-only) to avoid surprises, but it’s not a blocker.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (11)
go.modinternal/api/handlers/helpers.gointernal/api/server.gointernal/qbittorrent/sync_manager.gointernal/web/swagger/openapi.yamlweb/src/components/layout/Header.tsxweb/src/components/layout/MobileFooterNav.tsxweb/src/components/layout/Sidebar.tsxweb/src/lib/api.tsweb/src/routeTree.gen.tsweb/src/types/index.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- web/src/components/layout/Header.tsx
- web/src/components/layout/Sidebar.tsx
- go.mod
- web/src/components/layout/MobileFooterNav.tsx
- internal/api/server.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-28T18:44:10.496Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 876
File: internal/logstream/hub_test.go:188-192
Timestamp: 2025-12-28T18:44:10.496Z
Learning: In Go 1.25 (Aug 2025), use wg.Go(func()) to spawn a goroutine and automate the Add/Done lifecycle. Replace manual patterns like wg.Add(1); go func(){ defer wg.Done(); ... }() with wg.Go(func(){ ... }). Ensure the codebase builds with Go 1.25+ and apply this in relevant Go files (e.g., internal/logstream/hub_test.go). If targeting older Go versions, maintain the existing pattern.
Applied to files:
internal/api/handlers/helpers.gointernal/qbittorrent/sync_manager.go
🧬 Code graph analysis (2)
web/src/types/index.ts (1)
internal/api/handlers/helpers.go (1)
WarningResponse(22-24)
internal/qbittorrent/sync_manager.go (1)
web/src/types/index.ts (4)
RSSItems(1946-1948)RSSRules(1996-1996)RSSAutoDownloadRule(1976-1994)RSSMatchingArticles(1998-1998)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (8)
internal/qbittorrent/sync_manager.go (1)
5382-5512: RSS SyncManager proxies are consistent and minimalThe new RSS methods cleanly mirror existing patterns: they fetch the instance client once, wrap client acquisition errors with context, and delegate to the qBittorrent RSS APIs without adding extra behaviour. No issues from a correctness or style standpoint.
internal/api/handlers/helpers.go (1)
21-24: WarningResponse type aligns with planned usageIntroducing
WarningResponseas a simple{ "warning": string }envelope is consistent withErrorResponseand matches the frontendWarningResponseinterface. It gives you a clear way to return 2xx responses with structured caveats without touching generic helpers.web/src/routeTree.gen.ts (1)
18-251: Generated /rss route wiring looks consistentThe new
AuthenticatedRssRouteimport, route definition, and all associated entries inFileRoutesByFullPath,FileRoutesByTo,FileRoutesById, module augmentation, andAuthenticatedRouteChildrenfollow the same pattern as existing authenticated routes (e.g./search,/settings). This should give you a correctly typed and navigable/rssroute in the authenticated tree.Also applies to: 321-341
web/src/types/index.ts (1)
19-22: TS WarningResponse matches backend shapeThe
WarningResponseinterface (warning?: string) matches the new GoWarningResponsestruct and gives you a generic, reusable success‑with‑caveats envelope for the client without over‑constraining payloads.internal/web/swagger/openapi.yaml (2)
5970-6109: RSS schemas look accurate and match the client-facing types.
WarningResponse,RSSItems(recursive map),RSSFeed,RSSArticle,RSSAutoDownloadRule, andRSSRuleTorrentParamsall mirror the shapes implied by the TS types and qBittorrent’s RSS structures (including nested rules and torrent params). Field naming and optionality look reasonable for the current UI usage; I don’t see any correctness issues here.
6135-6136: Good addition of dedicated RSS tag.Tagging all new endpoints under a discrete
RSStag keeps the API surface organized and discoverable alongside existing groups (Torrents, Cross-Seed, Orphan Scan, etc.). No changes needed.web/src/lib/api.ts (2)
6-104: New type imports are well-scoped and all used.All added imports (
AddRSSFeedRequest,AddRSSFolderRequest,SetRSSFeedURLRequest,MoveRSSItemRequest,RemoveRSSItemRequest,RefreshRSSItemRequest,RenameRSSRuleRequest,MarkRSSAsReadRequest,RSSItems,RSSRules,RSSMatchingArticles,WarningResponse,WebSeed,LocalCrossSeedMatch) are consumed by the methods below and line up with the corresponding OpenAPI schemas. No dead types or obvious mismatches here.
1974-2063: RSS client methods correctly mirror the backend API and handle empty responses.
- Paths and verbs match the OpenAPI spec:
/instances/{instanceId}/rss/items,/folders,/feeds,/feeds/url,/items/move,/items/refresh,/articles/read,/rules,/rules/{ruleName}/…, and/rules/reprocessare all wired as expected.- Request payload shapes (
AddRSSFolderRequest,AddRSSFeedRequest,SetRSSFeedURLRequest,MoveRSSItemRequest,RemoveRSSItemRequest,RefreshRSSItemRequest,MarkRSSAsReadRequest,SetRSSRuleRequest,RenameRSSRuleRequest) match the OpenAPI inline body objects.addRSSFeedreturningPromise<WarningResponse | undefined>is now aligned withrequest<T>()’s behavior for empty bodies and the backend’s “warning-only” response semantics, so callers can safely treat the result as optional.- Using
voidreturn types for purely side-effecting mutations (delete/move/refresh/mark-as-read/rules ops) plays nicely with the genericrequest<T>handling of 204/empty bodies.I don’t see any correctness issues or missing error handling specific to these methods; they’re consistent with the rest of the ApiClient surface.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@web/src/pages/RSSPage.tsx`:
- Around line 209-233: The Select is receiving "0" when no instance is selected,
which blocks the placeholder; update the value passed to the Select in
renderInstanceSelector so it yields an empty string unless instanceId is a valid
non-zero id (e.g., use a guard that checks instanceId !== 0 && instanceId !=
null before calling toString()), and ensure handleInstanceSelection can accept
and handle an empty string as "no selection" so the placeholder shows correctly.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@web/src/pages/RSSPage.tsx`:
- Around line 909-915: The feed/folder row divs (the click handler using
onSelect with itemPath and isSelected) are not keyboard-accessible; update the
clickable row elements in the RSSPage rendering to either be a real interactive
element (replace the <div> with a <button> or similar) or add role="button",
tabIndex={0} and an onKeyDown handler that triggers the same onSelect(itemPath)
when Enter or Space is pressed; apply the same change to the other identical row
instance (the one referenced around lines 972-976) so both feeds and folders are
keyboard-accessible and retain the existing onClick behavior and styling.
- Around line 132-142: Validate the persisted selectedInstanceId against the
current instances inside the useEffect that auto-selects instances: if
selectedInstanceId is set but not present in instances (check by id), clear it
or replace it with a valid id (prefer a connected instance via instances.find(i
=> i.connected) or fallback to instances[0].id) and then call
setSelectedInstanceId; this prevents querying a stale localStorage id. Ensure
the logic in the useEffect (referencing selectedInstanceId, instances,
setSelectedInstanceId) handles the case where selectedInstanceId exists but is
not found in instances before returning early.
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/autobrr/qui](https://github.com/autobrr/qui) | minor | `v1.12.0` → `v1.13.0` | --- ### Release Notes <details> <summary>autobrr/qui (ghcr.io/autobrr/qui)</summary> ### [`v1.13.0`](https://github.com/autobrr/qui/releases/tag/v1.13.0) [Compare Source](autobrr/qui@v1.12.0...v1.13.0) #### Highlights - Native RSS support for feeds - Cross-seed upgrades: directory scanning for data-based matching and category affix (prefix/suffix) modes - Automations improvements: Move action support, uncategorized filter option, hasMissingFiles condition, include-cross-seeds mode, configurable FREE\_SPACE source - UI improvements: inline pieces progress bar, clickable dashboard error counts, better mobile workflow modal - Orphan scan safeguards for shared save paths - External programs: new {comment} placeholder ##### Notable bug fixes: - More stable sorting for timestamp fields and last\_activity - Tracker icon fetching reliability and timeout cooldown handling - Cross-seed matching fixes (anime/pack matching, size mismatch handling, V2 hash recheck) - Orphan scan ignores .parts files and OS/NAS artifacts - qBittorrent tracker-down matching improvements and magnet redirect handling #### Changelog ##### New Features - [`9acd789`](autobrr/qui@9acd789): feat(automations): add Move action to Automations ([#​1079](autobrr/qui#1079)) ([@​Barcode-eng](https://github.com/Barcode-eng)) - [`80aaf22`](autobrr/qui@80aaf22): feat(automations): add hasMissingfiles condition ([#​1081](autobrr/qui#1081)) ([@​jabloink](https://github.com/jabloink)) - [`f942a71`](autobrr/qui@f942a71): feat(automations): add import button to empty state ([#​1111](autobrr/qui#1111)) ([@​jabloink](https://github.com/jabloink)) - [`6b2831e`](autobrr/qui@6b2831e): feat(automations): add include-cross-seeds mode with hardlink support, fix free-space projection ([#​1116](autobrr/qui#1116)) ([@​Barcode-eng](https://github.com/Barcode-eng)) - [`b152a6a`](autobrr/qui@b152a6a): feat(automations): add tooltips to disabled conditions ([#​1286](autobrr/qui#1286)) ([@​jabloink](https://github.com/jabloink)) - [`f36eb76`](autobrr/qui@f36eb76): feat(automations): add uncategorized category filter option ([#​1268](autobrr/qui#1268)) ([@​aulterego](https://github.com/aulterego)) - [`d7dcb58`](autobrr/qui@d7dcb58): feat(automations): configurable FREE\_SPACE source (qBit or path) ([#​1181](autobrr/qui#1181)) ([@​s0up4200](https://github.com/s0up4200)) - [`d685bee`](autobrr/qui@d685bee): feat(backups): add tooltips and restore from lastest button ([#​1258](autobrr/qui#1258)) ([@​jabloink](https://github.com/jabloink)) - [`4c5a975`](autobrr/qui@4c5a975): feat(crossseed): add category affix with prefix/suffix modes ([#​1296](autobrr/qui#1296)) ([@​jabloink](https://github.com/jabloink)) - [`28cd96d`](autobrr/qui@28cd96d): feat(crossseed): add directory scanner for data-based matching ([#​1203](autobrr/qui#1203)) ([@​s0up4200](https://github.com/s0up4200)) - [`fe84eb0`](autobrr/qui@fe84eb0): feat(dirscan): resumable scans with per-run limit ([#​1274](autobrr/qui#1274)) ([@​s0up4200](https://github.com/s0up4200)) - [`1af2a12`](autobrr/qui@1af2a12): feat(dockerfile): add bash to Dockerfile ([#​903](autobrr/qui#903)) ([@​ryanwalder](https://github.com/ryanwalder)) - [`5c84c3f`](autobrr/qui@5c84c3f): feat(docs): add browser extension links and URL shorteners ([@​s0up4200](https://github.com/s0up4200)) - [`13c5461`](autobrr/qui@13c5461): feat(linking): add option to fallback on linking failure ([#​1056](autobrr/qui#1056)) ([@​Audionut](https://github.com/Audionut)) - [`5f7031f`](autobrr/qui@5f7031f): feat(orphanscan): protect shared save paths across instances ([#​1197](autobrr/qui#1197)) ([@​s0up4200](https://github.com/s0up4200)) - [`341092d`](autobrr/qui@341092d): feat(rss): add native rss support ([#​801](autobrr/qui#801)) ([@​s0up4200](https://github.com/s0up4200)) - [`8d2a371`](autobrr/qui@8d2a371): feat(sort): add stable fallback sorting for timestamp fields ([#​1317](autobrr/qui#1317)) ([@​aulterego](https://github.com/aulterego)) - [`28cf4ad`](autobrr/qui@28cf4ad): feat(torrents): use custom tracker names in table ([#​1120](autobrr/qui#1120)) ([@​s0up4200](https://github.com/s0up4200)) - [`e0d2bb4`](autobrr/qui@e0d2bb4): feat(ui): Use 4 column grid for dashboard instances on 2xl screens ([#​1195](autobrr/qui#1195)) ([@​ewenjo](https://github.com/ewenjo)) - [`7bc98e1`](autobrr/qui@7bc98e1): feat(ui): add inline pieces progress bar visualization ([#​1050](autobrr/qui#1050)) ([@​s0up4200](https://github.com/s0up4200)) - [`0154226`](autobrr/qui@0154226): feat(web): Improve performance for torrents with large file counts ([#​1252](autobrr/qui#1252)) ([@​0rkag](https://github.com/0rkag)) - [`a91df70`](autobrr/qui@a91df70): feat(web): allow merging trackers into existing groups ([#​1175](autobrr/qui#1175)) ([@​jabloink](https://github.com/jabloink)) - [`132c512`](autobrr/qui@132c512): feat(web): improve workflow modal mobile ([#​1302](autobrr/qui#1302)) ([@​jabloink](https://github.com/jabloink)) - [`a7f59ab`](autobrr/qui@a7f59ab): feat(web): make dashboard error counts clickable ([#​937](autobrr/qui#937)) ([@​Gykes](https://github.com/Gykes)) ##### Bug Fixes - [`c6f86e2`](autobrr/qui@c6f86e2): fix(auth): recover cleanly behind upstream SSO ([#​1142](autobrr/qui#1142)) ([@​s0up4200](https://github.com/s0up4200)) - [`0c19899`](autobrr/qui@0c19899): fix(automations): handle -1 value for incomplete torrent completion\_on ([#​1186](autobrr/qui#1186)) ([@​s0up4200](https://github.com/s0up4200)) - [`59a9120`](autobrr/qui@59a9120): fix(automations): include Move action in `rulesUseCondition` ([#​1079](autobrr/qui#1079)) ([#​1287](autobrr/qui#1287)) ([@​Barcode-eng](https://github.com/Barcode-eng)) - [`618d945`](autobrr/qui@618d945): fix(automations): not being able to enter multiple tags in tag action ([#​1131](autobrr/qui#1131)) ([@​Winter](https://github.com/Winter)) - [`bffecbd`](autobrr/qui@bffecbd): fix(automations): restore preview API and add loading state ([#​1194](autobrr/qui#1194)) ([@​s0up4200](https://github.com/s0up4200)) - [`9b3975b`](autobrr/qui@9b3975b): fix(automations): speed up delete hardlink-copy expansion ([#​1187](autobrr/qui#1187)) ([@​s0up4200](https://github.com/s0up4200)) - [`1549bcf`](autobrr/qui@1549bcf): fix(automations): support qBittorrent global/unlimited limits ([#​1134](autobrr/qui#1134)) ([@​s0up4200](https://github.com/s0up4200)) - [`e31d4aa`](autobrr/qui@e31d4aa): fix(automations): validate local access for hasMissingFiles ([#​1281](autobrr/qui#1281)) ([@​jabloink](https://github.com/jabloink)) - [`dba3419`](autobrr/qui@dba3419): fix(crossseed): allow extra files in size mismatch check ([#​1149](autobrr/qui#1149)) ([@​s0up4200](https://github.com/s0up4200)) - [`f7dced6`](autobrr/qui@f7dced6): fix(crossseed): correct match type for episode-from-pack cross-seeding ([#​1250](autobrr/qui#1250)) ([@​neelmehta247](https://github.com/neelmehta247)) - [`007d87e`](autobrr/qui@007d87e): fix(crossseed): normalize ampersand to "and" for title matching ([#​1202](autobrr/qui#1202)) ([@​s0up4200](https://github.com/s0up4200)) - [`048c034`](autobrr/qui@048c034): fix(crossseed): prevent automation search stalls ([#​1272](autobrr/qui#1272)) ([@​s0up4200](https://github.com/s0up4200)) - [`5c48ccb`](autobrr/qui@5c48ccb): fix(crossseed): prevent false cross-seed delete warnings ([#​1148](autobrr/qui#1148)) ([@​s0up4200](https://github.com/s0up4200)) - [`eda0eee`](autobrr/qui@eda0eee): fix(crossseed): reduce false negatives for anime matching ([#​1243](autobrr/qui#1243)) ([@​s0up4200](https://github.com/s0up4200)) - [`8b08b87`](autobrr/qui@8b08b87): fix(crossseed): restore reuse matched category option ([#​1150](autobrr/qui#1150)) ([@​s0up4200](https://github.com/s0up4200)) - [`b69682d`](autobrr/qui@b69682d): fix(crossseed): support v2 hashes for recheck ([#​1237](autobrr/qui#1237)) ([@​s0up4200](https://github.com/s0up4200)) - [`8fb16c1`](autobrr/qui@8fb16c1): fix(database): remove noisy stmt cache promotion log ([#​1307](autobrr/qui#1307)) ([@​s0up4200](https://github.com/s0up4200)) - [`c8d068b`](autobrr/qui@c8d068b): fix(docs): correct autobrr integration documentation ([@​s0up4200](https://github.com/s0up4200)) - [`c6f4a4a`](autobrr/qui@c6f4a4a): fix(docs): use absolute URLs for extension links ([@​s0up4200](https://github.com/s0up4200)) - [`48510e9`](autobrr/qui@48510e9): fix(instances): auth bypass persistence in instance form ([#​1219](autobrr/qui#1219)) ([@​s0up4200](https://github.com/s0up4200)) - [`0b71b46`](autobrr/qui@0b71b46): fix(makefile): make fmt target only format changed files ([#​1200](autobrr/qui#1200)) ([@​s0up4200](https://github.com/s0up4200)) - [`1f13b0e`](autobrr/qui@1f13b0e): fix(orphanscan): avoid duplicate inode entries in scans ([#​1212](autobrr/qui#1212)) ([@​s0up4200](https://github.com/s0up4200)) - [`fecfebd`](autobrr/qui@fecfebd): fix(orphanscan): gate scans until qBittorrent settled ([#​1193](autobrr/qui#1193)) ([@​s0up4200](https://github.com/s0up4200)) - [`77b2fd6`](autobrr/qui@77b2fd6): fix(orphanscan): ignore .parts files from qBittorrent partial downloads ([#​1264](autobrr/qui#1264)) ([@​s0up4200](https://github.com/s0up4200)) - [`0f7e646`](autobrr/qui@0f7e646): fix(orphanscan): ignore OS/NAS/k8s artifacts ([#​1259](autobrr/qui#1259)) ([@​s0up4200](https://github.com/s0up4200)) - [`b55881c`](autobrr/qui@b55881c): fix(orphanscan): reduce MaxFilesPerRun and fix accordion chevrons ([#​1095](autobrr/qui#1095)) ([@​s0up4200](https://github.com/s0up4200)) - [`0cb682c`](autobrr/qui@0cb682c): fix(qbittorrent): force sync+retry in BulkAction ([#​1097](autobrr/qui#1097)) ([@​s0up4200](https://github.com/s0up4200)) - [`aba9b7e`](autobrr/qui@aba9b7e): fix(qbittorrent): strip URLs before tracker-down pattern matching ([#​1224](autobrr/qui#1224)) ([@​s0up4200](https://github.com/s0up4200)) - [`a60076f`](autobrr/qui@a60076f): fix(reflinktree): fall back to FICLONERANGE when FICLONE unsupported ([#​1221](autobrr/qui#1221)) ([@​s0up4200](https://github.com/s0up4200)) - [`d79a0db`](autobrr/qui@d79a0db): fix(search): handle magnet redirects from indexer downloads ([#​1211](autobrr/qui#1211)) ([@​s0up4200](https://github.com/s0up4200)) - [`1e920c4`](autobrr/qui@1e920c4): fix(sort): truncate `last_activity` to 60s for stability ([#​1318](autobrr/qui#1318)) ([@​aulterego](https://github.com/aulterego)) - [`931d720`](autobrr/qui@931d720): fix(torznab): respect per-indexer caps limits ([#​1265](autobrr/qui#1265)) ([@​s0up4200](https://github.com/s0up4200)) - [`6fe91e8`](autobrr/qui@6fe91e8): fix(trackericons): apply failure cooldown on timeouts ([#​1322](autobrr/qui#1322)) ([@​s0up4200](https://github.com/s0up4200)) - [`35a50b5`](autobrr/qui@35a50b5): fix(web): add max-height to import workflow dialog ([#​1207](autobrr/qui#1207)) ([@​s0up4200](https://github.com/s0up4200)) - [`5bd2706`](autobrr/qui@5bd2706): fix(web): hide redundant actions dropdown ([#​1305](autobrr/qui#1305)) ([@​jabloink](https://github.com/jabloink)) - [`307907f`](autobrr/qui@307907f): fix(web): improve Reannounce In column display ([#​1320](autobrr/qui#1320)) ([@​soggy-cr0uton](https://github.com/soggy-cr0uton)) - [`7881bdc`](autobrr/qui@7881bdc): fix(web): make Add Instance dialog scrollable on small viewports ([#​1232](autobrr/qui#1232)) ([@​s0up4200](https://github.com/s0up4200)) - [`bec5433`](autobrr/qui@bec5433): fix(web): make dialogs scrollable on small viewports ([#​1233](autobrr/qui#1233)) ([@​s0up4200](https://github.com/s0up4200)) - [`0e47b9e`](autobrr/qui@0e47b9e): fix(web): prevent nested scroll containers on iOS ([#​1229](autobrr/qui#1229)) ([@​s0up4200](https://github.com/s0up4200)) - [`f2f0616`](autobrr/qui@f2f0616): fix(web): redirect already authenticated users to dashboard ([#​1127](autobrr/qui#1127)) ([@​burritothief](https://github.com/burritothief)) - [`7a633cf`](autobrr/qui@7a633cf): fix(web): toast message when searching then adding cross-seeds ([#​1290](autobrr/qui#1290)) ([@​rybertm](https://github.com/rybertm)) - [`da14100`](autobrr/qui@da14100): fix(web): use fixed px for sidebar width instead of rem ([#​1313](autobrr/qui#1313)) ([@​soggy-cr0uton](https://github.com/soggy-cr0uton)) ##### Other Changes - [`c93a326`](autobrr/qui@c93a326): chore(deps): bump the golang group across 1 directory with 16 updates ([#​1261](autobrr/qui#1261)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`d3de074`](autobrr/qui@d3de074): chore(docs): clarify tracker pattern necessity in documentation ([@​s0up4200](https://github.com/s0up4200)) - [`2351097`](autobrr/qui@2351097): chore(lint): Add Linting workflow to CI ([#​1270](autobrr/qui#1270)) ([@​Barcode-eng](https://github.com/Barcode-eng)) - [`e8436e3`](autobrr/qui@e8436e3): chore(lint): update linting base branch from main to develop ([@​s0up4200](https://github.com/s0up4200)) - [`e36afc8`](autobrr/qui@e36afc8): chore: add Ko-fi funding and pre-commit config ([#​1315](autobrr/qui#1315)) ([@​s0up4200](https://github.com/s0up4200)) - [`df4375e`](autobrr/qui@df4375e): chore: update copyright headers to 2025-2026 ([#​1303](autobrr/qui#1303)) ([@​s0up4200](https://github.com/s0up4200)) - [`7a6f2d1`](autobrr/qui@7a6f2d1): docs(automations): clarify tag behavior ([#​1260](autobrr/qui#1260)) ([@​jabloink](https://github.com/jabloink)) - [`05e2028`](autobrr/qui@05e2028): refactor(build): cross platform building ([#​1060](autobrr/qui#1060)) ([@​Audionut](https://github.com/Audionut)) - [`6355f4f`](autobrr/qui@6355f4f): refactor(crosssed): require full recheck for disc based content ([#​1168](autobrr/qui#1168)) ([@​Audionut](https://github.com/Audionut)) - [`7466a98`](autobrr/qui@7466a98): refactor(filesmanager): improve logging messages for orphan cleanup process ([@​s0up4200](https://github.com/s0up4200)) - [`ad90209`](autobrr/qui@ad90209): refactor(orphan): improve disc based content handling ([#​1167](autobrr/qui#1167)) ([@​Audionut](https://github.com/Audionut)) - [`3b85f8a`](autobrr/qui@3b85f8a): refactor(web): use TanStack Query for path autocomplete ([#​1309](autobrr/qui#1309)) ([@​soggy-cr0uton](https://github.com/soggy-cr0uton)) **Full Changelog**: <autobrr/qui@v1.12.0...v1.13.0> #### Docker images - `docker pull ghcr.io/autobrr/qui:v1.13.0` - `docker pull ghcr.io/autobrr/qui:latest` #### What to do next? - Join our [Discord server](https://discord.autobrr.com/qui) Thank you for using qui! </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi42OS4yIiwidXBkYXRlZEluVmVyIjoiNDIuNjkuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=--> Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/3540 Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net> Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>
Closes #478
Relies on autobrr/go-qbittorrent#100
Summary by CodeRabbit
New Features
API
Frontend
Chores
✏️ Tip: You can customize this high-level summary in your review settings.