feat(crossseed): add RSS source filters for categories and tags - #757
Conversation
Add ability to filter which local torrents are considered when searching for cross-seed matches in RSS Automation. Users can now include or exclude torrents based on categories and tags. - Add 4 new filter fields to CrossSeedAutomationSettings model - Add database migration for new columns (JSON arrays) - Implement matchesRSSSourceFilters() with case-sensitive matching - Add debug logging when filters reduce torrent candidates - Add MultiSelect dropdowns populated from target instances - Aggregate categories/tags from all selected target instances Closes #730
WalkthroughAdds RSS source include/exclude filters to cross-seed: DB columns, model fields, API patch fields and application logic, service-level filtering during snapshot construction, and frontend types/UI to edit and persist the filters. Changes
Sequence Diagram(s)sequenceDiagram
participant Frontend
participant API
participant Service
participant DB
Frontend->>API: PATCH /cross-seed/settings (rssSource* fields)
API->>DB: Upsert cross_seed_settings (encode rss_source_* JSON)
DB-->>API: OK
API-->>Frontend: 200 OK (updated settings)
note right of Service: Snapshot build during automation run
Service->>DB: GetSettings (decode rss_source_* JSON)
DB-->>Service: CrossSeedAutomationSettings (with rssSource* lists)
Service->>Service: buildAutomationSnapshots(settings)
Service->>Service: matchesRSSSourceFilters(torrent, settings)
Service->>Service: filter candidate torrents per snapshot
Service->>DB: record run results (include indexerName)
DB-->>Service: OK
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/services/crossseed/service.go (1)
1913-1989: Guard against nil instance deref in new debug logging + normalize filter inputs before use.
log.Debug().Str("instanceName", snap.instance.Name)(Line 1981) assumessnap.instance != nil. IfinstanceStore.Getcan ever return(nil, nil), this panics.Also, filtering uses raw
settings.RSSSource*values; consider trimming/deduping once (ideally invalidateAndNormalizeSettings) to avoid whitespace-caused misses.@@ - if len(torrents) != originalCount { - log.Debug(). - Int("instanceID", instanceID). - Str("instanceName", snap.instance.Name). + if len(torrents) != originalCount { + instanceName := "" + if snap.instance != nil { + instanceName = snap.instance.Name + } + log.Debug(). + Int("instanceID", instanceID). + Str("instanceName", instanceName). Int("original", originalCount). Int("filtered", len(torrents)). Msg("RSS source filters reduced torrent candidates") }
🧹 Nitpick comments (7)
internal/database/migrations/032_add_rss_source_filters.sql (1)
1-8: Consider adding JSON validity CHECKs for these TEXT “JSON array” columns.
Right now any non-JSON string could be written (manually or via a buggy client), andjson.Unmarshalin the app will fail hard on read. If you want stronger safety, addCHECK (json_valid(rss_source_categories))(and similarly for the other three) where supported.web/src/pages/CrossSeedPage.tsx (3)
380-391: Normalize rssSource lists on load.*
Right nowsettings.rssSourceCategories ?? []etc. are taken verbatim; if older data (or manual edits) contain whitespace/duplicates, the MultiSelect UX can get weird (duplicate pills, partial unselect). SuggestnormalizeStringList(...)for each of the four arrays here.
831-871: Consider sorting tag/category option labels for stable UX across instance ordering.
Setinsertion order depends on iteration order of the selected instances; sortingrssSourceTagNames(and maybe ensuring category keys are stable) avoids the dropdown “jumping” when instance selection order changes.
1253-1339: UI controls are clear; consider normalizing onChange to keep store data clean.
For the four RSS source MultiSelects, you currently storevaluesas-is. Even though the component avoids duplicates during selection, normalizing (normalizeStringList(values)) would keep persisted settings resilient to casing/whitespace drift and future UI changes.internal/api/handlers/crossseed.go (2)
164-255: Recommend normalizing RSSSource values when applying the patch (trim/dedup, maybe exclude-wins).*
Right now these arrays are accepted verbatim; it’s easy to end up persisting values like" movies "or duplicates, and it’s unclear what should happen if the same value appears in include+exclude. Consider normalizing at this boundary (or in the service/store) to keep persisted state predictable.
596-633: PUT/cross-seed/settingsdoesn’t include the new RSS source filter fields.
If PUT is intended to be a complete replacement/update API, consider extendingautomationSettingsRequest+ UpdateAutomationSettings() to include these 4 fields as well (otherwise it’s patch-only).internal/models/crossseed.go (1)
395-540: Upsert column list/placeholder count/args ordering look consistent.
Optional improvement: normalize these four slices before encoding (trim/dedup) so the DB never stores junk values (helps when other clients write settings besides the web UI).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
internal/api/handlers/crossseed.go(3 hunks)internal/database/migrations/032_add_rss_source_filters.sql(1 hunks)internal/models/crossseed.go(11 hunks)internal/services/crossseed/service.go(5 hunks)web/src/pages/CrossSeedPage.tsx(7 hunks)web/src/types/index.ts(2 hunks)
🧰 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/models/crossseed.gointernal/services/crossseed/service.go
📚 Learning: 2025-11-27T15:33:39.007Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.
Applied to files:
internal/models/crossseed.go
🧬 Code graph analysis (1)
web/src/pages/CrossSeedPage.tsx (2)
web/src/components/torrents/CategoryTree.tsx (2)
buildCategoryTree(59-114)CategoryNode(26-34)web/src/components/ui/multi-select.tsx (1)
MultiSelect(26-170)
⏰ 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 (11)
web/src/pages/CrossSeedPage.tsx (3)
52-62: Nice: form state cleanly separates RSS “target instances/indexers” from RSS “source filters”.
The naming/doc comment makes intent clear (“filter which local torrents to search”).
99-109: Defaults look correct (empty arrays => no filtering).
Matches the PR behavior contract and avoids null/undefined footguns in the UI.
478-504: Patch builder wiring looks consistent with the backend patch semantics.
You’re always sending these arrays (rather than omitting), which is fine since this “Save RSS automation settings” action is effectively a full-save of the automation section.internal/api/handlers/crossseed.go (2)
47-77: Patch request additions look correct and consistent with existing pointer-based patching.
Using*[]stringpreserves “omitted vs set to empty array” semantics.
135-162:isEmpty()updated correctly to account for the new fields.
This prevents silent no-op PATCHes when a client only updates RSS source filters.web/src/types/index.ts (2)
1403-1434: Types align with the backend contract and keep arrays non-null on the read model.
Good match for “empty array means all”.
1436-1465: Patch type shape is correct (optional arrays).
Supports “clear” by sending[], and “no change” by omitting.internal/models/crossseed.go (3)
18-64: Model field additions are clear and correctly documented (“LOCAL torrents”, empty means all).
Naming matches API/UI (rssSource*).
75-112: Defaults are set to empty slices (good for JSON + UI), and semantics are documented inline.
271-393: GetSettings read path looks correct (select/scan/decode for all 4 columns).
decodeStringSlicefallback to[]preserves the “no filtering” contract.internal/services/crossseed/service.go (1)
1539-1542: Good propagation of settings into snapshot building (keeps filtering concerns localized).
This comment was marked as resolved.
This comment was marked as resolved.
- Add indexerName field to CrossSeedRunResult to show which indexer releases came from in the RSS run details UI - Fix tag matching to be case-sensitive (Movies != movies) to match qBittorrent's actual behavior in matchesSearchFilters, matchesCompletionFilters, and hasCrossSeedTag - Improve RSS runs UI with expandable rows showing instance and indexer badges alongside matched torrent names
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/services/crossseed/service.go (1)
6434-6463: Case-sensitive tag matching requires confirmation or case normalization on save.
matchesSearchFilters,matchesCompletionFilters, andhasCrossSeedTagnow enforce strict case-sensitive equality for tag comparisons. qBittorrent treats tags as case-sensitive, so this behavior is correct; however, any existing persisted settings with tags in different casings (e.g., "Cross-Seed" vs "cross-seed") will silently stop matching. Consider normalizing tag casing on save to maintain backward compatibility with existing configurations, or document this as a breaking change requiring users to update their tag references.Also applies to: 6506-6545, 6547-6554
♻️ Duplicate comments (2)
web/src/pages/CrossSeedPage.tsx (1)
327-356: Aggregation should tolerate per-instance failures + stabilize queryKey ordering.
Promise.allmeans one instance failing categories/tags breaks the whole dropdown population, and the array-basedqueryKeywill refetch when the same IDs are selected in a different order.+ const sortedRssTargetInstanceIds = useMemo( + () => [...automationForm.targetInstanceIds].sort((a, b) => a - b), + [automationForm.targetInstanceIds] + ) + // Fetch categories/tags from all RSS Automation target instances (aggregated) const { data: rssSourceMetadata } = useQuery({ - queryKey: ["cross-seed", "rss-source-metadata", automationForm.targetInstanceIds], + queryKey: ["cross-seed", "rss-source-metadata", sortedRssTargetInstanceIds], queryFn: async () => { - if (automationForm.targetInstanceIds.length === 0) return null - const results = await Promise.all( - automationForm.targetInstanceIds.map(async (instanceId) => { - const [categories, tags] = await Promise.all([ - api.getCategories(instanceId), - api.getTags(instanceId), - ]) - return { categories, tags } - }) - ) + if (sortedRssTargetInstanceIds.length === 0) return null + const settled = await Promise.allSettled( + sortedRssTargetInstanceIds.map(async (instanceId) => { + const [categories, tags] = await Promise.all([ + api.getCategories(instanceId), + api.getTags(instanceId), + ]) + return { instanceId, categories, tags } + }) + ) + const results = settled + .filter((r): r is PromiseFulfilledResult<{ instanceId: number; categories: any; tags: string[] }> => r.status === "fulfilled") + .map(r => r.value) // Aggregate categories and tags from all instances const allCategories: Record<string, { name: string; savePath: string }> = {} const allTags = new Set<string>() - for (const result of results) { + for (const result of results) { for (const [name, cat] of Object.entries(result.categories)) { allCategories[name] = cat } for (const tag of result.tags) { allTags.add(tag) } } - return { categories: allCategories, tags: Array.from(allTags) } + return { categories: allCategories, tags: Array.from(allTags) } }, - enabled: automationForm.targetInstanceIds.length > 0, + enabled: sortedRssTargetInstanceIds.length > 0, staleTime: 5 * 60 * 1000, })internal/services/crossseed/service.go (1)
6465-6504: FixmatchesRSSSourceFiltersnil-settings contract (nil should mean “no filtering”).
The comment says empty filters mean “all”, butsettings == nilcurrently returnsfalse, which is a surprising helper contract and can cause accidental “filter everything out” if reused.func matchesRSSSourceFilters(torrent *qbt.Torrent, settings *models.CrossSeedAutomationSettings) bool { - if torrent == nil || settings == nil { - return false - } + if torrent == nil { + return false + } + if settings == nil { + return true // no filters configured + }
🧹 Nitpick comments (6)
web/src/pages/CrossSeedPage.tsx (2)
835-875: Sort/dedupe aggregated tag & category options for stable UX.
Right now the Set/iteration order can make the dropdown jump around between renders/instance selections.- const rssSourceTagNames = useMemo(() => rssSourceMetadata?.tags ?? [], [rssSourceMetadata]) + const rssSourceTagNames = useMemo( + () => (rssSourceMetadata?.tags ?? []).slice().sort((a, b) => a.localeCompare(b)), + [rssSourceMetadata] + )(Also consider sorting
flattenedbefore returning inrssSourceCategorySelectOptionsif tree order isn’t guaranteed by input.)
1343-1548: Runs panel is readable, but the Scheduled/Manual/Other render blocks are largely duplicated.
A smallRunGroup+RunRowcomponent (or a map over{key,label,icon,iconClass}) would reduce repetition and future bug drift.internal/models/crossseed.go (2)
31-37: Consider normalizing filter inputs before persisting (trim/dedupe)
Right now whatever the UI sends is stored verbatim; it’s easy to end up with duplicates or whitespace-only entries. You already havenormalizeStringSlice(Line 1292+); consider applying it toRSSSource*(and possibly other tag/category slices) inUpsertSettingsbefore encoding.
76-111: Avoid doubletime.Now()to prevent subtle CreatedAt/UpdatedAt skew
CreatedAtandUpdatedAtare set via separatetime.Now().UTC()calls (Line 109-110). Prefer a singlenow := time.Now().UTC()for consistency.internal/services/crossseed/service.go (2)
1619-1666: MakeIndexerNameconsistent: use the resolvedsourceIndexerinstead ofresult.Indexerin run results.
You already resolvesourceIndexerviaGetIndexerNameFromInfo(Line 1620-1623), but severalCrossSeedRunResult{ IndexerName: ... }assignments still useresult.Indexer, which can diverge from what you send inCrossSeedRequest.IndexerName.- IndexerName: result.Indexer, + IndexerName: sourceIndexer,(Apply similarly to the
existspre-check result blocks and the “no_match”/“dry-run”/“no_result” cases.)Also applies to: 1695-1705, 1752-1762, 1833-1893
1919-2002: RSS source filtering in snapshot build looks correct; consider normalizing filter slices once.
The overall flow (computehasRSSSourceFilters, filter per instance, log reduction) is solid. If these lists can get large, converting settings slices tomap[string]struct{}once per run would avoid repeatedslices.Containsin tight loops.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
internal/models/crossseed.go(11 hunks)internal/services/crossseed/service.go(15 hunks)internal/web/swagger/openapi.yaml(1 hunks)web/src/pages/CrossSeedPage.tsx(9 hunks)web/src/types/index.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- web/src/types/index.ts
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
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.
📚 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/services/crossseed/service.gointernal/models/crossseed.go
📚 Learning: 2025-12-11T08:39:55.579Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:39:55.579Z
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:
internal/services/crossseed/service.go
📚 Learning: 2025-11-21T21:11:50.633Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 625
File: internal/qbittorrent/sync_manager.go:1112-1123
Timestamp: 2025-11-21T21:11:50.633Z
Learning: In internal/qbittorrent/sync_manager.go, the GetCachedFilesBatch interface documentation (around line 39-40) should specify "uppercase hex" instead of "lowercase hex" to match the actual normalization practice used throughout the codebase (e.g., normalizeHash in internal/services/crossseed/service.go uses strings.ToUpper, and hash filtering uses uppercase).
Applied to files:
internal/services/crossseed/service.go
📚 Learning: 2025-11-27T15:33:39.007Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.
Applied to files:
internal/services/crossseed/service.go
🧬 Code graph analysis (3)
internal/services/crossseed/service.go (2)
internal/models/crossseed.go (1)
CrossSeedAutomationSettings(20-64)web/src/types/index.ts (3)
CrossSeedAutomationSettings(1404-1435)Torrent(226-283)Category(427-430)
internal/models/crossseed.go (1)
web/src/types/index.ts (2)
CrossSeedAutomationSettings(1404-1435)Category(427-430)
web/src/pages/CrossSeedPage.tsx (7)
web/src/lib/api.ts (1)
api(1711-1711)web/src/components/torrents/CategoryTree.tsx (2)
buildCategoryTree(59-114)CategoryNode(26-34)web/src/components/ui/label.tsx (1)
Label(29-29)web/src/components/ui/multi-select.tsx (1)
MultiSelect(26-170)web/src/components/ui/separator.tsx (1)
Separator(33-33)web/src/components/ui/collapsible.tsx (3)
Collapsible(14-14)CollapsibleTrigger(14-14)CollapsibleContent(14-14)web/src/components/ui/badge.tsx (1)
Badge(51-51)
⏰ 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 (7)
internal/web/swagger/openapi.yaml (1)
3741-3750:indexerNameaddition looks consistent with existing CrossSeedRunResult shape.
CamelCase matches the schema (instanceId,instanceName) andnullable: truealigns with other optional fields.web/src/pages/CrossSeedPage.tsx (1)
55-66: New RSS source filter fields are wired through defaults → load → patch payload correctly.
This is the right shape for “empty means include all / exclude none”, and the persistence path looks consistent.Also applies to: 103-113, 384-395, 482-507
internal/models/crossseed.go (4)
31-37: Run gofmt: struct field alignment is currently noisy
The PR notes already mention gofmt issues; these new fields add more alignment churn. Please run gofmt on this file so the struct formatting matches repo conventions.
160-170:CrossSeedRunResult.IndexerNameaddition is fine; verify producers populate it
This struct is (de)serialized viaresults_json, so the store layer won’t populate it directly—make sure the service layer that constructsrun.ResultssetsIndexerNamewhere intended.
274-370: Settings read path looks consistent (SELECT/Scan/decode), but ensure DB migration guarantees column presence
The SELECT + Scan ordering for the newrss_source_*columns matches the decode calls, so this looks correct. Please confirm the migration forcross_seed_settingsadds these columns (and doesn’t break older DBs) sinceGetSettingswill now always select them.
396-535: Upsert placeholder/arg count matches; consider making nil vs empty semantics explicit
The INSERT has 27 placeholders and you pass 27 args—good. SinceencodeStringSliceforcesnil -> [], you’ve chosen “always an array” semantics; just ensure this matches any PATCH semantics (e.g., omitting a field vs clearing it) upstream.internal/services/crossseed/service.go (1)
1539-1542: Good: threading automation settings into snapshot build enables correct RSS source filtering.
This change makes the filtering apply “upstream” (before candidate construction), which should reduce work later.
This comment was marked as resolved.
This comment was marked as resolved.
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/autobrr/qui](https://github.com/autobrr/qui) | minor | `v1.9.1` -> `v1.10.0` | --- ### Release Notes <details> <summary>autobrr/qui (ghcr.io/autobrr/qui)</summary> ### [`v1.10.0`](https://github.com/autobrr/qui/releases/tag/v1.10.0) [Compare Source](autobrr/qui@v1.9.1...v1.10.0) #### Changelog ##### New Features - [`f2b17e6`](autobrr/qui@f2b17e6): feat(config): add SESSION\_SECRET\_FILE env var ([#​661](autobrr/qui#661)) ([@​undefined-landmark](https://github.com/undefined-landmark)) - [`f5ede56`](autobrr/qui@f5ede56): feat(crossseed): add RSS source filters for categories and tags ([#​757](autobrr/qui#757)) ([@​s0up4200](https://github.com/s0up4200)) - [`9dee7bb`](autobrr/qui@9dee7bb): feat(crossseed): add Unicode normalization for title and file matching ([#​742](autobrr/qui#742)) ([@​s0up4200](https://github.com/s0up4200)) - [`d44058f`](autobrr/qui@d44058f): feat(crossseed): add skip auto-resume settings per mode ([#​755](autobrr/qui#755)) ([@​s0up4200](https://github.com/s0up4200)) - [`9e3534a`](autobrr/qui@9e3534a): feat(crossseed): add webhook source filters for categories and tags ([#​763](autobrr/qui#763)) ([@​s0up4200](https://github.com/s0up4200)) - [`c8bbe07`](autobrr/qui@c8bbe07): feat(crossseed): only poll status endpoints when features are enabled ([#​738](autobrr/qui#738)) ([@​s0up4200](https://github.com/s0up4200)) - [`fda8101`](autobrr/qui@fda8101): feat(sidebar): add size tooltips and deduplicate cross-seed sizes ([#​724](autobrr/qui#724)) ([@​s0up4200](https://github.com/s0up4200)) - [`e4c0556`](autobrr/qui@e4c0556): feat(torrent): add sequential download toggles ([#​776](autobrr/qui#776)) ([@​rare-magma](https://github.com/rare-magma)) - [`2a43f15`](autobrr/qui@2a43f15): feat(torrents): autocomplete paths ([#​634](autobrr/qui#634)) ([@​rare-magma](https://github.com/rare-magma)) - [`1c07b33`](autobrr/qui@1c07b33): feat(torrents): replace filtered speeds with global ([#​745](autobrr/qui#745)) ([@​jabloink](https://github.com/jabloink)) - [`cd0deee`](autobrr/qui@cd0deee): feat(tracker): add per-domain stats inclusion toggle for merged trackers ([#​781](autobrr/qui#781)) ([@​s0up4200](https://github.com/s0up4200)) - [`b6a6200`](autobrr/qui@b6a6200): feat(web): add Size column to Tracker Breakdown table ([#​770](autobrr/qui#770)) ([@​s0up4200](https://github.com/s0up4200)) - [`560071b`](autobrr/qui@560071b): feat(web): add zebra striping to torrent table ([#​726](autobrr/qui#726)) ([@​s0up4200](https://github.com/s0up4200)) - [`f8f65a8`](autobrr/qui@f8f65a8): feat(web): improve auto-search on completion UX ([#​743](autobrr/qui#743)) ([@​s0up4200](https://github.com/s0up4200)) - [`e36312f`](autobrr/qui@e36312f): feat(web): improve torrent selection UX with unified click and escape behavior ([#​782](autobrr/qui#782)) ([@​s0up4200](https://github.com/s0up4200)) - [`27c1daa`](autobrr/qui@27c1daa): feat(web): napster theme ([#​728](autobrr/qui#728)) ([@​s0up4200](https://github.com/s0up4200)) - [`e3950de`](autobrr/qui@e3950de): feat(web): new torrent details panel for desktop ([#​760](autobrr/qui#760)) ([@​s0up4200](https://github.com/s0up4200)) - [`6c66ba5`](autobrr/qui@6c66ba5): feat(web): persist tab state in URL for CrossSeed and Settings pages ([#​775](autobrr/qui#775)) ([@​s0up4200](https://github.com/s0up4200)) - [`59884a9`](autobrr/qui@59884a9): feat(web): share tracker customizations with filtersidebar ([#​717](autobrr/qui#717)) ([@​s0up4200](https://github.com/s0up4200)) ##### Bug Fixes - [`fafd278`](autobrr/qui@fafd278): fix(api): add webhook source filter fields to PATCH settings endpoint ([#​774](autobrr/qui#774)) ([@​s0up4200](https://github.com/s0up4200)) - [`bdf0339`](autobrr/qui@bdf0339): fix(api): support apikey query param with custom base URL ([#​748](autobrr/qui#748)) ([@​s0up4200](https://github.com/s0up4200)) - [`c3c8d66`](autobrr/qui@c3c8d66): fix(crossseed): compare Site and Sum fields for anime releases ([#​769](autobrr/qui#769)) ([@​s0up4200](https://github.com/s0up4200)) - [`cb4c965`](autobrr/qui@cb4c965): fix(crossseed): detect file name differences and fix hasExtraSourceFiles ([#​741](autobrr/qui#741)) ([@​s0up4200](https://github.com/s0up4200)) - [`fd9e054`](autobrr/qui@fd9e054): fix(crossseed): fix batch completion searches and remove legacy settings ([#​744](autobrr/qui#744)) ([@​s0up4200](https://github.com/s0up4200)) - [`26706a0`](autobrr/qui@26706a0): fix(crossseed): normalize punctuation in title matching ([#​718](autobrr/qui#718)) ([@​s0up4200](https://github.com/s0up4200)) - [`db30566`](autobrr/qui@db30566): fix(crossseed): rename files before folder to avoid path conflicts ([#​752](autobrr/qui#752)) ([@​s0up4200](https://github.com/s0up4200)) - [`8886ac4`](autobrr/qui@8886ac4): fix(crossseed): resolve category creation race condition and relax autoTMM ([#​767](autobrr/qui#767)) ([@​s0up4200](https://github.com/s0up4200)) - [`f8f2a05`](autobrr/qui@f8f2a05): fix(crossseed): support game scene releases with RAR files ([#​768](autobrr/qui#768)) ([@​s0up4200](https://github.com/s0up4200)) - [`918adee`](autobrr/qui@918adee): fix(crossseed): treat x264/H.264/H264/AVC as equivalent codecs ([#​766](autobrr/qui#766)) ([@​s0up4200](https://github.com/s0up4200)) - [`c4b1f0a`](autobrr/qui@c4b1f0a): fix(dashboard): merge tracker customizations with duplicate displayName ([#​751](autobrr/qui#751)) ([@​jabloink](https://github.com/jabloink)) - [`3c6e0f9`](autobrr/qui@3c6e0f9): fix(license): remove redundant validation call after activation ([#​749](autobrr/qui#749)) ([@​s0up4200](https://github.com/s0up4200)) - [`a9c7754`](autobrr/qui@a9c7754): fix(reannounce): simplify tracker detection to match qbrr logic ([#​746](autobrr/qui#746)) ([@​s0up4200](https://github.com/s0up4200)) - [`3baa007`](autobrr/qui@3baa007): fix(rss): skip download when torrent already exists by infohash ([#​715](autobrr/qui#715)) ([@​s0up4200](https://github.com/s0up4200)) - [`55d0ccc`](autobrr/qui@55d0ccc): fix(swagger): respect base URL for API docs routes ([#​758](autobrr/qui#758)) ([@​s0up4200](https://github.com/s0up4200)) - [`47695fd`](autobrr/qui@47695fd): fix(web): add height constraint to filter sidebar wrapper for proper scrolling ([#​778](autobrr/qui#778)) ([@​s0up4200](https://github.com/s0up4200)) - [`4b3bfea`](autobrr/qui@4b3bfea): fix(web): default torrent format to v1 in creator dialog ([#​723](autobrr/qui#723)) ([@​s0up4200](https://github.com/s0up4200)) - [`2d54b79`](autobrr/qui@2d54b79): fix(web): pin submit button in Services sheet footer ([#​756](autobrr/qui#756)) ([@​s0up4200](https://github.com/s0up4200)) - [`2bcd6a3`](autobrr/qui@2bcd6a3): fix(web): preserve folder collapse state during file tree sync ([#​740](autobrr/qui#740)) ([@​ewenjo](https://github.com/ewenjo)) - [`57f3f1d`](autobrr/qui@57f3f1d): fix(web): sort Peers column by total peers instead of connected ([#​759](autobrr/qui#759)) ([@​s0up4200](https://github.com/s0up4200)) - [`53a8818`](autobrr/qui@53a8818): fix(web): sort Seeds column by total seeds instead of connected ([#​747](autobrr/qui#747)) ([@​s0up4200](https://github.com/s0up4200)) - [`d171915`](autobrr/qui@d171915): fix(web): sort folders before files in torrent file tree ([#​764](autobrr/qui#764)) ([@​s0up4200](https://github.com/s0up4200)) ##### Other Changes - [`172b4aa`](autobrr/qui@172b4aa): chore(assets): replace napster.svg with napster.png for logo update ([@​s0up4200](https://github.com/s0up4200)) - [`dc83102`](autobrr/qui@dc83102): chore(deps): bump the github group with 3 updates ([#​761](autobrr/qui#761)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`75357d3`](autobrr/qui@75357d3): chore: fix napster logo ([@​s0up4200](https://github.com/s0up4200)) - [`206c4b2`](autobrr/qui@206c4b2): refactor(web): extract CrossSeed completion to accordion component ([#​762](autobrr/qui#762)) ([@​s0up4200](https://github.com/s0up4200)) **Full Changelog**: <autobrr/qui@v1.9.1...v1.10.0> #### Docker images - `docker pull ghcr.io/autobrr/qui:v1.10.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:eyJjcmVhdGVkSW5WZXIiOiI0Mi4zOS4xIiwidXBkYXRlZEluVmVyIjoiNDIuMzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=--> Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/2664 Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net> Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>
Summary
Test plan
Closes #730
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.