Skip to content

v0.2.0 — filters that cannot silently widen

Choose a tag to compare

@sredevopsorgagents sredevopsorgagents released this 08 Oct 19:35
· 3 commits to main since this release
7607e57

Four features on the screens where a selection feeds a write, and one of them changed a rule that was quietly wrong.

Narrowing that cannot silently widen

ArtistIds=<32hex>          albums=2     songs=21
ArtistIds=abc              albums=502   songs=5442   ← filter vanished
ArtistIds=<32hex>|<32hex>  albums=502   songs=5442   ← filter vanished
ArtistIds=<40hex>          albums=502   songs=5442   ← filter vanished

That is a live Jellyfin 12.2.0. An id list it cannot parse is discarded in full, and the server answers with the unfiltered library. A single typo turns "narrow to these three artists" into "the entire library" — and on these screens that set is what feeds a write, so the failure is a batch you did not mean to run.

Ids are now checked against the shape Jellyfin actually parses (32 hex characters, or a dashed GUID) and refused with a 422 that says why. The browser drops a bad id from a URL instead, deliberately: a link you edited should show the unfiltered list, not an error page.

Related, and the same class of quiet wrongness: ArtistIds on an artist query returns zero, not an error. An empty table reads as a library with nothing in it, so an inapplicable filter is now refused rather than answered.

Album and artist filters

Filter albums by artist, and songs by artist and/or album. These are Jellyfin's own parameters, so they are free: nothing is scanned, and total is genuinely the filtered count. ArtistIds=<ABBA> returns 2 of 502 albums and 21 of 5,442 songs; the two parameters union within themselves and intersect across each other.

Exclusion patterns

A pattern is a case-insensitive glob (*, ?) — not a substring, and not a regex.

It matches the item's name, its album and its album artist. All three, because Various Artists is a compilation marker that lives in the album artist and appears in no track or album name, so a matcher reading only the name would leave every compilation in the selection.

Literal, so live hides exactly live and *live* is what hides a substring. Implicit substring matching is how Rock comes to swallow Rockabilly one field over, and the same mistake here would drop items nobody meant to drop. Glob rather than regex because the input is typed into a box by hand, and fnmatch escapes everything but the wildcards — so a pathological pattern cannot become a catastrophic-backtracking regex pointed at your own server.

Jellyfin has no parameter for this, so it costs a read, and the response says what the read covered: scanned, matched, excluded, truncated, limit. A truncated scan is reported as a lower bound rather than presented as a total.

Pagination above the table

As well as below it. A long page is read from the middle, and an operator who has scrolled to the last row should not have to travel back up to move on. The two controls carry distinct accessible names, so they are not ambiguous to a screen reader.

Also

  • A filtered batch or removal now fills its limit. limit: 25 with missing=genres previously returned however many of the first 25 happened to lack genres — maybe four — with nothing to explain the difference. The selection keeps reading until it has 25 that pass, and excluded explains a short result.
  • The same three narrowings apply to batch diffs, genre removals and harvests, so one selection means one thing across the app.
  • Patterns narrow a view or a selection. They never delete anything — a pattern that hides an album from the browse screen does not remove a genre from it. Deleting is still the separate, reviewed, revertible Remove genre tool.
  • Changing any filter clears the current selection, because that selection feeds a write.

Upgrading

No migration and no configuration change. METAEDIT_BROWSE_SCAN_CAP (default 2,000) bounds how much a read-backed filter will look at, and is now documented as an operator knob rather than a test hook — exclusion patterns make scans routine. Raise it on a large library and the filter gets slower rather than wrong, which is the direction to prefer.

Verification

Check Result
Backend suite (CI) 1035 passed, 20 deselected
e2e, clean stack via scripts/e2e.sh 52 passed (12 new)
Frontend unit 28 passed
ruff / mypy / contract --check clean
CodeQL, image build and health pass

Both new mechanisms are mutation-tested rather than trusted. Deleting the facet parameter fails exactly the five facet specs and leaves the seven exclusion specs green, so they are covered independently; removing patterns from the selection key fails exactly the "selection is cleared" spec.

Five defects were found and fixed during this work, all in the author's own code — including a spec that asserted toHaveCount(0) on text that never renders while a selection exists, so it passed whatever the screen did. It had passed twice before anyone noticed. Reasoning for all of the above is in ADR 0017.

Known gap, stated rather than implied: the end-to-end suite is not a CI job. Both features are things you click, and the central guarantee — that filtering narrows rather than widens — lives in specs no CI run executes.