Skip to content

v0.0.4 — filtering, pagination and sorting

Choose a tag to compare

@sredevopsorgagents sredevopsorgagents released this 08 Oct 12:00
· 26 commits to main since this release
113f229

v0.0.4 — filtering, pagination and sorting

Browse screens you can actually narrow down. Library and Archive gain real paging, sortable headers, filter chips and a scan note, and /api/items grows sort, order and a per-media-type missing filter.

The feature is the smaller half. Five of the six commits in this release fix something that was already broken, and all five were found by using the tool rather than by its tests.

The archive was losing a row while showing another twice

ORDER BY name is not a total order. 8 album rows and 2 track rows share a name with another row, and OFFSET paging over a non-total order is incorrect by construction. Measured against a real archive:

109 rows fetched, 108 distinct
duplicated across pages: [90]   'Corazones' — Jorge González
never appearing:         [8]    'Corazones' — Los Prisioneros

The count said 109. The walk yielded 109 entries. One of them was a duplicate — so the missing row was invisible from the UI, and no amount of looking at the screen would have found it.

Every ordering now ends with id, which the reindex assigns deterministically, so the tiebreaker survives a rebuild. Confirmed deterministic rather than flaky, five runs of each:

ORDER BY name       108 distinct, duplicate id 90 in EVERY run
ORDER BY name, id   109 distinct, no duplicates in ANY run

The missing-metadata filter was useless for songs

MusicArtist  500 items: overview in 254, provider ids in 414, genres in 392
Audio        500 items: overview in   1, provider ids in  69, genres in 314

A song without an Overview is the normal state of a song. The old filter counted that as missing metadata, so over a song library it reported 5,441 of 5,442 items incomplete — a wall of red that buried the 1,466 songs whose missing genres were actually fixable. It could not be fixed by filtering harder; it needed a per-media-type definition.

Jellyfin cannot express this for music (Filters=IsMissing is accepted and ignored), so matching items means reading them. That scan is capped, and the cap is reported:

?kind=song&missing=genres  →  679 matched from 2,000 checked, truncated, 1.8s

A labelled lower bound, rather than a total that looks complete and is not.

The bulk and harvest selections had kept their own copy of the same broken test — on a screen whose output is a write. All three surfaces now share one definition.

Two more that shaped how the screens behave

  • The library's total described a set the table could not show. It fetched one page of 200 and filtered in the browser, so the header read "641 artists" above 200 rows. With 5,442 songs, 96% of the library was unreachable and nothing said so.
  • "Select all" meant "select the first page", on a path that feeds a write. It now says "Select these 50", reports how many of the selection are on the current page, and keeps a selection across pages — because selecting across pages is why paging exists — while still clearing it when the query changes.

Also in this release

  • NULLS LAST, argued from the data rather than from theory: all 383 artist rows have a NULL listeners, against 0 of 109 albums. Postgres defaults to NULLS FIRST on DESC, so "sort artists by listeners, descending" would have led with every row that has no listener count.
  • The end-to-end specs are typechecked for the first time. tsconfig.json includes only src and Playwright transpiles without checking, so every spec was the only TypeScript in the app that nothing verified. Adding the config found seven strict-mode errors waiting in the pre-existing specs.
  • The frontend has unit tests, run by node's native TypeScript stripping so the paging arithmetic needs no test runner. The runner treats "no test files matched" as a failure, because node --test exits 0 on an empty glob — a renamed file would otherwise have left the build green while checking nothing.
  • The e2e stub can now fail. It held 5 items, ignored sortBy entirely, and computed TotalRecordCount after slicing — so it reported the wrong total for the one number this release is about. Rebuilt: 65 items, two artists named "Twin Peaks", sort applied before the window, real filters. The suite went from 19 to 27 passing specs.

Verification

Backend 790 passed, 20 deselected
Frontend unit 17 passed
End-to-end 27 passed (was 19)
Types app + test + e2e configs, strict
Lint ruff check, ruff format --check, mypy
Contract export_contract.py --check passes

Every fix was verified by reintroducing the defect and confirming the test fails — sort translation, filter passing, truncation reporting, the tiebreaker, NULLS LAST, per-kind aspects, list query encoding, and four paging arithmetic cases.

Six of the new tests initially passed for the wrong reason and are now fixed: a paging walk that clicked Next and re-read without waiting (collecting page 1 fifty times), an assertion comparing a page's distinct names to the library total, a sort injection the fixture could not detect because every row shared one listeners value, two truncation tests with the cap above the fixture size, and a query test that tested a copy of the function rather than the function.

Known limitations

  • The end-to-end suite is not run in CI. It needs a browser and a compose stack. It was run locally six times and the final run is green, but a future change can break it without CI noticing.
  • No sort tiebreaker is sent to Jellyfin. A second key could not be verified to do anything — appending Id, or even a nonsense key, produced byte-identical responses on a live 12.2.0 server. Adding a key that does nothing would read as a guarantee it is not. The browse is safe today (all 6,583 items walk without loss, and ten identical requests return one identical order), but that guarantee is the server's, not ours. Recorded in domain/browse.py.