feat(plex): make the Collection filter multi-select with Any/All

A title can belong to several Plex collections, so the collection filter becomes
multi-select combining with Any (OR, default) / All (AND) — the same shape as the
genre/actor filters.

Backend (query.py): `collection` (single) → `collections` CSV + `collection_mode`
on LibraryFilters; meta_filter_conds ORs/ANDs `collection_keys.contains` per id.
Frontend: PlexFilters `collection`/`collectionTitle` → `collections` + optional
`collectionMode` + `collectionTitles` map; CollectionPicker is now a multi-select
Combobox with the any/all match footer; one chip per collection (usePlexActive-
Filters); the info-page "Browse collection" strip unions into the list; applyFilter
now shallow-merges label maps so adding one keeps the others' titles. PlexProvider
migrates a legacy persisted single `collection` into the new shape on read.

Test: test_plex_collections_filter (single / any-union / all-intersection).
This commit is contained in:
2026-08-22 08:18:25 +02:00
parent 6c7038cf1f
commit a60164edc4
8 changed files with 151 additions and 37 deletions
+14 -3
View File
@@ -123,9 +123,20 @@ export function PlexProvider({ children }: { children: ReactNode }) {
const filters = useMemo<PlexFilters>(() => {
try {
// Drop the legacy `sortDir` — direction now lives in the sort SortState. Left here it would leak
// into the facets query key and be re-persisted on every filter change.
const { sortDir: _drop, ...rest } = JSON.parse(filtersRaw);
return { ...EMPTY_PLEX_FILTERS, ...rest };
// into the facets query key and be re-persisted on every filter change. Also migrate the legacy
// single `collection`/`collectionTitle` into the multi-select `collections`/`collectionTitles`.
const {
sortDir: _drop,
collection: legacyCollection,
collectionTitle: legacyTitle,
...rest
} = JSON.parse(filtersRaw);
const merged: PlexFilters = { ...EMPTY_PLEX_FILTERS, ...rest };
if (legacyCollection && !merged.collections.length) {
merged.collections = [legacyCollection];
if (legacyTitle) merged.collectionTitles = { [legacyCollection]: legacyTitle };
}
return merged;
} catch {
return EMPTY_PLEX_FILTERS;
}