# Changelog All notable changes to JellyTau are documented here. Entries are grouped by the capability they change, not by commit. Requirement IDs in parentheses point at [docs/requirements.md](docs/requirements.md); the generated trace matrix lives in [docs/traceability.md](docs/traceability.md). For how long each fixed defect had been shipping before it was found, see [docs/defect-windows.md](docs/defect-windows.md). ## v0.5.5 ### ✨ Features - **Library artwork is laid out as a mosaic instead of cropped to one box.** The library overview and the home shortcut strip showed three different artwork shapes — square music covers, 16:9 backdrops, 2:3 posters — in grids that pick one box and crop everything to it; the home strip lined its row up by cutting the music covers down. Both surfaces now justify rows to a shared height with each tile as wide as its own artwork, packing from the *decoded* aspect ratio and committing one debounced batch so the grid does not reshuffle as artwork lands. The last row is deliberately left unstretched, so one leftover tile does not inflate into a banner. Favourites also gain a tile per category beside the library it belongs to — which collection type maps to which category is Jellyfin vocabulary, so it is derived in Rust rather than rebuilding the exact leak `SearchScope::item_types` was extracted to close. (UR-075, UR-067 → DR-163, DR-164) ### 🐛 Fixes - **The server no longer burns subtitles into the picture.** Reported as "subtitles are shown even when off", with no toggle in the app clearing them — because they were never the app's subtitles. `PlaybackInfo` omitted `SubtitleStreamIndex`, which does not mean "none": the server then honours the source's own default flag, and on the reported episode that default was a PGS bitmap track, which cannot go out as a sidecar. So it composited the track onto every frame. The cost landed on the *video*: burn-in rules out remuxing, so an HEVC stream that needed only its audio transcoded was re-encoded frame by frame, which the server could not sustain — playback stalled every few seconds and seeks took five to nine seconds to draw a frame. The negotiation and the stream URL now both ask for `-1` and advertise every text format the app can render as `External`, and the picker offers only subtitles the app can actually draw, with the codec verdict decided in Rust and carried across the boundary. Nothing is lost: the app already fetches text tracks and draws them itself. (UR-020, UR-004 → DR-176) - **Switching bitrate mid-film no longer stalls playback.** Jellyfin keys a transcode job by device and play session, but every stream URL carried the same hardcoded `DeviceId` and no `PlaySessionId` at all — so a second stream for an item was indistinguishable from the first and nothing ever stopped the old ffmpeg. The server served the new playlist and then answered 400 for its segments. Re-opening a stream is not rare: a quality switch, a transcoded seek and an audio-track switch all do it. Each open now mints a session id and stops the job it supersedes, in the URL builder so every re-open path is covered by construction. Two client faults that made the same incident worse go with it: the fatal-HLS-error handler double-counted the transcode seek offset, so past roughly halfway through a film any transient network error read as end-of-stream and autoplay skipped to the next item; and the HTML5 reload primitive resolved on its own timeout, reporting success for a reload the server never served. (UR-074, UR-004 → DR-177) - **Downloading an album gets the whole album.** `download_album` read its track list from the local catalog cache, but Jellyfin does not return `AlbumId` on every listing endpoint, so tracks cached from one of those were invisible to the query — three albums in the reported database had it NULL on every track. The frontend then resolved stream URLs from its *own* list and paired them with the returned rows by position, so a row could be handed another track's URL and anything past the end of the shorter list never started. The same missing link hid downloaded tracks under their album offline. The operation now belongs to Rust end to end — the server is asked what the album contains, the album link is written onto every track queued, URLs resolve in the backend scoped to the rows just queued, and each track gets its own file so a title repeated across two discs stops overwriting itself. Re-tapping download on a broken album heals it. (DR-173) - **Playback positions reported to Jellyfin are real ones.** Returning to the foreground before the background-audio stream had started playing handed the frontend 0.0s, so the episode restarted from the beginning and the stop report wrote that zero to the server as the resume point. The same blind spot covered webview-rendered media, whose native position is a permanent 0 — 14 of 14 stop reports in a 35-minute trace were zeroes, one landing 40s after the frontend had correctly reported 15:22 for the same episode. Position is now the maximum of the backend's reading, the last position webview media reported and the handoff base (at most one is ever meaningful); zero-position stop reports are withheld, since a zero is never information and only ever destroys a real resume point; and progress is reported from the controller's own ticks — `/Sessions/Playing/Progress` had previously been requested zero times in those 35 minutes. A finished audio-only episode is also reported stopped at its runtime so Jellyfin's 90% rule marks it played, which nothing else could do once the webview was suspended. (UR-005, UR-025, UR-040, UR-071 → DR-178, DR-179, DR-180) - **The streaming quality button uses a speedometer icon**, not the cloud-download glyph that read as a download action. ## v0.5.4 ### 🐛 Fixes - **Native Android video is opt-in again — enabling it by default shipped sound with a blank screen.** The decode path was never at fault: ExoPlayer ran and fed a live SurfaceView the whole time, behind an opaque page. The step that clears the layers above it never took effect — the WebView was logged going transparent `= false` and never `= true`. This is precisely what the flag existed to contain, and v0.5.3 had turned it on so picture-in-picture would have a real surface to shrink. Reverting costs nothing that matters: PiP drives from the WebView `