Batch of reported bugs and enhancements.
UI
- Pages no longer inherit the previous page's scroll position (DR-156, UR-072).
The shell keeps its scrollers alive across navigation by design, so the
element never remounts and its scrollTop survived the route change; SvelteKit
restores window scroll, which this app never uses. ScrollMemory records the
offset per route and per container: forward moves reset to the top, Back
restores where the route was left.
- Season header stacks on narrow screens, and the title span gets min-w-0 so it
actually truncates instead of overflowing under the action buttons.
- Favourites gets a labelled tile at the head of the library grid rather than
only an unlabelled heart icon in the header.
Playback
- Full-screen video on Android hides the system bars (DR-157, UR-066).
requestFullscreen() cannot touch the Activity window from inside a WebView, so
the control did nothing visible while the bars stayed painted over the video.
ImmersiveModeBridge hides them, restored on exit, Escape and teardown.
- Background-audio handoff stops leaking its relative timeline (DR-159).
background_audio_base was a display-only correction applied in two places
while progress reports to Jellyfin, the frontend and media3's own seeks all
worked in the relative timeline treating it as absolute — each crossing losing
exactly `base` seconds. The conversion now happens once, in the position tick,
and inbound seeks resolve through seek_absolute, which re-opens the stream at
the requested position because the handoff transcode cannot seek.
- Picture-in-picture works on the path that actually plays video (DR-160).
canEnterPip demanded a native ExoPlayer surface, but that path is behind a
flag defaulting to off, so PiP could never engage. It now accepts the WebView
<video> too, keeping the WebView visible and routing play/pause to the element.
- Native video is now the default so PiP has a real surface (DR-161). The
scrub-regression tests pinned the flag-off path implicitly; they now mock it
off explicitly. The native scrub/seek path is not covered by the suite and
needs device verification.
Watched state
- Watched toggle on the episode row, season header, series and movie hero, and
the Episode Focus View (DR-158, UR-073). Both backend halves already existed
with no caller. storage_set_watched covers a container's episodes so the
toggle is honest offline, and QueuedOp::MarkUnplayed gives the sync queue the
missing direction.
Release
- Fix the Android versionCode floor (set-version.sh). v0.5.2 shipped code 5002
under an earlier minor*1000 scheme, but the current minor*100 formula yields
1502 for that version and 1503 for 0.5.3 — so every 0.5.x release built from
it was an un-installable downgrade for anyone already on v0.5.2. Widened to
10000 + major*1000000 + minor*1000 + patch (0.5.3 -> 15003).
- Bump to 0.5.3.
Search's instant leg read only downloaded items, so with no downloads it
returned nothing and every keystroke fell through to a full Recursive=true
server query. It now reads the whole synced catalog through the same
availability CTE get_items uses, gated on the same include_catalog_browse
flag so search and browse cannot diverge. (UR-065, DR-108)
Also fixes three defects found while confirming that:
- items_fts grew by a full duplicate index every catalog pass. INSERT OR
REPLACE fires no AFTER DELETE trigger without recursive_triggers, so the
old index row was orphaned, and a TEXT PRIMARY KEY meant the replacement
took a fresh rowid and inserted a second entry. Now a real upsert, with
migration 021 rebuilding existing indexes. (DR-110)
- DELETE FROM items existed nowhere, so server-side deletions never
propagated. Adds a post-crawl mark-and-sweep, scoped to crawled types,
skipping downloaded items, and refusing to run after a partial crawl
because items.parent_id cascades. (DR-110)
- The index omitted MusicArtist, Playlist and People, which search groups
results by. Adds them plus people_fts (migration 022). (DR-111)
Re-indexing moves from a frontend startup call to a Rust background task
with a 6h TTL, so a long session no longer searches a stale catalog and a
restart no longer forces a crawl regardless of freshness. (DR-109, IR-030)
Downloads gain a lifetime tier. Eviction selected every completed row by
age with no download_source filter, so hitting the storage limit deleted
the oldest download -- typically one saved deliberately for offline -- to
make room for a precached track. It now reclaims only 'auto' rows, and
expired ones are reclaimed first, before live cache is evicted.
(DR-126, DR-127)
Downloaded video and audio-only handoffs now play from disk instead of
streaming; the video path had never consulted downloads at all. No
transcode is involved: MPV runs video=no and ExoPlayer has no surface for
an Audio item. (DR-123 in part, DR-128)
FTS queries are built as quoted phrases so apostrophes, hyphens and
slashes are data rather than operator syntax, and the item-type filter is
bound rather than interpolated.
Specs: docs/specs/catalog-index-search.md,
docs/specs/read-through-media-cache.md
Includes concurrently-developed favourites browsing and background-audio
stream-end handling; the two workstreams share offline.rs, lib.rs and
online.rs, so no subset of files builds independently.
Typing in the desktop header search bar ran library.search() in place and
relied on /library rendering the results inline. On every other /library/**
route nothing rendered them, so the search bar looked broken: results were
fetched and never shown.
Make /search the single surface that renders results. The header bar becomes a
navigator — it hands the query and route-derived scope to /search via ?q= and
?scope=, which seed the page and run the search on arrival. The inline result
block and the header's scope chips are removed; the chips live on /search,
which owns the results. The empty `all` scope is omitted from the URL, and
typing while already on /search does not push a history entry per keystroke.
Migrate catalog MediaItem consumers from stringly item.type ("Audio",
"MusicAlbum", …) to the neutral item.kind enum across all classification
logic: home, library detail, player routing, artist/person/related/genre
components, tv store.
Model refinements found during migration (each a real distinction the
flat item_type collapsed):
- MediaKind::LiveChannel — live TV (playable, non-seekable) vs
- MediaKind::ChannelItem — channel VOD leaf (playable, seekable) vs
- MediaKind::Channel — channel container (drill-in).
TvChannel->LiveChannel, non-folder ChannelFolderItem->ChannelItem.
RelatedItemsSection and GenreTags props migrated from Jellyfin type
strings to MediaKind; MediaKind re-exported from api/types.
Deferred by design: display {item.type} text, ResultsCounter labels,
Person.type (role), stream.type (phase 4), and all runTimeTicks/tick math
(coupled to playbackPositionTicks — phase 3). Old fields still dual-carried
so nothing breaks.
Rust 456 + 7 domain tests, frontend 644 tests, check clean.
Navigation:
- Split conflated "back" into navigateUp (deterministic route parent) and a
history-safe navigateBack that tracks in-app depth via afterNavigate instead
of history.length. Fixes the resume-from-background trap where a stale WebView
stack left the header arrow stuck on the current page.
- /library self-corrects for music/tv/movies (which have dedicated landing
pages): a leftover currentLibrary no longer forces the inline content-list
view, so "up"/back shows the libraries overview. Live TV / channels / other
types still render inline.
Startup (unblock first paint):
- auth.initialize() no longer awaits security-status, player-config, or session
verification before flipping isInitialized. These run fire-and-forget after the
session is restored, so the library overview paints without waiting on several
serial IPC round-trips.
Versioning / CI:
- tauri.conf.json + package.json aligned to 0.0.15 (the tag series had drifted to
0.1.0, whose formula-derived versionCode 1000 outran the v0.0.x tags).
- Release workflow now pins a monotonic Android versionCode
(1000 + major*10000 + minor*100 + patch) so tagged builds never downgrade
below prior installs and always increase in semver order.
Tests: navigation (4), auth (29), playbackMode (23) green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Library screens:
- Add dedicated music, TV, and movie landing pages (hero banner +
horizontal carousels) backed by new music/tv/movies stores.
- Route tvshows libraries to /library/tv; surface rediscover ("haven't
listened to in a while") albums via a new repository method across
online/offline/hybrid repos plus the repository_get_rediscover_albums
command.
- Add an A-Z jump bar for long alphabetically-sorted lists, with grid
index anchors in LibraryGrid/LibraryListView/TrackList.
- Filter the "Podcasts" folder out of music library queries.
Downloads:
- Add a backend queue pump: enqueue_download / enqueue_video_downloads
persist the resolved stream URL + target dir on each row (migration
017), and the pump starts up to max_concurrent and drains the rest
automatically as slots free, instead of the frontend silently dropping
items past the concurrency limit. Album/series/season buttons now
enqueue rather than calling start_download directly.
Other fixes:
- Hybrid search now returns instant cache results and pushes the merged
cache+server union via a request-id-tagged search-event, so superseded
queries can't clobber fresher results.
- URL-encode SearchTerm / genres / item types in online repo requests.
- Android: pause on audio-becoming-noisy (headphone/BT disconnect).
- Convert music category buttons from <button> to native <a> links for better Android compatibility
- Convert artist/album nested buttons in TrackList to <a> links to fix HTML validation issues
- Add event handlers with proper stopPropagation to maintain click behavior
- Increase library overview card sizes from medium to large (50% bigger)
- Increase thumbnail sizes in list view from 10x10 to 16x16
- Add console logging for debugging click events on mobile
- Remove preventDefault() handlers that were blocking Android touch events
These changes resolve navigation issues on Android devices where buttons weren't responding to taps. Native <a> links provide better cross-platform compatibility and allow SvelteKit to handle navigation more reliably.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>