Files
proton-stream/docs/BUGS.md
T
2026-08-02 19:37:03 +02:00

14 KiB

Bugs

The issue ledger. Open items first, then a log of what was fixed and where.

Format: Bn — one-line summary, then symptom, cause, fix, and how it was verified. Reference entries from PRs.

Open

None yet — the app is young enough that everything known is in docs/DEVELOPMENT.md as unbuilt rather than broken.

Fixed

B13 — the player's controls ran off the bottom of the window

Symptom. On the player page the transport row — previous, ±seconds, play, volume, the pickers — sat flush against the bottom edge of the window and was clipped by it. The floating Skip opening button sat at the same height as the seek bar rather than above the controls, four pixels out of line with them.

Cause. CHROME_HEIGHT was a constant 132 px. The controls are two rows of themed buttons whose height follows the theme and the platform's text scaling, so the constant was a guess — and a guess that is low does not clip in egui, it draws past the rectangle and off the window. The skip button then used its own 22 px inset where the controls used 26.

Fix. ui::player::chrome returns the height it actually measured (ui.min_rect().height() plus padding), the app carries it in Overlay, and the next frame uses it for the scrim and for floating anything above the controls. The content is anchored to the bottom of the window rather than laid out from the top of the strip, so the last row ends a fixed distance above the edge whatever it turns out to contain. One frame of lag, and only while resizing. floating puts both the skip button and the up-next card at the controls' own CHROME_PAD_X.

B12 — the skip button believed a chapter's name

Symptom. Episode one of Oshi no Ko opens with a chapter called Intro that is eleven minutes of the story. The player offered Skip opening over it.

Cause. ChapterRole::of read the first word of the name and nothing else, mapping intro to Opening unconditionally. Nothing looked at how long the chapter was or at what else the file contained.

Fix. Two passes — Claim from the name, roles from the name and the file. A name that could go either way (Intro, Credits, Cast) is resolved by length and position: an opening has to be theme-length, near the front, and the only candidate in the file. Both ambiguities fail towards content. The same pass is what credits_start reads, so the end-of-episode countdown does not assume the last chapter is the ending either — see the Part C case in docs/DEVELOPMENT.md.

Verified. a_long_intro_is_the_episode_and_not_an_opening, a_theme_length_intro_near_the_front_is_an_opening, content_after_the_ending_keeps_the_ending_out_of_the_tail.

B11 — seasons past the first had no episode names, and were one query from having the wrong ones

Symptom. Oshi no Ko seasons two and three showed filenames where season one showed episode names. Season one only "worked" because its filenames carry them (S01E01-Mother and Children.mkv); episode_metadata was empty for the whole library.

Cause. Two, stacked. A title is looked up once, and on AniList that match is the first season's entry — a sequel is a separate id numbering from one — so nothing was ever asked about seasons two and three. And EpisodeGuide::get fell back from (Some(2), 1) to the provider's absolute (None, 1), so the moment season one's episodes were fetched, every later season would have been captioned with season one's names. A wrong caption does not read as a bug; it reads as the library being wrong.

Fix. Provider::seasons_are_separate_entries distinguishes the two providers. For AniList, MetadataService::season_episodes searches each season past the first by the name the provider uses (<title> 2nd Season, then <title> Season 2) and tags the result with that season; TMDB already returns seasons numbered and is left alone. The absolute fallback now stops at season one.

Verified. an_absolutely_numbered_answer_never_reaches_a_later_season, a_season_is_searched_for_the_way_the_provider_names_it. Against the live API, [Oshi no Ko] 2nd Season is id 166531 with 13 episodes and 3rd Season is 182587 with 11 — which is exactly what the share holds.

Still missing for that show, and not a bug here. AniList's episode titles come from streamingEpisodes, and all three Oshi no Ko entries have none at all. Those rows keep their filenames until the viewer switches to TMDB.

B10 — every episode of season three was called "3rd Season"

Symptom. Under Oshi no Ko, all eleven rows of season three read 3rd Season instead of an episode name.

Cause. naming::parse knew S03 and Season 03 but not 3rd Season, so [DB]Oshi no Ko 3rd Season_-_07_….mkv parsed as a title of Oshi no Ko 3rd Season. catalog::build_rows then took the difference between that and the folder's Oshi no Ko to be the episode's own name — which is what it is for, and here it was a season marker.

Fix. find_worded_season reads both orders (3rd Season, Season 3) and ends the title there, so the season comes out of the filename as a season. build_rows additionally asks naming::is_season_marker before keeping any leftover as an episode name, and folds it into the season instead. The dash is deliberately not a separator between Season and its number: Show 2nd Season - 04 puts the episode there, and reading across it made episode four season four.

Verified. underscore_separated_numbering_is_read, a_worded_season_is_read_as_a_season, an_ordinal_that_is_part_of_the_title_is_not_a_season, a_season_stated_in_the_filename_does_not_become_the_episodes_name.

B9 — a series split into one poster per season on the shelf

Symptom. The library wall showed Game of Thrones - Season 1 … Season 8 as eight separate titles, each with its own poster and its own resume point.

Cause. naming::classify_folder recognised a season folder only when the whole name was one (Season 01, S03). A folder named <Series> - Season 1 therefore classified as a title, and the season number stayed in the title string, so the grouping key differed per season.

Fix. naming::trailing_season splits a … Season N / … SNN tail off a folder name, and catalog::ancestry takes the season from a folder that names both. The keyword is required — Evangelion 3 and Alien 3 keep their number, and Blade Runner 2049 still reads as a year.

Verified. a_folder_naming_series_and_season_yields_both, a_title_ending_in_a_number_keeps_it; on the real share, 30 titles became 23, with Game of Thrones one poster of 73 episodes.

B5 — a multi-byte character in a filename panicked the parser

Symptom. The crawl aborted on [Anime Time] Neon Genesis Evangelion - Death (True)²-007.mkv with start byte index 34 is not a char boundary; it is inside '²'.

Cause. Year detection scanned bytes and sliced on byte offsets, so a token boundary could land inside a multi-byte character.

Fix. bare_year walks char_indices; bracketed_year verifies the four bytes are ASCII digits before slicing.

Verified. a_multi_byte_character_does_not_break_parsing.

B4 — files were catalogued with no title

Symptom. Every episode in the catalog had an empty title: a real library is laid out Sousou no Frieren/Season 01/S01E01.mkv, and S01E01.mkv states no series at all.

Cause. Rows were built from the filename alone.

Fix. catalog::build_rows walks the folder path: the folder names the series, the filename names the numbering, grouping folders (Movies, OVA, Specials) are stepped over, and a Season NN folder supplies a season the filename omitted. Non-video files (.nfo, .jpg) are dropped.

Verified. Seven build_rows tests, and a real 354-node share that went from 0 usable titles to 22 correct ones.

B3 — several naming conventions lost their episode numbers

Symptom. 35 of 310 files in a real share had no episode number.

Cause. Four gaps: a bare E11.mkv; a bare 11.mkv; _-_01_ underscore separators; and — the subtle one — Oshi no Ko S2 - 04, where matching the season returned, so the differently-stated episode was never looked for.

Fix. find_episode_marker, find_bare_number, underscore separators in find_dash_numbering, and find_numbering continuing past a season-only match.

Verified. Down to 11 unnumbered files, all of which are films or OP/ED extras that genuinely have no episode number.

Symptom. download_file over a public link returned a file cut off at exactly 50 blocks, with no error.

Cause. The visitor path issued a single un-paged GET …/revisions/{rid} and used whatever Blocks came back. The endpoint pages at 50.

Fix. proton-sdk-rs 0.3.3: the visitor path now uses the same paged list_blocks as the authenticated one, and fetches blocks concurrently.

Verified. a_visitor_can_seek_within_a_shared_multi_block_file reads back a 9 MiB file whole and in ranges.

B2 — a catalog crawl re-derived the same keys per child

Symptom. Listing a folder of several hundred episodes over a public link took minutes.

Cause. resolve_parent_key walked and re-unlocked the whole ancestor chain for every child. Each unlock is an S2K derivation — tens of milliseconds — so 500 siblings cost 500 redundant unlocks of the same parent key. enumerate_nodes also never chunked its link ids, exceeding the server's 150-id batch limit.

Fix. proton-sdk-rs 0.3.3: an LRU node-key cache with an iterative ancestor walk (up to the nearest cached key, then unlock back down caching each) behind SingleFlight; enumerate_nodes chunked at 150 and fanned out.

B6 — read-ahead starved the seeks it was supposed to smooth

Symptom. Dragging to a new position took ~2.9 s with the block layer's read-ahead enabled, versus ~0.57 s with it turned off. Sustained throughput was also lower with read-ahead than without: 5.0 MiB/s against 7.2.

Cause. Prefetches queued from the position the viewer had just left kept running. The block they were fetching was exactly the bandwidth the seek needed, and a 4 MiB block over this link takes most of a second, so the seek waited behind up to six of them.

Fix. pstr-stream::stream tracks where the previous read ended and treats a read landing more than one block away as a seek. A seek cancels every outstanding prefetch before it fetches anything. Aborting mid-fetch throws those bytes away, which is the intended trade — they were speculative, and the request slot they held is what the seek is waiting for. Read-ahead resumes wherever the seek settles.

Verified. Worst-case cold seek back to 715 ms with read-ahead on. a_seek_cancels_the_read_ahead_it_invalidates and sequential_playback_never_cancels_its_own_read_ahead pin both directions, and a_block_whose_prefetch_was_cancelled_can_still_be_read pins that an aborted prefetch releases its single-flight entry rather than leaving the block permanently unfetchable.

B7 — a read-ahead window deeper than the ring evicted its own blocks

Symptom. Raising read-ahead from 12 to 32 blocks reduced sustained throughput, 8.7 MiB/s to 7.8, and the ring started reporting evictions.

Cause. The default 128 MiB ring holds 32 blocks of 4 MiB. A 32-block read-ahead window is the entire ring, so blocks at the front of the window were evicted before the player reached them and had to be fetched a second time.

Fix. clamp_readahead caps the window at half the ring's capacity in blocks — half rather than all, because the ring also has to hold what the player has just passed, so a scrub a few seconds backwards does not refetch. The default depth is 12, chosen because it never evicted and wasted a third less bandwidth on cancelled prefetches than 16 did, for indistinguishable throughput.

Verified. read_ahead_is_capped_at_half_the_rings_capacity and a_tiny_ring_still_reads_one_block_ahead.

B8 — the block layer's read-ahead made playback worse once mpv was in front of it

Symptom. With pstr-stream's tuned default of 12 blocks, mpv took 5.5 s to show a first frame and 3.6 s to resume after a mid-file seek. Turning the block layer's read-ahead off entirely cut both to 2.5 s and 1.5 s, and fetched a third as much data for the same playback.

Cause. B6 and B7 were measured against pstr bench, a reader with no read-ahead of its own — for that reader, 12 blocks is right. mpv is not that reader. Its demuxer cache already runs demuxer-readahead-secs ahead of the picture, sequentially and eagerly, so a second speculative layer underneath adds no buffer at all. It only competes for bandwidth with the reads mpv is blocked on right now, and it does so worst exactly when the viewer is waiting: at startup and just after a seek. The degradation is monotonic in depth.

blocks ahead first frame seek resumed blocks fetched
0 2.5 s 1479 ms 12
6 3.7 s 2225 ms 21
12 5.5 s 3581 ms 31
24 7.7 s 6122 ms 42

Fix. pstr_player::READAHEAD_BLOCKS is 0, and it is what pstr play and the app pass to StreamConfig. pstr_stream::DEFAULT_READAHEAD_BLOCKS stays 12 for consumers that genuinely have no read-ahead — the benchmark, and anything that reads a revision without a demuxer. mpv's buffer is also expressed in seconds rather than bytes (PlayerConfig::readahead_seconds, 30 s), because a byte budget buys two minutes of buffer on a 4 Mbit/s episode and eight seconds of it on a 4K remux, which is backwards from what either wants; the byte figure is kept only as a ceiling, sized under the ring so B7 cannot recur one layer up.

Verified. Sustained playback holds realtime at depth 0 (178 s of media in 180 s of wall clock, the deficit being the 2.4 s to first frame), with no ring evictions, against the same 761 MiB episode over a real public link.