feat(crossseed): add custom category option for cross-seeds - #907
Conversation
Add a third category mode allowing users to specify a static category name for all cross-seeds. The three modes are now mutually exclusive: - Suffix mode (default): appends .cross to matched torrent's category - Indexer mode: uses indexer name as category - Custom mode: uses exact user-specified category name Custom category mode disables autoTMM (like indexer mode) and uses explicit save paths derived from the matched torrent's location. Frontend uses RadioGroup for clear mutual exclusivity and a creatable MultiSelect dropdown for category name with suggestions from existing qBittorrent categories.
WalkthroughAdds a configurable cross-seed category mode (suffix | indexer | custom), persists new settings in the DB, threads custom-category through models and services (affecting autoTMM logic and decision paths), and updates frontend UI/types with a RadioGroup and custom-category input. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Browser as Client (UI)
participant API as Server (HTTP/API)
participant DB as Database
participant Svc as CrossSeed Service
Note over Browser,API: User selects category mode and optional customCategory
User->>Browser: choose mode + (optional) customCategory
Browser->>API: PATCH /settings { useCustomCategory, customCategory, useCategoryFromIndexer, ... }
API->>DB: UPSERT cross_seed_settings (use_custom_category, custom_category, ...)
DB-->>API: ACK
API-->>Browser: 200 OK
Note over Svc,DB: Later, on cross-seed match
Svc->>DB: SELECT cross_seed_settings for account
DB-->>Svc: (use_custom_category, custom_category, use_category_from_indexer, ...)
Svc->>Svc: determineCrossSeedCategory(mode, custom_category, matchedCategory, indexerName)
Svc->>Svc: shouldEnableAutoTMM(crossCategory, matchedAutoManaged, useCategoryFromIndexer, useCustomCategory, matchedSavePath)
alt autoTMM enabled
Svc->>Svc: proceed with auto-managed save logic
else autoTMM disabled
Svc->>Svc: require explicit save path / manual handling
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
web/src/pages/CrossSeedPage.tsx (1)
2483-2491: Consider using a single-select component for custom category.The
MultiSelectcomponent is repurposed for single-value selection by takingvalues[0]. While this works correctly, a dedicated single-select creatable dropdown component would be more semantically appropriate and might provide better UX.That said, the current implementation functions correctly and the
creatablefeature works as intended. This is more of a future enhancement suggestion.internal/services/crossseed/service.go (1)
2961-3036: Custom-category flag correctly integrated into autoTMM decisionThe extra
useCustomCategoryflag is plumbed fromGetAutomationSettingsintoshouldEnableAutoTMMand the decision logging, so custom mode cleanly participates in the same decision flow as indexer mode. This matches the “custom mode disables autoTMM” requirement and doesn’t add any new calls toGetAutomationSettingsbeyond what was already there.If you ever want to reduce settings lookups, a future (optional) refactor could pass a single
*CrossSeedAutomationSettingsdown into bothdetermineCrossSeedCategoryand this TMM decision instead of reloading inside each helper, but that’s not urgent given this is a single-row settings table.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
web/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (10)
README.mddocs/CROSS_SEEDING.mdinternal/database/migrations/046_add_cross_seed_custom_category.sqlinternal/models/crossseed.gointernal/services/crossseed/auto_tmm_test.gointernal/services/crossseed/service.goweb/package.jsonweb/src/components/ui/radio-group.tsxweb/src/pages/CrossSeedPage.tsxweb/src/types/index.ts
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-12-19T07:24:42.043Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 825
File: internal/models/crossseed.go:68-68
Timestamp: 2025-12-19T07:24:42.043Z
Learning: In the autobrr/qui repository, SQLite migrations should use DEFAULT 0 for boolean column defaults instead of DEFAULT FALSE. This ensures consistency with SQLite typing and avoids potential confusion, even though both are functionally equivalent for SQLite. Apply this rule to all SQL migration files (e.g., migrations/*.sql and any SQL in subdirectories).
Applied to files:
internal/database/migrations/046_add_cross_seed_custom_category.sql
📚 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.go
📚 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/models/crossseed.godocs/CROSS_SEEDING.md
📚 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/models/crossseed.gointernal/services/crossseed/service.gointernal/services/crossseed/auto_tmm_test.go
📚 Learning: 2025-11-28T22:21:20.730Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go:2415-2457
Timestamp: 2025-11-28T22:21:20.730Z
Learning: Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go
Learning: The determineSavePath function intentionally includes a contentLayout string parameter for future/content-layout branching and API consistency. Its presence is by design even if unused in the current body; do not flag as an issue in reviews.
Applied to files:
internal/services/crossseed/service.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/CrossSeedPage.tsx
🧬 Code graph analysis (2)
web/src/components/ui/radio-group.tsx (1)
web/src/lib/utils.ts (1)
cn(10-12)
web/src/pages/CrossSeedPage.tsx (5)
web/src/lib/category-utils.ts (1)
buildCategorySelectOptions(9-33)web/src/components/ui/radio-group.tsx (2)
RadioGroup(42-42)RadioGroupItem(42-42)web/src/components/ui/label.tsx (1)
Label(29-29)web/src/components/ui/tooltip.tsx (3)
Tooltip(184-184)TooltipTrigger(184-184)TooltipContent(184-184)web/src/components/ui/multi-select.tsx (1)
MultiSelect(28-176)
🔇 Additional comments (16)
README.md (1)
432-436: Documentation clearly explains the three category modes.The descriptions accurately reflect the implementation: suffix mode inherits AutoTMM, while indexer and custom modes disable it with explicit save paths.
web/src/components/ui/radio-group.tsx (1)
1-42: Well-structured RadioGroup component following established patterns.The implementation follows the project's Radix UI wrapper conventions with proper forwardRef usage, accessibility-focused styling (focus-visible states, disabled handling), and correct displayName assignment.
docs/CROSS_SEEDING.md (1)
194-228: Comprehensive documentation of category modes and autoTMM behavior.The table clearly communicates the autoTMM inheritance/disabled behavior per mode, and the explanations accurately reflect the implementation logic.
internal/services/crossseed/auto_tmm_test.go (1)
65-75: Good test coverage for the new useCustomCategory parameter.The new test case correctly validates that custom category mode disables autoTMM even when all other conditions would enable it (matchedAutoManaged: true, useCategoryFromIndexer: false).
internal/database/migrations/046_add_cross_seed_custom_category.sql (1)
1-6: Migration follows SQLite conventions correctly.The boolean column uses
DEFAULT 0instead ofDEFAULT FALSE, which is the correct approach for SQLite consistency. Based on learnings, this aligns with the repository's migration standards.internal/models/crossseed.go (2)
62-64: Model fields properly defined with appropriate types and JSON tags.The new
UseCustomCategory(bool) andCustomCategory(string) fields are correctly placed after the existing category-related fields, maintaining logical grouping.
539-545: INSERT statement columns and placeholders are aligned.The INSERT statement has 38 columns and 38 placeholders on line 545—they match. No changes needed.
web/src/types/index.ts (2)
1667-1668: Type definitions correctly added to match backend model.The
useCustomCategory: booleanandcustomCategory: stringfields align with the Go struct definitions ininternal/models/crossseed.go.
1713-1714: Patch interface correctly uses optional modifiers.The optional
?modifiers allow partial updates, consistent with the existing patch pattern used throughout the interface.web/package.json (1)
35-35: LGTM - new Radix UI dependency for RadioGroup component.The addition is consistent with the existing Radix UI primitives pattern used throughout the project, and v1.3.8 is the latest available version.
web/src/pages/CrossSeedPage.tsx (4)
23-23: Type definitions and defaults look good.The new
CategoryModetype, interface extensions, and default values are well-structured and properly typed for the three-way category mode system.Also applies to: 77-78, 102-103, 128-129
1218-1232: Helper functions correctly enforce mutual exclusivity.The
getCategoryModeandsetCategoryModefunctions work together to maintain a consistent three-way exclusive state. The priority order (custom > indexer > suffix) ingetCategoryModeprovides a deterministic fallback if flags are somehow inconsistent.
1208-1215: Category options correctly sourced from all active instances.Using
webhookSourceMetadata(which aggregates categories from all active instances) provides comprehensive suggestions for the custom category dropdown, and the selected array parameter ensures the current value always appears.
2422-2495: RadioGroup UI implementation is well-structured and accessible.The RadioGroup properly implements mutual exclusivity with good accessibility features (associated labels, aria-labels, tooltips), clear descriptions for each mode, and appropriate conditional rendering for the custom category input.
internal/services/crossseed/service.go (2)
7148-7192: shouldEnableAutoTMM: custom categories correctly treated like indexer categoriesExtending
autoTMMDecisionwithUseCustomCategoryand updatingshouldEnableAutoTMMto:
- accept
useCustomCategoryas a parameter, and- require
!useCategoryFromIndexer && !useCustomCategoryto enable autoTMM,ensures both indexer-based and custom-category modes always disable autoTMM, which is exactly what the new mode promises. Path-matching is still computed for observability via
PathsMatch, without altering prior behavior.
7315-7351: determineCrossSeedCategory: custom category precedence and suffix behavior look correctThe new early branch:
if settings != nil && settings.UseCustomCategory && settings.CustomCategory != "" { return settings.CustomCategory, settings.CustomCategory }ensures:
- custom mode has highest precedence over request-level
Category/IndexerName,- the custom category is used as both base and cross category, and
- no
.crosssuffix is ever applied in that mode.This aligns with the “mutually exclusive custom mode using an exact category” design and works cleanly with the downstream
ensureCrossCategory+ save-path logic.
| useCustomCategory: settings.useCustomCategory ?? false, | ||
| customCategory: settings.customCategory ?? "", |
There was a problem hiding this comment.
Add validation for custom category when custom mode is selected.
The state initialization and patch building correctly handle the new fields, but there's no validation in handleSaveGlobal to ensure that customCategory is non-empty when useCustomCategory is true. This could allow saving an invalid configuration.
🔎 Suggested validation to add
Add validation in the handleSaveGlobal function (around line 1024):
const handleSaveGlobal = () => {
if (ignorePatternError) {
setValidationErrors(prev => ({ ...prev, ignorePatterns: ignorePatternError }))
return
}
+
+ if (globalSettings.useCustomCategory && !globalSettings.customCategory.trim()) {
+ setValidationErrors(prev => ({ ...prev, customCategory: "Custom category name is required when custom mode is selected" }))
+ toast.error("Enter a custom category name or select a different category mode")
+ return
+ }
if (validationErrors.ignorePatterns) {
setValidationErrors(prev => ({ ...prev, ignorePatterns: "" }))
}Also applies to: 895-896, 922-923
🤖 Prompt for AI Agents
In web/src/pages/CrossSeedPage.tsx around lines 791-792 (and also the related
initialization at 895-896 and 922-923) the component allows useCustomCategory
true with an empty customCategory; add validation in handleSaveGlobal (around
line 1024) to check if formState.useCustomCategory is true then require
formState.customCategory.trim() to be non-empty; if empty, set an appropriate
user-visible error (or toast) and return early to prevent saving, and ensure the
same check is enforced before building the patch so invalid configs cannot be
submitted.
|
Closes: #904 |
- Replace hardlink/reflink switches with radio group (Regular/Hardlink/Reflink) making mutual exclusivity explicit in the UI - Consolidate mode toggle handlers into single handleModeChange function - Move validation to handleSave for consistent save-on-click behavior - Show base directory and organization fields only when hardlink/reflink selected - Remove redundant reflink alert (info already in radio descriptions) - Fix incorrect tooltips for indexer/custom category modes: autoTMM is disabled in these modes, not inherited from matched torrent
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
web/src/pages/CrossSeedPage.tsx (1)
729-730: Add validation for custom category when custom mode is selected.The state initialization and patch building correctly handle the new fields, but there's no validation in
handleSaveGlobalto ensure thatcustomCategoryis non-empty whenuseCustomCategoryis true. This could allow saving an invalid configuration.🔎 Suggested validation to add
Add validation in the
handleSaveGlobalfunction (around line 962):const handleSaveGlobal = () => { if (ignorePatternError) { setValidationErrors(prev => ({ ...prev, ignorePatterns: ignorePatternError })) return } + + if (globalSettings.useCustomCategory && !globalSettings.customCategory.trim()) { + setValidationErrors(prev => ({ ...prev, customCategory: "Custom category name is required when custom mode is selected" })) + toast.error("Enter a custom category name or select a different category mode") + return + } if (validationErrors.ignorePatterns) { setValidationErrors(prev => ({ ...prev, ignorePatterns: "" })) }Also applies to: 833-834, 860-861
🧹 Nitpick comments (1)
web/src/pages/CrossSeedPage.tsx (1)
238-243: Consider extracting InstanceFormState type to file level.The type is currently defined inside the component. For consistency with other types in the file (like AutomationFormState, GlobalCrossSeedSettings), consider extracting it to the file level.
🔎 Optional refactor
+// Per-instance hardlink/reflink form state +interface InstanceFormState { + useHardlinks: boolean + useReflinks: boolean + hardlinkBaseDir: string + hardlinkDirPreset: "flat" | "by-tracker" | "by-instance" +} + /** Per-instance hardlink/reflink mode settings component */ function HardlinkModeSettings() { const { instances, updateInstance, isUpdating } = useInstances() const [expandedInstances, setExpandedInstances] = useState<string[]>([]) const [dirtyMap, setDirtyMap] = useState<Record<number, boolean>>({}) - type InstanceFormState = { - useHardlinks: boolean - useReflinks: boolean - hardlinkBaseDir: string - hardlinkDirPreset: "flat" | "by-tracker" | "by-instance" - } const [formMap, setFormMap] = useState<Record<number, InstanceFormState>>({})
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
web/src/pages/CrossSeedPage.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 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/CrossSeedPage.tsx
🧬 Code graph analysis (1)
web/src/pages/CrossSeedPage.tsx (6)
web/src/types/index.ts (1)
Instance(19-36)web/src/components/ui/radio-group.tsx (2)
RadioGroup(42-42)RadioGroupItem(42-42)web/src/components/ui/select.tsx (5)
Select(180-180)SelectTrigger(188-188)SelectValue(189-189)SelectContent(181-181)SelectItem(183-183)web/src/lib/category-utils.ts (1)
buildCategorySelectOptions(9-33)web/src/components/ui/tooltip.tsx (3)
Tooltip(184-184)TooltipTrigger(184-184)TooltipContent(184-184)web/src/components/ui/multi-select.tsx (1)
MultiSelect(30-181)
🔇 Additional comments (5)
web/src/pages/CrossSeedPage.tsx (5)
102-103: LGTM! Clean type definition for category modes.The CategoryMode union type provides type safety for the RadioGroup and helper functions.
77-78: LGTM! Interface and defaults are correctly defined.The new fields are properly typed and initialized with sensible defaults.
Also applies to: 128-129
304-319: Excellent validation logic prevents invalid mode configurations.The validation correctly checks for local filesystem access and base directory before enabling hardlink/reflink modes, preventing runtime issues.
1156-1170: LGTM! Category mode helpers are well-structured and type-safe.The
getCategoryModeandsetCategoryModefunctions correctly manage the mutual exclusivity of the three category mode flags. The priority order ingetCategoryMode(custom > indexer > suffix) provides defensive handling if multiple flags are somehow true.
2360-2433: LGTM! Category mode UI is well-implemented with clear UX.The RadioGroup properly exposes all three category modes with helpful tooltips explaining the behavior of each. The conditional MultiSelect for custom category supports both selection from existing categories and creation of new ones.
The use of MultiSelect for a single-value field is slightly unconventional (typically Select or ComboBox is used), but it works well here and the creatable feature provides good UX.
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/autobrr/qui](https://github.com/autobrr/qui) | minor | `v1.11.0` → `v1.12.0` | --- ### Release Notes <details> <summary>autobrr/qui (ghcr.io/autobrr/qui)</summary> ### [`v1.12.0`](https://github.com/autobrr/qui/releases/tag/v1.12.0) [Compare Source](autobrr/qui@v1.11.0...v1.12.0) #### Changelog ##### New Features - [`202e950`](autobrr/qui@202e950): feat(automations): Add `free_space` condition ([#​1061](autobrr/qui#1061)) ([@​Barcode-eng](https://github.com/Barcode-eng)) - [`3b106d6`](autobrr/qui@3b106d6): feat(automations): make conditions optional for non-delete rules and add drag reorder ([#​939](autobrr/qui#939)) ([@​s0up4200](https://github.com/s0up4200)) - [`0684d75`](autobrr/qui@0684d75): feat(config): Add QUI\_\_OIDC\_CLIENT\_SECRET\_FILE env var ([#​841](autobrr/qui#841)) ([@​PedDavid](https://github.com/PedDavid)) - [`dac0173`](autobrr/qui@dac0173): feat(config): allow disabling tracker icon fetching ([#​823](autobrr/qui#823)) ([@​s0up4200](https://github.com/s0up4200)) - [`dc10bad`](autobrr/qui@dc10bad): feat(crossseed): add cancel run and opt-in errored torrent recovery ([#​918](autobrr/qui#918)) ([@​s0up4200](https://github.com/s0up4200)) - [`cd1fcc9`](autobrr/qui@cd1fcc9): feat(crossseed): add custom category option for cross-seeds ([#​907](autobrr/qui#907)) ([@​s0up4200](https://github.com/s0up4200)) - [`d189fe9`](autobrr/qui@d189fe9): feat(crossseed): add indexerName to webhook apply + fix category mode defaults ([#​916](autobrr/qui#916)) ([@​s0up4200](https://github.com/s0up4200)) - [`03a147e`](autobrr/qui@03a147e): feat(crossseed): add option to skip recheck-required matches ([#​825](autobrr/qui#825)) ([@​s0up4200](https://github.com/s0up4200)) - [`edae00a`](autobrr/qui@edae00a): feat(crossseed): add optional hardlink mode for cross-seeding ([#​849](autobrr/qui#849)) ([@​s0up4200](https://github.com/s0up4200)) - [`0938436`](autobrr/qui@0938436): feat(crossseed): add source aliasing for WEB/WEB-DL/WEBRip precheck matching ([#​874](autobrr/qui#874)) ([@​s0up4200](https://github.com/s0up4200)) - [`65f6129`](autobrr/qui@65f6129): feat(crossseed): show failure reasons, prune runs, and add cache cleanup ([#​923](autobrr/qui#923)) ([@​s0up4200](https://github.com/s0up4200)) - [`e10fba8`](autobrr/qui@e10fba8): feat(details): torrent details panel improvements ([#​884](autobrr/qui#884)) ([@​s0up4200](https://github.com/s0up4200)) - [`6921140`](autobrr/qui@6921140): feat(docs): add Docusaurus documentation site ([@​s0up4200](https://github.com/s0up4200)) - [`6a5a66c`](autobrr/qui@6a5a66c): feat(docs): add Icon and webUI variables to the Unraid install guide ([#​942](autobrr/qui#942)) ([@​BaukeZwart](https://github.com/BaukeZwart)) - [`281fce7`](autobrr/qui@281fce7): feat(docs): add local search plugin ([#​1076](autobrr/qui#1076)) ([@​s0up4200](https://github.com/s0up4200)) - [`566de08`](autobrr/qui@566de08): feat(docs): add qui logo, update readme, remove v4 flag ([@​s0up4200](https://github.com/s0up4200)) - [`b83ac5a`](autobrr/qui@b83ac5a): feat(docs): apply minimal.css theme to Docusaurus ([@​s0up4200](https://github.com/s0up4200)) - [`fe6a6df`](autobrr/qui@fe6a6df): feat(docs): improve documentation pages and add support page ([@​s0up4200](https://github.com/s0up4200)) - [`62a7ad5`](autobrr/qui@62a7ad5): feat(docs): use qui favicon ([@​s0up4200](https://github.com/s0up4200)) - [`5d124c0`](autobrr/qui@5d124c0): feat(orphan): add auto cleanup mode ([#​897](autobrr/qui#897)) ([@​s0up4200](https://github.com/s0up4200)) - [`3172ad9`](autobrr/qui@3172ad9): feat(settings): add log settings + live log stream ([#​876](autobrr/qui#876)) ([@​s0up4200](https://github.com/s0up4200)) - [`3c1b34b`](autobrr/qui@3c1b34b): feat(torrents): add "torrent introuvable" to unregistered status ([#​836](autobrr/qui#836)) ([@​kephasdev](https://github.com/kephasdev)) - [`afe4d39`](autobrr/qui@afe4d39): feat(torrents): add tracker URL editing for single torrents ([#​848](autobrr/qui#848)) ([@​s0up4200](https://github.com/s0up4200)) - [`76dedd7`](autobrr/qui@76dedd7): feat(torrents): update GeneralTabHorizontal to display limits and improve layout ([#​1078](autobrr/qui#1078)) ([@​martylukyy](https://github.com/martylukyy)) - [`6831c24`](autobrr/qui@6831c24): feat(ui): unify payment options into single dialog ([@​s0up4200](https://github.com/s0up4200)) - [`4dcdf7f`](autobrr/qui@4dcdf7f): feat(web): add local file access indicator to instance cards ([#​911](autobrr/qui#911)) ([@​s0up4200](https://github.com/s0up4200)) - [`a560e5e`](autobrr/qui@a560e5e): feat(web): compact torrent details panel ([#​833](autobrr/qui#833)) ([@​martylukyy](https://github.com/martylukyy)) - [`557e7bd`](autobrr/qui@557e7bd): feat: add issue/discussion template ([#​945](autobrr/qui#945)) ([@​s0up4200](https://github.com/s0up4200)) - [`8b93719`](autobrr/qui@8b93719): feat: add workflow automation system with category actions, orphan scanner, and hardlink detection ([#​818](autobrr/qui#818)) ([@​s0up4200](https://github.com/s0up4200)) ##### Bug Fixes - [`b85ad6b`](autobrr/qui@b85ad6b): fix(automations): allow delete rules to match incomplete torrents ([#​926](autobrr/qui#926)) ([@​s0up4200](https://github.com/s0up4200)) - [`ae06200`](autobrr/qui@ae06200): fix(automations): make tags field condition operators tag-aware ([#​908](autobrr/qui#908)) ([@​s0up4200](https://github.com/s0up4200)) - [`ace0101`](autobrr/qui@ace0101): fix(crossseed): detect folder mismatch for bare file to folder cross-seeds ([#​846](autobrr/qui#846)) ([@​s0up4200](https://github.com/s0up4200)) - [`1cc1243`](autobrr/qui@1cc1243): fix(crossseed): enforce resolution and language matching with sensible defaults ([#​855](autobrr/qui#855)) ([@​s0up4200](https://github.com/s0up4200)) - [`cefb9cd`](autobrr/qui@cefb9cd): fix(crossseed): execute external program reliably after injection ([#​1083](autobrr/qui#1083)) ([@​s0up4200](https://github.com/s0up4200)) - [`867e2da`](autobrr/qui@867e2da): fix(crossseed): improve matching with size validation and relaxed audio checks ([#​845](autobrr/qui#845)) ([@​s0up4200](https://github.com/s0up4200)) - [`4b5079b`](autobrr/qui@4b5079b): fix(crossseed): persist custom category settings in PATCH handler ([#​913](autobrr/qui#913)) ([@​s0up4200](https://github.com/s0up4200)) - [`cfbbc1f`](autobrr/qui@cfbbc1f): fix(crossseed): prevent season packs matching episodes ([#​854](autobrr/qui#854)) ([@​s0up4200](https://github.com/s0up4200)) - [`c7c1706`](autobrr/qui@c7c1706): fix(crossseed): reconcile interrupted runs on startup ([#​1084](autobrr/qui#1084)) ([@​s0up4200](https://github.com/s0up4200)) - [`7d633bd`](autobrr/qui@7d633bd): fix(crossseed): use ContentPath for manually-managed single-file torrents ([#​832](autobrr/qui#832)) ([@​s0up4200](https://github.com/s0up4200)) - [`d5db761`](autobrr/qui@d5db761): fix(database): include arr\_instances in string pool cleanup + remove auto-recovery ([#​898](autobrr/qui#898)) ([@​s0up4200](https://github.com/s0up4200)) - [`c73ec6f`](autobrr/qui@c73ec6f): fix(database): prevent race between stmt cache access and db close ([#​840](autobrr/qui#840)) ([@​s0up4200](https://github.com/s0up4200)) - [`a40b872`](autobrr/qui@a40b872): fix(db): drop legacy hardlink columns from cross\_seed\_settings ([#​912](autobrr/qui#912)) ([@​s0up4200](https://github.com/s0up4200)) - [`e400af3`](autobrr/qui@e400af3): fix(db): recover wedged SQLite writer + stop cross-seed tight loop ([#​890](autobrr/qui#890)) ([@​s0up4200](https://github.com/s0up4200)) - [`90e15b4`](autobrr/qui@90e15b4): fix(deps): update rls to recognize IP as iPlayer ([#​922](autobrr/qui#922)) ([@​s0up4200](https://github.com/s0up4200)) - [`8e81b9f`](autobrr/qui@8e81b9f): fix(proxy): honor TLSSkipVerify for proxied requests ([#​1051](autobrr/qui#1051)) ([@​s0up4200](https://github.com/s0up4200)) - [`eb2bee0`](autobrr/qui@eb2bee0): fix(security): redact sensitive URL parameters in logs ([#​853](autobrr/qui#853)) ([@​s0up4200](https://github.com/s0up4200)) - [`40982bc`](autobrr/qui@40982bc): fix(themes): prevent reset on license errors, improve switch performance ([#​844](autobrr/qui#844)) ([@​s0up4200](https://github.com/s0up4200)) - [`a8a32f7`](autobrr/qui@a8a32f7): fix(ui): incomplete torrents aren't "Completed: 1969-12-31" ([#​851](autobrr/qui#851)) ([@​finevan](https://github.com/finevan)) - [`5908bba`](autobrr/qui@5908bba): fix(ui): preserve category collapse state when toggling incognito mode ([#​834](autobrr/qui#834)) ([@​jabloink](https://github.com/jabloink)) - [`25c847e`](autobrr/qui@25c847e): fix(ui): torrents with no creation metadata don't display 1969 ([#​873](autobrr/qui#873)) ([@​finevan](https://github.com/finevan)) - [`6403b6a`](autobrr/qui@6403b6a): fix(web): column filter status now matches all states in category ([#​880](autobrr/qui#880)) ([@​s0up4200](https://github.com/s0up4200)) - [`eafc4e7`](autobrr/qui@eafc4e7): fix(web): make delete cross-seed check rely on content\_path matches ([#​1080](autobrr/qui#1080)) ([@​s0up4200](https://github.com/s0up4200)) - [`d57c749`](autobrr/qui@d57c749): fix(web): only show selection checkbox on normal view ([#​830](autobrr/qui#830)) ([@​jabloink](https://github.com/jabloink)) - [`60338f6`](autobrr/qui@60338f6): fix(web): optimize TorrentDetailsPanel for mobile view and make tabs scrollable horizontally ([#​1066](autobrr/qui#1066)) ([@​martylukyy](https://github.com/martylukyy)) - [`aedab87`](autobrr/qui@aedab87): fix(web): speed limit input reformatting during typing ([#​881](autobrr/qui#881)) ([@​s0up4200](https://github.com/s0up4200)) - [`df7f3e0`](autobrr/qui@df7f3e0): fix(web): truncate file progress percentage instead of rounding ([#​919](autobrr/qui#919)) ([@​s0up4200](https://github.com/s0up4200)) - [`2fadd01`](autobrr/qui@2fadd01): fix(web): update eslint config for flat config compatibility ([#​879](autobrr/qui#879)) ([@​s0up4200](https://github.com/s0up4200)) - [`721cedd`](autobrr/qui@721cedd): fix(web): use fixed heights for mobile torrent cards ([#​812](autobrr/qui#812)) ([@​jabloink](https://github.com/jabloink)) - [`a7db605`](autobrr/qui@a7db605): fix: remove pnpm-workspace.yaml breaking CI ([@​s0up4200](https://github.com/s0up4200)) - [`c0ddc0a`](autobrr/qui@c0ddc0a): fix: use prefix matching for allowed bash commands ([@​s0up4200](https://github.com/s0up4200)) ##### Other Changes - [`fff52ce`](autobrr/qui@fff52ce): chore(ci): disable reviewer ([@​s0up4200](https://github.com/s0up4200)) - [`7ef2a38`](autobrr/qui@7ef2a38): chore(ci): fix automated triage and deduplication workflows ([#​1057](autobrr/qui#1057)) ([@​s0up4200](https://github.com/s0up4200)) - [`d84910b`](autobrr/qui@d84910b): chore(docs): move Tailwind to documentation workspace only ([@​s0up4200](https://github.com/s0up4200)) - [`37ebe05`](autobrr/qui@37ebe05): chore(docs): move netlify.toml to documentation directory ([@​s0up4200](https://github.com/s0up4200)) - [`e25de38`](autobrr/qui@e25de38): chore(docs): remove disclaimer ([@​s0up4200](https://github.com/s0up4200)) - [`c59b809`](autobrr/qui@c59b809): chore(docs): update support sections ([#​1063](autobrr/qui#1063)) ([@​s0up4200](https://github.com/s0up4200)) - [`b723523`](autobrr/qui@b723523): chore(tests): remove dead tests and optimize slow test cases ([#​842](autobrr/qui#842)) ([@​s0up4200](https://github.com/s0up4200)) - [`662a1c6`](autobrr/qui@662a1c6): chore(workflows): update runners from 4vcpu to 2vcpu for all jobs ([#​859](autobrr/qui#859)) ([@​s0up4200](https://github.com/s0up4200)) - [`46f2a1c`](autobrr/qui@46f2a1c): chore: clean up repo root by moving Docker, scripts, and community docs ([#​1054](autobrr/qui#1054)) ([@​s0up4200](https://github.com/s0up4200)) - [`2f27c0d`](autobrr/qui@2f27c0d): chore: remove old issue templates ([@​s0up4200](https://github.com/s0up4200)) - [`04f361a`](autobrr/qui@04f361a): ci(triage): add labeling for feature-requests-ideas discussions ([@​s0up4200](https://github.com/s0up4200)) - [`f249c69`](autobrr/qui@f249c69): ci(triage): remove needs-triage label after applying labels ([@​s0up4200](https://github.com/s0up4200)) - [`bdda1de`](autobrr/qui@bdda1de): ci(workflows): add self-dispatch workaround for discussion events ([@​s0up4200](https://github.com/s0up4200)) - [`a9732a2`](autobrr/qui@a9732a2): ci(workflows): increase max-turns to 25 for Claude workflows ([@​s0up4200](https://github.com/s0up4200)) - [`d7d830d`](autobrr/qui@d7d830d): docs(README): add Buy Me a Coffee link ([#​863](autobrr/qui#863)) ([@​s0up4200](https://github.com/s0up4200)) - [`266d92e`](autobrr/qui@266d92e): docs(readme): Clarify ignore pattern ([#​878](autobrr/qui#878)) ([@​quorn23](https://github.com/quorn23)) - [`9586084`](autobrr/qui@9586084): docs(readme): add banner linking to stable docs ([#​925](autobrr/qui#925)) ([@​s0up4200](https://github.com/s0up4200)) - [`e36a621`](autobrr/qui@e36a621): docs(readme): use markdown link for Polar URL ([@​s0up4200](https://github.com/s0up4200)) - [`9394676`](autobrr/qui@9394676): docs: add frontmatter titles and descriptions, remove marketing language ([@​s0up4200](https://github.com/s0up4200)) - [`ba9d45e`](autobrr/qui@ba9d45e): docs: add local filesystem access snippet and swizzle Details component ([@​s0up4200](https://github.com/s0up4200)) - [`4329edd`](autobrr/qui@4329edd): docs: disclaimer about unreleased features ([#​943](autobrr/qui#943)) ([@​s0up4200](https://github.com/s0up4200)) - [`735d065`](autobrr/qui@735d065): docs: improve external programs, orphan scan, reverse proxy, tracker icons documentation ([@​s0up4200](https://github.com/s0up4200)) - [`78faef2`](autobrr/qui@78faef2): docs: remove premature tip and fix stat command ([@​s0up4200](https://github.com/s0up4200)) - [`eaad3bf`](autobrr/qui@eaad3bf): docs: update social card image in Docusaurus configuration ([@​s0up4200](https://github.com/s0up4200)) - [`02a68e5`](autobrr/qui@02a68e5): refactor(crossseed): hardcode ignore patterns for file matching ([#​915](autobrr/qui#915)) ([@​s0up4200](https://github.com/s0up4200)) **Full Changelog**: <autobrr/qui@v1.11.0...v1.12.0> #### Docker images - `docker pull ghcr.io/autobrr/qui:v1.12.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/3060 Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net> Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>
Summary
.crossto matched torrent's categoryTest plan
Summary by CodeRabbit
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.