Repository navigation
Library: keep the A-Z rail under Unwatched and genre filters (#869) - #1030
Open
Bardioc1977 wants to merge 11 commits into
Open
Bardioc1977 wants to merge 11 commits into
Bardioc1977 wants to merge 11 commits into
Conversation
…hedLeaves and genre
The mock's firstCharacter ignored every filter and the type, and /all ignored
unwatchedLeaves (the key the app sends for shows and seasons). Both now scope
like the real PMS 1.43.5: the counts describe the filtered listing, so their
sum is the filtered totalSize and prefix sums are the offsets of each letter.
New class LetterRailFilters pins that. Against the unmodified mock it failed
with e.g. "60 != 40 : unwatched=1", "60 != 16 : genre=1" and
"Lists differ: [{'key': 'a', ...}] != []" (a genre matching nothing).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: Heiko Kopp <47413920+Bardioc1977@users.noreply.github.com>
Tests for GLinnik21#869, written before the fix and failing against it: - the letters request carries the terms the grid sends (wire level, movies and the TV types), and is asked again when they change - an answer asked under older terms, a genre restored by the first page, a read-back or scan that changed the membership: the old counts are never shown over the new grid - a watch while the Collections page is listed: the loan handed the pre-watch Unwatched counts back over the refetched grid - the rail follows the sort and the list, not the filters (the existing view test that pinned the opposite is turned around) Guards that stay green: a sort edit owes nothing, an empty answer is a landed list. The loopback rig learns a port and a section kind. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Heiko Kopp <47413920+Bardioc1977@users.noreply.github.com>
`Client::section_directory` took only a metadata type, so a `firstCharacter` read could never describe a filtered listing. It now takes the wire terms of the listing it describes and appends them exactly as `section_items_query` does (one pair at a time, in order); an unfiltered movie request is the same bytes as before. `LibraryType::type_terms` is the one place the `type=` pair is made, for the listing request, the genre list and the saved-genre check. No caller sends more than the type yet. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Heiko Kopp <47413920+Bardioc1977@users.noreply.github.com>
…the grid's own listing The rail hid itself under Unwatched or a genre because its letter counts came from an unfiltered firstCharacter read, asked once per section and type, while the grid was filtered: the jump offsets are prefix sums of those counts and would land on the wrong titles. - The letters job sends the same terms as the grid's page read (SecState::query_filters), and its answer comes back under a LettersKey: the applied Unwatched, the applied genre and a membership generation. - A list is shown only while its key is the one the section's query would ask with (SecState::letters_current), checked where the letters are read. That covers a genre restored by the first page's landing, an answer asked under older terms, and a sort edit changes nothing. - A read-back or library scan that changed what the section lists bumps the generation, so the counts are asked again (also for unfiltered lists). - Every library type change forgets the letters, and the Collections loan no longer carries them: a watch done while Collections is listed cannot leave the pre-watch counts over the refetched grid. - A read that fails or panics leaves the list as it was, hidden, and holds the same key back for 120 steps instead of dropping the rail for good; the directory answer carries ok for it. - rail_available loses its Unwatched and genre terms; the sort rule is unchanged (title ascending, more than one letter). The new code is browse/letters.rs. Tests: the red commit's plus the request shape, the back-off, and the loan test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Heiko Kopp <47413920+Bardioc1977@users.noreply.github.com>
…b carries them pms-api.md 2b records that firstCharacter honours type, unwatched, unwatchedLeaves and genre like /all (counts sum to the filtered totalSize, an empty filter answers an empty Directory, prefix-summed offsets land on the right title; PMS 1.43.5, movies and shows). sans-io.md follows the listing lanes: letters are owed by letters_current rather than a done flag, a failed letters read backs off instead of landing empty, and the job carries the grid's terms and a LettersKey. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Heiko Kopp <47413920+Bardioc1977@users.noreply.github.com>
SecState::loaned said the Collections loan holds the letters; it stopped doing so when the letters became keyed counts that every type change forgets. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Heiko Kopp <47413920+Bardioc1977@users.noreply.github.com>
Signed-off-by: Heiko Kopp <47413920+Bardioc1977@users.noreply.github.com>
Bardioc1977
marked this pull request as ready for review
October 25, 2026 10:31
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Heiko Kopp <47413920+Bardioc1977@users.noreply.github.com>
…g a filter off keeps the rail One list slot per section made a genre or Unwatched switched on and off hide the rail and re-ask the unfiltered counts on every return. The section now holds the landed list and the one it replaced (two keys, never more); every reader picks through SecState::shown_letters. A list counted under Unwatched is never the kept one, since a watch made while the switch is off flags nothing, and a membership change or a type change forgets the kept list as it forgets the shown one. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Heiko Kopp <47413920+Bardioc1977@users.noreply.github.com>
…nd seasons A real PMS answers them differently for shows (30 of 33 against 24, docs/pms-api.md), and the mock treated them as one, so it could not catch an app that sent the wrong key. unwatched=1 on a show or season now reads the row's own viewCount; a test pins that the two keys differ and points at the Rust request-terms test for the key the app sends. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Heiko Kopp <47413920+Bardioc1977@users.noreply.github.com>
docs/sans-io.md: kept main's text and numbers (discovery lane, pins' session input, Related tail window) and kept this branch's "letters carry the grid's terms" bullet, which sits in the listing lanes section ahead of main's new bullets. rust-modules/data/src/browse/mod.rs: declared both `mod letters` and main's `pub mod discovery_job`; the pump step keeps this branch's letters-backoff tick and takes main's `land_discovery_owned_with_gate(.., session)` and `maybe_discover_owned(adapter, keys, jobs)`. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Heiko Kopp <47413920+Bardioc1977@users.noreply.github.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Title-sorted Library listings keep their A-Z rail when Unwatched or a genre is on. Before, the rail vanished on the first filter.
Why
ListingView::rail_availablehid the rail under a filter because the letter counts came from an unfilteredfirstCharacterrequest, fetched once per section and listing type, while the grid was filtered. The rail jumps by prefix sums of those counts,
so with a filter on every jump would have landed on the wrong title.
How
Client::section_directorytakes the listing's wire terms; the letters job sends the SAME terms the grid page sends(
SecState::query_filters, the single source:type=,unwatched/unwatchedLeaves,genre).LettersKey: applied Unwatched, applied genre, a membership generation,in the new
browse/letters.rs) and are shown only while that key is the one the section's query would ask with. It ischecked where the letters are read, so the saved-genre restore (applied by the first page's landing, after the letters
were asked) is correct without touching its code, and an answer asked under older terms is never shown.
asked again (this also fixes a latent offset shift after a scan on unfiltered lists).
Collections is listed cannot leave the pre-watch counts over the refetched grid. Cost: one small request on every return
from Collections.
SecState::letters_kept, every readerpicks through
SecState::shown_letters). Turning a genre or Unwatched on and then off brings the unfiltered rail back atonce, with no read. Stated cost and bounds: a third key evicts the oldest (genre A, genre B, then back to unfiltered reads
once more), and a list counted under Unwatched is never the one kept, because a watch made while the switch is off flags
nothing (
membership_depends_on_viewing), so a kept one could come back wrong. A membership change (scan, read-back) or alibrary type change forgets the kept list as it forgets the shown one.
key back for 120 steps instead of dropping the rail for good; an edit asks at once.
rail_availableloses its Unwatched/genre terms. The sort rule is unchanged: title ascending and more than one letter.Descending title and every other sort still hide the rail (a descending rail is feasible, measured below, but changes
rail.rssemantics and is a follow-up).firstCharacterand/allhonourtype,unwatched,unwatchedLeaves,genre. On shows and seasons the mockkeeps
unwatchedandunwatchedLeavesapart, as a real PMS does (30 of 33 shows against 24,docs/pms-api.md):unwatchedLeavesis "not every leaf watched",
unwatchedreads the row's ownviewCount, which a show never carries in the mock. Only thedivergence is modelled, not PMS's undocumented rule.
LetterRailFilters.test_unwatched_is_not_unwatched_leaves_on_showspins that the mock tells the keys apart; the key the app sends for a show is pinned by the Rust request-terms test
the_letters_request_carries_the_terms_the_grid_sends(data/src/browse/listing_jobs_tests.rs).docs/pms-api.md2b,docs/sans-io.md. README untouched (bug fix).Verified
Red, then green (new tests failed against the unfixed code for the intended reasons, observed before the fix):
listing_jobs: 9 of 17 failed, e.g.the_letters_request_carries_the_terms_the_grid_sends("Movie Primary: the edit owes theletters again", left 0, right 1),
an_answer_asked_under_older_terms_is_never_shown(counts of the unfiltered listing over afiltered grid:
[("A", 3), ("B", 4)]),a_restored_genre_owes_the_letters_again,a_membership_change_owes_the_letters_again,a_scan_owes_the_letters_of_a_section_that_is_not_showing,returning_from_collections_asks_for_the_letters_again,the_rail_is_hidden_by_the_sort_and_the_list_and_not_by_the_filters, and the Collections-loan regressiona_watch_while_collections_is_listed_does_not_leave_old_counts_over_the_new_grid("the counts from before the watch over therefetched grid:
[("A", 3), ("B", 4)]");view::query_and_menus_are_one_retained_publication("Unwatched and a genre nolonger hide the rail").
tests/test_mock_pms_library.py::LetterRailFiltersfailed against the old mock (60 != 40 : unwatched=1).turning_a_filter_off_brings_the_unfiltered_rail_back_without_a_readandonly_the_last_two_lists_are_heldfailed on the one-slot code (left: [],right: [("A", 3), ("B", 4)]);test_unwatched_is_not_unwatched_leaves_on_showsfailed against the one-predicate mock (5 == 5). Guards mutation-checked(each mutant killed): keeping an Unwatched list (
an_unwatched_list_is_not_kept_across_the_switch),forget_lettersnotclearing the kept list and the kept list ignoring the generation (
a_membership_change_and_a_type_change_forget_the_kept_list).origin/main(merge89aaa61f, 29 commits of main, no textual conflict; headae264baa), committed tree,git status --shortempty before and after:make check-cargo: exit 0, 14 test binaries, among themtest result: ok. 1263 passed; 0 failed(data),test result: ok. 1721 passed; 0 failed(screens),test result: ok. 1864 passed; 0 failed; 67 ignored(media)make test-hostsim:test result: ok. 1175 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out;make test-hostsim C=plx_media:test result: ok. 2068 passed; 0 failed; 67 ignoredmake check-nodef,make lint,ci/check-module-layers.py(green — 14 layers, 1446 modules),ci/check-list-caps.py(289 sites, every one classed),ci/check-localization.py: exit 0ci/check-deps.sh: every stageok, ledgers unchanged (blocking 2, workers 9, globals 66, forks 58, fxlanes 11 sites)make check-python-harnessandtests/test_harness.py:Ran 544 tests in 231.918s/OK (skipped=1)tests/test_mock_pms_library.py:Ran 89 tests ... OK (skipped=8);tests/test_mock_pms_discover.py:Ran 22 tests ... OK;tests/mock_pms.py --selftest: exit 0make check-replay:3 fixtures, 6 replays, 0 failures; 4 parallel, every replayverdict=SAMEmake check:1 of 113 steps FAILED(cargo branch ok, 283 s); the one failure isci/test_disk_preflight.py::test_a_checkout_outside_the_named_roots_is_not_planned_under_an_override(
1 != 2 : disk-preflight: only 2.0 GiB free for ~/main/rust-modules/target ...), an assertion about the free space of themachine it ran on, not this change.
totalSizeand prefix-summed offsets land on theright title for movies (unwatched, genre, both, unwatched descending) and shows (
unwatchedLeaves, genre). Measured whileplanning;
docs/pms-api.md2b records it.tests/mock_pms.py --movies 60 --rail-fixture; host FFmpeg not built, UI/browse only):rail A C F M Z with Unwatched on ("40 of 60 films"), the request log shows
firstCharacter?unwatched=1; jumping to F lands onthe card whose year and length equal the mock's item at the prefix-summed offset; with Unwatched + genre (11 films) the rail
is present and a jump to Z lands on offset 9; Date Added hides the rail and re-selecting Title brings it back with no new
letters request; a Collections round trip with the filters on costs exactly one new
firstCharacter?unwatched=1&genre=1and therail returns after the reload.
Notes
that is not loaded, or watched from Home) flags nothing while Unwatched is on, so later pages and the counts can differ by
one until the next requery. The page lane has the same blind spot.
made with Unwatched off and Unwatched switched on again could show that filtered list once more; the page lane's
membership_stalehas no term to flag it with. It needs a failed read, a watch and a toggle inside the 120-step back-off.page's landing); it is never shown over the filtered grid.
parity-specs,docs/library-filters-spec.mdlines 67-69 ("today that holds for unwatched/genre") become true withthis PR; its open question 5 is partly answered (unwatched, unwatchedLeaves, genre, type: yes; other facets unmeasured).
Closes #869
Signed-off-by: Heiko Kopp 47413920+Bardioc1977@users.noreply.github.com
🤖 Generated with Claude Code