Skip to content

feat(crossseed): add custom category option for cross-seeds - #907

Merged
s0up4200 merged 4 commits into
mainfrom
feat/cross-seed-custom-category
Dec 31, 2025
Merged

feat(crossseed): add custom category option for cross-seeds#907
s0up4200 merged 4 commits into
mainfrom
feat/cross-seed-custom-category

Conversation

@s0up4200

@s0up4200 s0up4200 commented Dec 31, 2025

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a third category mode for cross-seeds: custom category
  • Three mutually exclusive modes now available via RadioGroup UI:
    • 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) for safety
  • Frontend includes creatable dropdown with suggestions from existing qBittorrent categories

Test plan

  • Verify RadioGroup displays all three category modes
  • Verify selecting custom mode shows the category input dropdown
  • Verify custom category persists after save/reload
  • Verify cross-seeds use exact custom category without any suffix
  • Verify autoTMM is disabled for custom category mode
  • Verify switching between modes updates settings correctly

Summary by CodeRabbit

  • New Features

    • Added a single category-mode selector (suffix, indexer, custom) with updated UI controls and radio-group input.
    • Added custom category option allowing a static category name (no suffix) and creation of custom categories in the UI.
    • Settings now persist and surface the selected category mode.
  • Documentation

    • Updated cross-seeding docs to explain the three modes, their save-path behavior, and mode-dependent AutoTMM rules.

✏️ Tip: You can customize this high-level summary in your review settings.

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.
@coderabbitai

coderabbitai Bot commented Dec 31, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds 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

Cohort / File(s) Change Summary
Documentation
README.md, docs/CROSS_SEEDING.md
Replace prior comparison with a three-mode category system (suffix, indexer, custom); clarify per-mode autoTMM behavior, save-path resolution, piece-boundary safety, and related notes.
DB Migration
internal/database/migrations/046_add_cross_seed_custom_category.sql
Add use_custom_category (BOOLEAN NOT NULL DEFAULT false) and custom_category (TEXT DEFAULT '') to cross_seed_settings.
Backend Models
internal/models/crossseed.go
Add UseCustomCategory (bool) and CustomCategory (string) to CrossSeedAutomationSettings; read/write/default handling added.
Backend Services & Tests
internal/services/crossseed/service.go, internal/services/crossseed/auto_tmm_test.go
Thread useCustomCategory through category resolution and shouldEnableAutoTMM (signature and callers updated); prioritize custom category when set; update tests and decision logging.
Frontend Types
web/src/types/index.ts
Add useCustomCategory and customCategory to CrossSeedAutomationSettings and optional fields to the patch type.
Frontend UI & Components
web/src/pages/CrossSeedPage.tsx, web/src/components/ui/radio-group.tsx
Introduce CategoryMode (`"suffix"
Web Dependencies
web/package.json
Add @radix-ui/react-radio-group (^1.3.8).

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement, cross-seed, web

Poem

🐇 I hopped through code and database, too,

Three modes to pick — suffix, indexer, or new.
A radio click, a custom name tucked in,
DB writes done — let cross-seeds begin! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately and concisely describes the main feature: adding a custom category option for cross-seeds, which aligns with the substantial changes across backend models, database migrations, and frontend UI components.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/cross-seed-custom-category

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 MultiSelect component is repurposed for single-value selection by taking values[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 creatable feature 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 decision

The extra useCustomCategory flag is plumbed from GetAutomationSettings into shouldEnableAutoTMM and 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 to GetAutomationSettings beyond what was already there.

If you ever want to reduce settings lookups, a future (optional) refactor could pass a single *CrossSeedAutomationSettings down into both determineCrossSeedCategory and 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3172ad9 and ee5c4f4.

⛔ Files ignored due to path filters (1)
  • web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • README.md
  • docs/CROSS_SEEDING.md
  • internal/database/migrations/046_add_cross_seed_custom_category.sql
  • internal/models/crossseed.go
  • internal/services/crossseed/auto_tmm_test.go
  • internal/services/crossseed/service.go
  • web/package.json
  • web/src/components/ui/radio-group.tsx
  • web/src/pages/CrossSeedPage.tsx
  • web/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.go
  • docs/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.go
  • internal/services/crossseed/service.go
  • internal/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 0 instead of DEFAULT 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) and CustomCategory (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: boolean and customCategory: string fields align with the Go struct definitions in internal/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 CategoryMode type, 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 getCategoryMode and setCategoryMode functions work together to maintain a consistent three-way exclusive state. The priority order (custom > indexer > suffix) in getCategoryMode provides 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 categories

Extending autoTMMDecision with UseCustomCategory and updating shouldEnableAutoTMM to:

  • accept useCustomCategory as a parameter, and
  • require !useCategoryFromIndexer && !useCustomCategory to 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 correct

The 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 .cross suffix 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.

Comment on lines +791 to +792
useCustomCategory: settings.useCustomCategory ?? false,
customCategory: settings.customCategory ?? "",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

@onedr0p

onedr0p commented Dec 31, 2025

Copy link
Copy Markdown
Contributor

Closes: #904

@s0up4200 s0up4200 added this to the v1.12.0 milestone Dec 31, 2025
@s0up4200 s0up4200 linked an issue Dec 31, 2025 that may be closed by this pull request
- 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 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 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9a5d1e6 and 478f83b.

📒 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 getCategoryMode and setCategoryMode functions correctly manage the mutual exclusivity of the three category mode flags. The priority order in getCategoryMode (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.

@s0up4200 s0up4200 added enhancement New feature or request cross-seed labels Dec 31, 2025
@s0up4200
s0up4200 merged commit cd1fcc9 into main Dec 31, 2025
12 checks passed
@s0up4200
s0up4200 deleted the feat/cross-seed-custom-category branch December 31, 2025 21:01
alexlebens pushed a commit to alexlebens/infrastructure that referenced this pull request Jan 4, 2026
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 ([#&#8203;1061](autobrr/qui#1061)) ([@&#8203;Barcode-eng](https://github.com/Barcode-eng))
- [`3b106d6`](autobrr/qui@3b106d6): feat(automations): make conditions optional for non-delete rules and add drag reorder ([#&#8203;939](autobrr/qui#939)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`0684d75`](autobrr/qui@0684d75): feat(config): Add QUI\_\_OIDC\_CLIENT\_SECRET\_FILE env var ([#&#8203;841](autobrr/qui#841)) ([@&#8203;PedDavid](https://github.com/PedDavid))
- [`dac0173`](autobrr/qui@dac0173): feat(config): allow disabling tracker icon fetching ([#&#8203;823](autobrr/qui#823)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`dc10bad`](autobrr/qui@dc10bad): feat(crossseed): add cancel run and opt-in errored torrent recovery ([#&#8203;918](autobrr/qui#918)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`cd1fcc9`](autobrr/qui@cd1fcc9): feat(crossseed): add custom category option for cross-seeds ([#&#8203;907](autobrr/qui#907)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d189fe9`](autobrr/qui@d189fe9): feat(crossseed): add indexerName to webhook apply + fix category mode defaults ([#&#8203;916](autobrr/qui#916)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`03a147e`](autobrr/qui@03a147e): feat(crossseed): add option to skip recheck-required matches ([#&#8203;825](autobrr/qui#825)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`edae00a`](autobrr/qui@edae00a): feat(crossseed): add optional hardlink mode for cross-seeding ([#&#8203;849](autobrr/qui#849)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`0938436`](autobrr/qui@0938436): feat(crossseed): add source aliasing for WEB/WEB-DL/WEBRip precheck matching ([#&#8203;874](autobrr/qui#874)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`65f6129`](autobrr/qui@65f6129): feat(crossseed): show failure reasons, prune runs, and add cache cleanup ([#&#8203;923](autobrr/qui#923)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e10fba8`](autobrr/qui@e10fba8): feat(details): torrent details panel improvements ([#&#8203;884](autobrr/qui#884)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`6921140`](autobrr/qui@6921140): feat(docs): add Docusaurus documentation site ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`6a5a66c`](autobrr/qui@6a5a66c): feat(docs): add Icon and webUI variables to the Unraid install guide ([#&#8203;942](autobrr/qui#942)) ([@&#8203;BaukeZwart](https://github.com/BaukeZwart))
- [`281fce7`](autobrr/qui@281fce7): feat(docs): add local search plugin ([#&#8203;1076](autobrr/qui#1076)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`566de08`](autobrr/qui@566de08): feat(docs): add qui logo, update readme, remove v4 flag ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b83ac5a`](autobrr/qui@b83ac5a): feat(docs): apply minimal.css theme to Docusaurus ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`fe6a6df`](autobrr/qui@fe6a6df): feat(docs): improve documentation pages and add support page ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`62a7ad5`](autobrr/qui@62a7ad5): feat(docs): use qui favicon ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5d124c0`](autobrr/qui@5d124c0): feat(orphan): add auto cleanup mode ([#&#8203;897](autobrr/qui#897)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`3172ad9`](autobrr/qui@3172ad9): feat(settings): add log settings + live log stream ([#&#8203;876](autobrr/qui#876)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`3c1b34b`](autobrr/qui@3c1b34b): feat(torrents): add "torrent introuvable" to unregistered status ([#&#8203;836](autobrr/qui#836)) ([@&#8203;kephasdev](https://github.com/kephasdev))
- [`afe4d39`](autobrr/qui@afe4d39): feat(torrents): add tracker URL editing for single torrents ([#&#8203;848](autobrr/qui#848)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`76dedd7`](autobrr/qui@76dedd7): feat(torrents): update GeneralTabHorizontal to display limits and improve layout ([#&#8203;1078](autobrr/qui#1078)) ([@&#8203;martylukyy](https://github.com/martylukyy))
- [`6831c24`](autobrr/qui@6831c24): feat(ui): unify payment options into single dialog ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`4dcdf7f`](autobrr/qui@4dcdf7f): feat(web): add local file access indicator to instance cards ([#&#8203;911](autobrr/qui#911)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a560e5e`](autobrr/qui@a560e5e): feat(web): compact torrent details panel ([#&#8203;833](autobrr/qui#833)) ([@&#8203;martylukyy](https://github.com/martylukyy))
- [`557e7bd`](autobrr/qui@557e7bd): feat: add issue/discussion template ([#&#8203;945](autobrr/qui#945)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8b93719`](autobrr/qui@8b93719): feat: add workflow automation system with category actions, orphan scanner, and hardlink detection ([#&#8203;818](autobrr/qui#818)) ([@&#8203;s0up4200](https://github.com/s0up4200))

##### Bug Fixes

- [`b85ad6b`](autobrr/qui@b85ad6b): fix(automations): allow delete rules to match incomplete torrents ([#&#8203;926](autobrr/qui#926)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`ae06200`](autobrr/qui@ae06200): fix(automations): make tags field condition operators tag-aware ([#&#8203;908](autobrr/qui#908)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`ace0101`](autobrr/qui@ace0101): fix(crossseed): detect folder mismatch for bare file to folder cross-seeds ([#&#8203;846](autobrr/qui#846)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`1cc1243`](autobrr/qui@1cc1243): fix(crossseed): enforce resolution and language matching with sensible defaults ([#&#8203;855](autobrr/qui#855)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`cefb9cd`](autobrr/qui@cefb9cd): fix(crossseed): execute external program reliably after injection ([#&#8203;1083](autobrr/qui#1083)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`867e2da`](autobrr/qui@867e2da): fix(crossseed): improve matching with size validation and relaxed audio checks ([#&#8203;845](autobrr/qui#845)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`4b5079b`](autobrr/qui@4b5079b): fix(crossseed): persist custom category settings in PATCH handler ([#&#8203;913](autobrr/qui#913)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`cfbbc1f`](autobrr/qui@cfbbc1f): fix(crossseed): prevent season packs matching episodes ([#&#8203;854](autobrr/qui#854)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c7c1706`](autobrr/qui@c7c1706): fix(crossseed): reconcile interrupted runs on startup ([#&#8203;1084](autobrr/qui#1084)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`7d633bd`](autobrr/qui@7d633bd): fix(crossseed): use ContentPath for manually-managed single-file torrents ([#&#8203;832](autobrr/qui#832)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d5db761`](autobrr/qui@d5db761): fix(database): include arr\_instances in string pool cleanup + remove auto-recovery ([#&#8203;898](autobrr/qui#898)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c73ec6f`](autobrr/qui@c73ec6f): fix(database): prevent race between stmt cache access and db close ([#&#8203;840](autobrr/qui#840)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a40b872`](autobrr/qui@a40b872): fix(db): drop legacy hardlink columns from cross\_seed\_settings ([#&#8203;912](autobrr/qui#912)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e400af3`](autobrr/qui@e400af3): fix(db): recover wedged SQLite writer + stop cross-seed tight loop ([#&#8203;890](autobrr/qui#890)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`90e15b4`](autobrr/qui@90e15b4): fix(deps): update rls to recognize IP as iPlayer ([#&#8203;922](autobrr/qui#922)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8e81b9f`](autobrr/qui@8e81b9f): fix(proxy): honor TLSSkipVerify for proxied requests ([#&#8203;1051](autobrr/qui#1051)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`eb2bee0`](autobrr/qui@eb2bee0): fix(security): redact sensitive URL parameters in logs ([#&#8203;853](autobrr/qui#853)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`40982bc`](autobrr/qui@40982bc): fix(themes): prevent reset on license errors, improve switch performance ([#&#8203;844](autobrr/qui#844)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a8a32f7`](autobrr/qui@a8a32f7): fix(ui): incomplete torrents aren't "Completed: 1969-12-31" ([#&#8203;851](autobrr/qui#851)) ([@&#8203;finevan](https://github.com/finevan))
- [`5908bba`](autobrr/qui@5908bba): fix(ui): preserve category collapse state when toggling incognito mode ([#&#8203;834](autobrr/qui#834)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`25c847e`](autobrr/qui@25c847e): fix(ui): torrents with no creation metadata don't display 1969 ([#&#8203;873](autobrr/qui#873)) ([@&#8203;finevan](https://github.com/finevan))
- [`6403b6a`](autobrr/qui@6403b6a): fix(web): column filter status now matches all states in category ([#&#8203;880](autobrr/qui#880)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`eafc4e7`](autobrr/qui@eafc4e7): fix(web): make delete cross-seed check rely on content\_path matches ([#&#8203;1080](autobrr/qui#1080)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d57c749`](autobrr/qui@d57c749): fix(web): only show selection checkbox on normal view ([#&#8203;830](autobrr/qui#830)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`60338f6`](autobrr/qui@60338f6): fix(web): optimize TorrentDetailsPanel for mobile view and make tabs scrollable horizontally ([#&#8203;1066](autobrr/qui#1066)) ([@&#8203;martylukyy](https://github.com/martylukyy))
- [`aedab87`](autobrr/qui@aedab87): fix(web): speed limit input reformatting during typing ([#&#8203;881](autobrr/qui#881)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`df7f3e0`](autobrr/qui@df7f3e0): fix(web): truncate file progress percentage instead of rounding ([#&#8203;919](autobrr/qui#919)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`2fadd01`](autobrr/qui@2fadd01): fix(web): update eslint config for flat config compatibility ([#&#8203;879](autobrr/qui#879)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`721cedd`](autobrr/qui@721cedd): fix(web): use fixed heights for mobile torrent cards ([#&#8203;812](autobrr/qui#812)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`a7db605`](autobrr/qui@a7db605): fix: remove pnpm-workspace.yaml breaking CI ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c0ddc0a`](autobrr/qui@c0ddc0a): fix: use prefix matching for allowed bash commands ([@&#8203;s0up4200](https://github.com/s0up4200))

##### Other Changes

- [`fff52ce`](autobrr/qui@fff52ce): chore(ci): disable reviewer ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`7ef2a38`](autobrr/qui@7ef2a38): chore(ci): fix automated triage and deduplication workflows ([#&#8203;1057](autobrr/qui#1057)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d84910b`](autobrr/qui@d84910b): chore(docs): move Tailwind to documentation workspace only ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`37ebe05`](autobrr/qui@37ebe05): chore(docs): move netlify.toml to documentation directory ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e25de38`](autobrr/qui@e25de38): chore(docs): remove disclaimer ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c59b809`](autobrr/qui@c59b809): chore(docs): update support sections ([#&#8203;1063](autobrr/qui#1063)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b723523`](autobrr/qui@b723523): chore(tests): remove dead tests and optimize slow test cases ([#&#8203;842](autobrr/qui#842)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`662a1c6`](autobrr/qui@662a1c6): chore(workflows): update runners from 4vcpu to 2vcpu for all jobs ([#&#8203;859](autobrr/qui#859)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`46f2a1c`](autobrr/qui@46f2a1c): chore: clean up repo root by moving Docker, scripts, and community docs ([#&#8203;1054](autobrr/qui#1054)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`2f27c0d`](autobrr/qui@2f27c0d): chore: remove old issue templates ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`04f361a`](autobrr/qui@04f361a): ci(triage): add labeling for feature-requests-ideas discussions ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`f249c69`](autobrr/qui@f249c69): ci(triage): remove needs-triage label after applying labels ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`bdda1de`](autobrr/qui@bdda1de): ci(workflows): add self-dispatch workaround for discussion events ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a9732a2`](autobrr/qui@a9732a2): ci(workflows): increase max-turns to 25 for Claude workflows ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d7d830d`](autobrr/qui@d7d830d): docs(README): add Buy Me a Coffee link ([#&#8203;863](autobrr/qui#863)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`266d92e`](autobrr/qui@266d92e): docs(readme): Clarify ignore pattern ([#&#8203;878](autobrr/qui#878)) ([@&#8203;quorn23](https://github.com/quorn23))
- [`9586084`](autobrr/qui@9586084): docs(readme): add banner linking to stable docs ([#&#8203;925](autobrr/qui#925)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e36a621`](autobrr/qui@e36a621): docs(readme): use markdown link for Polar URL ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`9394676`](autobrr/qui@9394676): docs: add frontmatter titles and descriptions, remove marketing language ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`ba9d45e`](autobrr/qui@ba9d45e): docs: add local filesystem access snippet and swizzle Details component ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`4329edd`](autobrr/qui@4329edd): docs: disclaimer about unreleased features ([#&#8203;943](autobrr/qui#943)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`735d065`](autobrr/qui@735d065): docs: improve external programs, orphan scan, reverse proxy, tracker icons documentation ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`78faef2`](autobrr/qui@78faef2): docs: remove premature tip and fix stat command ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`eaad3bf`](autobrr/qui@eaad3bf): docs: update social card image in Docusaurus configuration ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`02a68e5`](autobrr/qui@02a68e5): refactor(crossseed): hardcode ignore patterns for file matching ([#&#8203;915](autobrr/qui#915)) ([@&#8203;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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow using a static cross seed category when using hardlink/reflink

2 participants