Skip to content

custom_folders: add category / not_category filter primitives#262

Open
jakeyr wants to merge 1 commit into
sirrobot01:betafrom
jakeyr:feature/filter-by-category
Open

custom_folders: add category / not_category filter primitives#262
jakeyr wants to merge 1 commit into
sirrobot01:betafrom
jakeyr:feature/filter-by-category

Conversation

@jakeyr
Copy link
Copy Markdown
Contributor

@jakeyr jakeyr commented Apr 26, 2026

Summary

Adds two filter primitives — category and not_category — to the Virtual Folders feature, letting a folder scope to torrents added by a specific *arr without any out-of-band metadata.

The qBit category set by the *arr at torrent-add time is already a hot field on IndexEntry (populated in pkg/server/qbit/http.go). This change just exposes it to the existing virtual-folder filter chain.

Why

A common ask is "show me only the torrents Sonarr added" or "everything Radarr added except 4K" as a virtual folder. The metadata is already there; the filter language just couldn't reference it.

With this PR, configs like the following work:

"custom_folders": {
  "sonarr":   { "filters": { "category": "tv-sonarr" } },
  "radarr":   { "filters": { "category": "radarr" } },
  "whisparr": { "filters": { "category": "backup" } },
  "non-radarr": { "filters": { "not_category": "radarr" } }
}

category interacts cleanly with the existing AND-stack — e.g. a "Sonarr + 2160p" folder is just { "category": "tv-sonarr", "include": "2160p" }.

What changed

  • pkg/manager/custom_folders.go — two new filter constants (filterByCategory, filterByNotCategory); matchesFilter and checkSingleFilter take a new category parameter; two case branches do exact-match / negated-match against the torrent's Category.
  • pkg/storage/entry.go — surface Category on EntryMetaInfo so the metadata-only iterator (ForEachMeta) can pass it without a disk read. The field is already on IndexEntry, so no new state.
  • pkg/manager/entry.go — the single call site in getCustomFolderChildren passes meta.Category.
  • pkg/manager/custom_folders_test.go — table tests for: simple match / mismatch, empty-category rejection (an empty Category never matches a non-empty filter), not_category positive + negative, and category in an AND-stack with include (Sonarr+2160p match, Sonarr+1080p reject, Radarr+2160p reject, empty+2160p reject).

Empty-category behavior is intentional and tested: a torrent with no category never matches a category: "X" filter ("" == "tv-sonarr" is false), so older torrents that predate qBit category tracking simply don't show up in category-filtered folders — they remain visible under __all__, the per-provider folder, and any non-category virtual folder.

Test plan

  • go test ./pkg/manager/... -run TestMatchesFilterCategory — all 9 new cases pass
  • go test ./... — full suite green
  • Built locally and validated end-to-end against a live cache:
    • sonarr folder (filter category: tv-sonarr) returned 394 torrents
    • radarr folder (filter category: radarr) returned 69 torrents
    • Spot-checked entries against torrents.json to confirm the category labels match

Notes for the reviewer

  • No config-schema change beyond accepting two new filter-type strings; existing virtual-folder configs continue to work unchanged.
  • The new category parameter is threaded through matchesFilter / checkSingleFilter — there is exactly one call site (the metadata-only iterator in getCustomFolderChildren), and the signature change is local to the manager package.
  • I'm separately running a small one-shot backfill on my deploy that fills in IndexEntry.Category for entries created before this field was tracked, by reading qBit's torrents.json. That's not part of this PR — happy to send it as a follow-up if it's useful upstream.

Adds `category` and `not_category` filter primitives to the Virtual
Folders feature. Lets a virtual folder scope to torrents added by a
specific *arr without inventing any out-of-band metadata: the qBit
category set by the *arr at add time is already a hot field on
IndexEntry (set in pkg/server/qbit/http.go).

Concretely, this unblocks per-arr virtual folder configs like:

    "custom_folders": {
      "sonarr":   {"filters": {"category": "tv-sonarr"}},
      "radarr":   {"filters": {"category": "radarr"}},
      "whisparr": {"filters": {"category": "backup"}}
    }

Changes:
- Expose Category on EntryMetaInfo (already a hot field on IndexEntry).
- Thread category through matchesFilter/checkSingleFilter (5th param).
- Add filterByCategory and filterByNotCategory case branches.
- Update the only call site in pkg/manager/entry.go to pass meta.Category.
- Table tests covering: simple match/mismatch, empty-category rejection,
  not_category positive/negative, and category in an AND-stack with
  another primitive (include).

All 9 new test cases pass; rest of the test suite unaffected.
@sirrobot01 sirrobot01 changed the base branch from main to beta May 5, 2026 09:37
@sirrobot01
Copy link
Copy Markdown
Owner

Nice work @jakeyr

But this needs to be added to the UI as well.

@kebbob
Copy link
Copy Markdown

kebbob commented May 20, 2026

Ah! I could see "Filter key (e.g. name, category)" is listed in an empty input in the Virtual Folder filter, but I couldn't get it to work. Thought I was doing it wrong.

+1 for this being added

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants