metaedit

0017. Facet filters are id-validated; exclusion patterns are literal globs

Date: 2026-10-08

Status

Accepted

Context

Operators needed to point at a precise set before that set fed a write: “ABBA’s albums”, “these three artists’ songs in this one album”, “everything except the live records”. The browse screen offered a search box, a sort and a missing filter, and nothing that could express any of those three.

Two of them are cheap and one is not, and the difference is not obvious from the UI:

Two hazards showed up while probing, and both are the kind that produce a plausible screen rather than an error:

  1. An unparseable id list is discarded in full. ArtistIds=abc, ArtistIds=<32 hex>|<32 hex> and ArtistIds=<40 hex> each returned the entire library – 502 albums and 5,442 songs – because nothing in the list parsed and the parameter was ignored. A junk entry alongside a valid one is dropped individually, so ArtistIds=<junk>,<ABBA> correctly returns ABBA’s 2. The failure mode is therefore a filter that silently does the opposite of filtering, and on these screens the result feeds a write.
  2. A facet that cannot apply returns zero, not an error. ArtistIds on IncludeItemTypes=MusicArtist returns 0 of 639 albums’ worth of artists, and AlbumIds on a music album does the same. An empty table reads as “this library has nothing in it” rather than “that filter cannot work here”.

Decision

Ids are shape-checked at the boundary, and an unusable one is refused with a 422. domain.identifiers accepts exactly the two spellings Jellyfin parses – 32 hex characters, or the dashed GUID form, either case, whitespace tolerated – and refuses anything else, naming the field and the value. The message states the reason, because an operator looking at a “filter” that widened their selection needs to know why.

Dropping the bad id instead was rejected: dropping an entry narrows a union, so the caller would get results and believe they were the ones asked for. Refusing to answer is the only outcome that cannot be mistaken for success.

The SPA takes the opposite approach at its own boundary, and deliberately: it drops non-GUID ids out of the URL rather than sending them, exactly as it already falls back for an unrecognised sort. A URL is a link someone may have edited, not a contract, and erroring a screen over a stale link is worse than showing the unfiltered list. The API still refuses, so a hand-written request cannot bypass the guard.

A facet that cannot apply to the media type is refused rather than answered with zero. An empty table that looks like missing data is the class of defect this project treats as a bug, so facet_filters raises instead.

Exclusion patterns are literal, case-insensitive globs matched against the item’s name, its album and its album artist. Three parts to that:

Exclusion is evaluated before the missing-aspect test, in one shared pass. This keeps excluded and matched disjoint, so the report can name either without double-counting an item. Reporting scanned - matched instead would call an item dropped by a pattern “missing genres”, which is a different fact about the library.

A filtered selection over-fetches to fill its limit. A batch of 25 that drops 21 items to a pattern would otherwise return four, and the operator would read that as “there is nothing else to do”. The selection keeps reading until it has limit items that pass, the library ends, or the scan cap is reached – and reports what it read.

Exclusion narrows a view or a selection; it never deletes. A pattern that hides an album from the browse screen does not remove a genre from it. Deleting is the separate, reviewed, revertible removal tool (ADR 0016’s sibling feature).

Consequences

Alternatives rejected