Files
jellytau/docs/requirements.md
T
dtourolle 9d099268b9
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 6m30s
🏗️ Build and Test JellyTau / Android Compile Check (push) Has been skipped
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m25s
Traceability Validation / Check Requirement Traces (push) Successful in 18s
Build & Release / Run Tests (push) Failing after 6m4s
Build & Release / Build Linux (push) Has been skipped
Build & Release / Build Windows (push) Has been skipped
Build & Release / Build Android (push) Has been skipped
Build & Release / Create Release (push) Has been skipped
fix(player): make the video seek bar work by touch (DR-099)
On Android, dragging or tapping the progress bar moved the thumb but
playback stayed where it was. Two separate defects, both touch-only,
which is why the mouse-driven scrub tests never caught either.

1. Gesture hijack. DR-098 taught handleTouchStart to ignore touches that
   land on a control, but handleTouchMove kept running. It measures
   against touchStartX/Y, which that early return leaves at the PREVIOUS
   gesture's values, so a seek-bar drag produced a huge bogus vertical
   delta: read as a brightness swipe, it dimmed the screen to the 0.3
   floor and fired a spurious play/pause "correction" mid-drag. A gesture
   is now latched at touchstart (playerGestureActive) and touchmove
   ignores anything unlatched — re-checking the move target cannot
   recover a start point that was never recorded.

2. Commit signal. The seek was committed only from `change`, which
   Android's WebView does not reliably fire for a touch interaction on a
   range input, so the thumb moved to the tapped position and no seek
   ever ran. touchend/mouseup now commit too; `input` arms a one-shot
   latch so whichever release signal arrives first commits and the other
   is a no-op. seekRelative shares the same commitSeek entry point
   instead of fabricating a synthetic change event.

Tests drive the slider with real touch events (UT-089, UT-090) and fail
against the pre-fix component.
2026-08-01 10:41:23 +02:00

58 KiB
Raw Blame History

Requirements Specification

This document captures JellyTau's user requirements, software requirements, traceability matrix, test traceability, and known technical debt.

For a narrative overview of the system design, see docs/architecture/. For development workflows, see the README and scripts/README.md.

1. User Requirements

ID Requirement Priority Status
UR-001 Run the app on multiple platforms (Linux, Android) High In Progress
UR-002 Access media when online or offline High Done
UR-003 Play videos High Done
UR-004 Play audio uninterrupted High Done
UR-005 Control media playback (pause, play, skip, scrub) High Done
UR-006 Control media when device is on lock screen or via BLE headsets Medium Done
UR-007 Navigate media in library High Done
UR-008 Search media across libraries High Done
UR-009 Connect to Jellyfin to access media High Done
UR-010 Control playback of Jellyfin remote sessions Low Done
UR-011 Download media on demand Medium Done
UR-012 Login info shall be stored securely and persistently High Done
UR-013 View and manage downloaded media Medium Done
UR-014 Make and edit playlists of music that sync back to Jellyfin Medium Done
UR-015 View and manage current audio queue (add, reorder tracks) Medium Done
UR-016 Change system settings while playing (brightness, volume) Low Planned
UR-017 Like or unlike audio, albums, movies, etc. Medium Done
UR-018 Choose to download series, albums, songs, artist discography Medium Done
UR-019 Resume playback from where you left off (movies, shows, albums) High Done
UR-020 Select subtitles for video content High Done
UR-021 Select audio track for video content High Done
UR-022 Control streaming quality and transcoding settings Medium Planned
UR-023 View "Next Up" / Continue Watching on home screen; auto-play next episode with countdown popup and configurable episode limit Medium Done
UR-024 View recently added content on server Medium Done
UR-025 Sync watch history and progress back to Jellyfin High Done
UR-026 Sleep timer for audio and video playback (roller UI, time/track/episode modes) Low Done
UR-027 Audio equalizer for sound customization Low Done (Linux only)
UR-028 Navigate to artist/album by tapping names in now playing view High Done
UR-029 Toggle between grid and list view in library Medium Done
UR-030 Quick genre browsing and filtering Medium Done
UR-031 Crossfade between audio tracks Low Not implemented (blocked — see DR-034)
UR-032 Gapless playback for seamless album listening Medium Done (Linux only)
UR-033 Volume normalization to prevent volume jumps between tracks Low Done (Linux only)
UR-034 Rich home screen with hero banners, carousels, and personalized sections High Done
UR-035 View cast/crew (actors, directors) on movie/show detail pages High Done
UR-036 Navigate to actor/person page showing their filmography Medium Done
UR-037 Visually appealing video library with poster grids and metadata High Done
UR-038 Movie/show detail page with backdrop, ratings, and rich metadata High Done
UR-039 Navigate between main sections via bottom navigation bar High Done
UR-040 Keep a video's audio playing when the app is backgrounded or the screen is locked, stopping video decode until the app returns to the foreground (per-player toggle; Android) Medium Done (pending device verification)
UR-041 Continue watching locally-playing video in a floating picture-in-picture window when leaving the app (Android) — PiP applies to video only, never to audio playback, library/menu browsing, or remote/cast sessions Medium Done
UR-042 Authenticate to a server and manage the session lifecycle (connect, log in, Quick Connect, background session verification, re-authenticate, log out) High Done
UR-043 Automatically detect server reachability and switch between online and offline operation without user intervention High Done
UR-044 Pin downloaded media so it is protected from automatic cache eviction Low Done
UR-045 Predictively pre-cache likely-next media (queue lookahead and album affinity) within a storage budget Low Done
UR-046 Group multiple remote players into a synchronized playback group (LMS SyncGroups) Low Done
UR-047 Manage multiple Jellyfin servers (add, list, remove) and switch the active server/account Medium Planned (backend store done; switcher UI pending)
UR-048 See the next episodes of a series directly below the episode/series being viewed, above cast and similar-shows content, so continuing a show is the shortest path (see ux-flows.md §5B) High Done
UR-049 Search is scoped by where it was started — inside a library it searches that library, from Home/library-root/search-tab it searches everything — with the scope shown as filter chips under the search bar that preselect from context and can be changed without retyping (see ux-flows.md §6.1) High Implemented
UR-050 Reorder search result groups (Songs, Albums, Artists, Movies, TV Shows) by drag and drop in settings, so the media a user cares about most appears first (see ux-flows.md §6.3) Medium Implemented
UR-051 Browse library pages in a consistent layout where card shape signals media type (square music, poster video, thumbnail episode), ordinal content stays listed, and the grid/list preference persists across pages (see ux-flows.md §5A) Medium Partial (implemented; toggle not reachable from settings)
UR-052 While offline, library pages show only media available on the device by default, with an opt-in toggle that additionally reveals the cached server catalog as greyed-out entries which can be queued for download on the next reconnect High Done
UR-053 Restrict media downloads to unmetered networks via a "WiFi Only" setting: when enabled, queued downloads are held while the device is on cellular or a metered connection (including metered WiFi hotspots) and resume automatically once an unmetered network is available Medium Done (pending device verification)
UR-054 Reach account actions (Settings, Downloads, Display preferences, Sign out) from every authenticated screen via a single account menu anchored to the user's name, identical on desktop and mobile (see ux-flows.md §1.2) High Done
UR-055 Browse downloaded media as an offline-scoped library — reusing the same library grids, cards, and detail pages as online browsing, showing only libraries/containers with downloaded content — with the transfer-progress list demoted to a secondary "Transfers" view (see ux-flows.md §7.2) High Done
UR-056 See how much disk each downloaded item/album/series consumes, in familiar rounded units shown on the card and detail page, with a device total on the Downloaded surface and a reclaim amount stated at the point of removal (see ux-flows.md §7.3.1) Medium Done
UR-057 Settings apply the instant a control is changed — no "Save" button and no save/dirty state — so leaving the page never loses a change; sliders show a live readout while dragging but persist on release (see ux-flows.md §8.1) Medium Done
UR-058 On the home screen, a tap on a media card opens the item (movie/episode detail page, or the series Episode Focus View for episodes) rather than starting playback; a long-press starts "play now" after a confirm; an episode detail/focus page links back to its parent series and season (see ux-flows.md §5B.5 and §5B.1) Medium Done
UR-059 Skipping to the next episode records the episode left behind as fully watched rather than saving a mid-episode resume point — skipping means "done with this one", not "stopped here" — and Continue Watching hides episodes the viewer has already moved past (a partial position behind that series' next-up episode), so the row only ever offers genuinely unfinished media Medium Done
UR-060 Search results are ordered by how well they match: a name that starts with the query outranks one matching mid-word (typing "parks" finds "Parks and Recreation" before "Sparks of Love"), and at equal match quality a container outranks its contents (a series before its episodes). Results are grouped into distinct categories — TV Shows, Episodes, Movies, Songs, Albums, Artists and People — so a show never competes with its own episodes for the same slot, and searching an actor's name reaches their bio High Done
UR-061 Double tapping the video skips within it — right half jumps forward 30 seconds, left half jumps back 10 seconds — with an on-screen indicator naming the amount. A double tap leaves the play state unchanged — playing jumps and keeps playing, paused jumps and stays paused — because the second tap re-toggles what the first tap toggled (see DR-098); the skip lands relative to the position the player actually reports, and repeated double taps accumulate rather than all skipping from the same spot Medium Done

2. Software Requirements

2.1 Integration Requirements

External system integrations and platform-specific implementations.

ID Requirement Category Traces To Status
IR-001 Build system supporting multiple targets (Linux, Android) Build UR-001 Done
IR-002 Build scripts for Android and Linux Build UR-001 Done
IR-003 Integration of libmpv for Linux playback Playback UR-003, UR-004 Done
IR-004 Integration of ExoPlayer for Android playback Playback UR-003, UR-004 In Progress (basic playback works, audio settings missing)
IR-005 MPRIS D-Bus integration for Linux lockscreen/media controls Platform UR-006 Planned
IR-006 Android MediaSession integration for lockscreen controls Platform UR-006 Done
IR-007 Bluetooth AVRCP integration via system media session Platform UR-006 Planned
IR-008 Android audio focus handling (pause on call) Platform UR-004, UR-006 Done
IR-009 Jellyfin API client for authentication API UR-009, UR-012 Done
IR-010 Jellyfin API client for library browsing API UR-007, UR-008 Done
IR-011 Jellyfin API client for playback streaming API UR-003, UR-004 Done
IR-012 Jellyfin Sessions API for remote playback control API UR-010 Done
IR-021 Android MediaRouter integration for remote volume in system panel Platform UR-010, UR-016 Planned
IR-013 SQLite integration for local database Storage UR-002, UR-011 Done
IR-014 Secure credential storage (keyring/keychain) Security UR-012 Done
IR-015 Jellyfin API client for playback progress reporting API UR-019, UR-025 Done
IR-016 Jellyfin API client for subtitle/audio track info API UR-020, UR-021 Done
IR-017 Jellyfin API client for transcoding parameters API UR-022 Planned
IR-018 libmpv subtitle rendering and selection Playback UR-020 Planned
IR-019 libmpv audio track selection Playback UR-021 Planned
IR-020 libmpv/ExoPlayer equalizer integration Playback UR-027 Done (Linux/MPV; Android parity pending)
IR-022 Jellyfin API client for person/cast data API UR-035, UR-036 Done
IR-023 Database schema for person/cast caching Storage UR-035, UR-036 Done
IR-024 Jellyfin API client for home screen data (featured, continue watching) API UR-034 Done
IR-025 Android background-audio handoff: WebView <video> → native ExoPlayer foreground service on background/lock, and back on foreground (audio continues, video decode stops) Platform UR-040 Done (pending device verification)
IR-026 Android picture-in-picture: auto-enter on user-leave-hint via enterPictureInPictureMode, only while a local video surface is actively rendering (never for audio-only playback, menu/library browsing, or remote/cast sessions — enforced by the native canEnterPip guard, re-checked at leave time); aspect-ratio sizing; a play/pause RemoteAction that reflects live player play/pause state (updated whenever playback state changes, not only on button press); WebView hide/restore on mode change Platform UR-041 Done
IR-027 Jellyfin /System/Info/Public reachability probe used as an offline→online recovery detector API UR-043 Done
IR-028 Jellyfin/LMS SyncGroups API client (list, create, join, unsync, dissolve sync groups) API UR-046 Done
IR-029 Android ConnectivityManager/NetworkCapabilities transport probe with a NetworkCallback change subscription, surfaced to the frontend via the AndroidNetworkType JS bridge and the jellytau-network-changed WebView event (requires ACCESS_NETWORK_STATE) Platform UR-053 Done (pending device verification)

2.2 Jellyfin API Requirements

API endpoints and data contracts required for Jellyfin integration.

ID Requirement Endpoint Category Traces To Status
JA-001 Server connection and discovery System UR-009 Done
JA-002 User authentication (username/password) Users UR-009, UR-012 Done
JA-003 Get user library views UserViews UR-007 Done
JA-004 Get library items (paginated) Items UR-007 Done
JA-005 Get item details and metadata Items UR-007 Done
JA-006 Search across libraries Items UR-008 Done
JA-007 Get playback info and stream URL MediaInfo UR-003, UR-004 Done
JA-008 Get available subtitles for item MediaInfo UR-020 Done
JA-009 Get available audio tracks for item MediaInfo UR-021 Done
JA-010 Report playback start Sessions UR-025 Done
JA-011 Report playback progress (periodic) Sessions UR-025 Done
JA-012 Report playback stopped Sessions UR-025 Done
JA-013 Get resume position for item UserData UR-019 Done
JA-014 Get "Next Up" items Shows UR-023 Done
JA-015 Get "Continue Watching" items Items UR-023 Done
JA-016 Get recently added items Items UR-024 Done
JA-017 Mark item as favorite UserData UR-017 Done
JA-018 Remove item from favorites UserData UR-017 Done
JA-019 Get/create/update playlists Playlists UR-014 Done
JA-020 Add/remove items from playlist Playlists UR-014 Done
JA-021 Get active sessions list Sessions UR-010 Done
JA-022 Send playback commands to remote session (play/pause/stop) Sessions UR-010 Done
JA-023 Send seek command to remote session Sessions UR-010 Done
JA-024 Send next/previous track commands to remote session Sessions UR-010 Done
JA-025 Play specific item on remote session Sessions UR-010 Done
JA-026 Send volume/mute commands to remote session Sessions UR-010 Done
JA-027 Get transcoding options MediaInfo UR-022 Planned
JA-028 Get image/artwork URLs Images UR-007 Done
JA-029 Get cast/crew for item (actors, directors) Items UR-035 Done
JA-030 Get person details and filmography Persons UR-036 Done
JA-031 Get items by person (actor/director filmography) Items UR-036 Done
JA-032 Get audio-only stream URL for a video item (selected audio-stream index) MediaInfo UR-040 Done

2.3 Development Requirements

Internal architecture, components, and application logic.

ID Requirement Category Traces To Status
DR-001 Player state machine (idle, loading, playing, paused, seeking, error) Player UR-005 Done
DR-002 MediaItem struct tracking source, location, duration, metadata Player UR-003, UR-004 Done
DR-003 Source-agnostic media abstraction (Remote, Local, DirectUrl) Player UR-002, UR-011 Done
DR-004 PlayerBackend trait for platform-agnostic playback Player UR-003, UR-004 Done
DR-005 Queue manager with shuffle, repeat, history Player UR-005, UR-015 Done
DR-006 Audio pre-caching for seamless track transitions Player UR-004 Planned
DR-007 Library browsing screens (grid view, search, filters) UI UR-007, UR-008 Done
DR-008 Album/Series detail view with track listing UI UR-007 Done
DR-009 Audio player UI (mini player, full screen) UI UR-005 Done
DR-010 Video player UI (fullscreen, controls overlay) UI UR-003, UR-005 Done
DR-011 Search bar with cross-library search UI UR-008 Done
DR-012 Local database for media metadata cache Storage UR-002 Done
DR-013 Repository pattern for online/offline data access Storage UR-002 Done
DR-014 Offline mutation queue for sync-back operations Storage UR-002, UR-014, UR-017 Done
DR-015 Download manager with queue and progress tracking Storage UR-011, UR-018 Done
DR-016 Thumbnail caching and sync with server Storage UR-007 Done
DR-017 "Manage Downloads" screen for local media management UI UR-013 Done
DR-018 Download buttons on library/album/player screens UI UR-011, UR-018 Done
DR-019 Playlist creation and editing UI UI UR-014 Done
DR-020 Queue management UI (add, remove, reorder) UI UR-015 Done
DR-021 Like/favorite functionality on media items UI UR-017 Done
DR-022 Resume position tracking and restoration on play Player UR-019 Done
DR-023 Subtitle selection UI in video player UI UR-020 Done
DR-024 Audio track selection UI in video player UI UR-021 Done
DR-025 Quality/transcoding settings UI UI UR-022 Planned
DR-026 "Continue Watching" / "Next Up" home section UI UR-023 Done
DR-027 "Recently Added" home section UI UR-024 Done
DR-028 Playback progress sync service (periodic reporting) Player UR-025 Done
DR-029 Sleep timer with roller UI, time/track/episode modes, and auto-stop (audio + video players) Player UR-026 Done
DR-049 Auto-play episode limit (configurable max episodes per session) Player UR-023 Done
DR-050 Reusable scroll picker (roller) component UI UR-026 Done
DR-030 Equalizer UI with presets and custom bands UI UR-027 Done
DR-031 Clickable artist/album links in now playing view UI UR-028 Done
DR-032 List view option for library browsing (albums, artists) UI UR-029 Done
DR-033 Genre browsing screen with quick filters UI UR-030 Done
DR-034 Crossfade engine with configurable duration (0-12s) Player UR-031 Not implemented (blocked on MPV: single-stream audio chain; acrossfade needs 2 inputs — see docs/specs/playback-backend-unification.md)
DR-035 Gapless playback between sequential tracks Player UR-032 Done (Linux only)
DR-036 Volume normalization with preset levels (Loud/Normal/Quiet) Player UR-033 Done (Linux only)
DR-037 Remote session browser and control UI UI UR-010 Done
DR-038 Home screen with hero banner carousel (featured/continue watching) UI UR-034 Done
DR-039 Home screen horizontal carousels (recently added, recommendations) UI UR-034, UR-024 Done
DR-040 Cast/crew section on movie/show detail pages UI UR-035 Done
DR-041 Person/actor detail page with filmography grid UI UR-036 Done
DR-042 Video library grid with poster cards, year, and rating badges UI UR-037 Done
DR-043 Movie/show detail page with backdrop hero, synopsis, and metadata UI UR-038 Done
DR-044 Horizontal scrolling actor/cast row with profile images UI UR-035 Done
DR-045 Bottom navigation bar with Home, Library, Search buttons UI UR-039 Done
DR-046 Dedicated search page with input and results UI UR-039 Done
DR-047 Next episode auto-play popup with configurable countdown and episode limit Player UR-023 Done
DR-048 Video settings (auto-play toggle, countdown duration, episode limit) Settings UR-023, UR-026 Done
DR-051 Background-audio toggle button in the video player controls (suppresses auto-PiP while enabled) UI UR-040 Done (pending device verification)
DR-052 Background-audio handoff state machine: on background/lock tear down the WebView <video>/HLS decode and start native audio-only playback at the current position; on foreground return position and resume <video>; exactly one audio source active at every transition (no dual audio) Player UR-040 Done (pending device verification)
DR-053 PictureInPictureManager: canEnterPip gate (local video surface actively rendering — false for audio, browsing, and remote/cast), aspect-ratio clamp, a RemoteAction play/pause receiver whose icon reflects live player state (refreshed on every playback-state change while in PiP, not only on button press), WebView hide/restore, surface re-fit on exit; plus the AndroidPictureInPicture JS bridge and the PiP button (shown only when PiP is supported) in the video player UI UR-041 Done
DR-054 Auth manager and session lifecycle: connect-to-server, login, Quick Connect verification poll (start/stop), session get/set, background session verifier, re-authenticate, logout Auth UR-042 Done
DR-055 ConnectivityMonitor deriving reachability from real repository traffic, with online/offline state, mark-reachable/unreachable reporting, and a probe-based recovery poller active only while offline Connectivity UR-043 Done
DR-056 Download pinning (pin/unpin/is-pinned) that excludes an item from smart-cache eviction Storage UR-044 Done
DR-057 Smart cache manager: album-affinity tracking, queue-lookahead pre-cache, storage-limit enforcement, config, stats, and recommendations Storage UR-045 Done
DR-058 Remote sync-group control (LMS SyncGroups): list, create, unsync a player, dissolve a group Player UR-046 Done
DR-059 Playback-mode transfer state machine: get/set current mode, transferring guard, transfer-to-remote / transfer-to-local, remote session status Player UR-010 Done
DR-060 Multi-server store and active-account selection: save/get/delete server, save/get user, set/get active user (per-server), active-session resolution Storage UR-047 Partial (store done; server-switcher UI pending)
DR-061 Episode Focus View: episode hero followed immediately by the "More Episodes" strip — a forward-biased window (~3 before / ~6 after) around the current episode, spanning season boundaries in series order, with the current episode present and badged, per-card resume progress and watched state, and click-to-swap focus (no playback) UI UR-048 Done
DR-062 Detail-page section ordering: continuation content precedes discovery content — Episode Focus View renders hero → episode strip → cast → similar; Series renders hero → seasons/episodes → cast → similar UI UR-048 Done
DR-063 Search scope taxonomy owned by Rust: SearchScope (All / Music / Movies / TV) crosses IPC as an opaque enum and SearchScope::item_types() expands it to Jellyfin item types, resolved once in repository_search before the cache and server paths diverge so online and offline filter identically; All expands to no filter rather than the union of the other scopes (which would drop People and folders). The frontend maps the originating route to a scope (resolveSearchScope, presentation) and never names an item type for search Backend UR-049 Implemented
DR-064 Scope chip row rendered under the search bar on both the search page and the in-library header search: preselected from context, horizontally scrollable, re-runs the search preserving the query on change UI UR-049 Implemented
DR-065 Thread SearchOptions.includeItemTypes through library.search() so the global/header search honours scope (backend online + offline paths already support it) UI UR-049 Implemented
DR-066 Persisted search result group order with a drag-and-drop settings list, keyboard-accessible reordering, a shipped default (see DR-091 for the current group set and order), and empty-group omission Settings UR-050 Implemented
DR-067 SearchResults renders groups in the user-configured order rather than hardcoded markup order, without altering intra-group ranking UI UR-050 Implemented
DR-068 Library card shape by media type: 1:1 square for music (circular mask for artists), 2:3 poster for movies/series/seasons, 16:9 for episodes and collection folders UI UR-051 Done
DR-069 Responsive library grid (2/3/4/5/6 columns across base→xl) with two-line truncated card text and artwork-overlay progress/watched state UI UR-051 Done
DR-070 Global persisted grid/list view preference honoured by browse pages, suppressed for ordinal content (album tracks, season episodes) UI UR-051, UR-029 Partial (persisted store + page-header toggle; no settings entry)
DR-075 Shared AccountMenu component: identity header (user + server), Downloads / Settings / Display entries, divider, Sign out last; anchored to the username/avatar trigger and identical on desktop and mobile UI UR-054 Done
DR-076 App shell exposes the header (and therefore the account menu) on every authenticated non-immersive route, including /, /search, and /downloads; only /player/* and /login remain chrome-free UI UR-054 Done
DR-077 Display section in Settings binding the existing persisted grid/list viewMode store, giving the preference a discoverable home Settings UR-054, UR-029 Done
DR-078 Catalog-visibility gate spanning the "Show all server media" toggle → set_show_server_catalogINCLUDE_CATALOG_BROWSE → the synced-catalog UNION branch of offline get_items. Visibility resolves to serverReachable || showServerCatalog, so offline with the toggle off lists downloaded/local media only Storage UR-052, UR-002 Done
DR-079 isConnected derives from backend-reported server reachability alone; navigator.onLine is advisory and may only trigger a recheck, never force or clear the offline state (a reachable LAN server while the browser reports offline, and an unreachable server on a live link, must both resolve correctly) Connectivity UR-052, UR-043 Done
DR-080 With the catalog-browse gate off, an empty offline get_items result is authoritative "no downloads here" and must be returned as-is; the hybrid repository must not treat it as a cache miss and fall through to the server Storage UR-052, UR-013 Done
DR-074 WiFi-only download gate: NetworkState/NetworkType transport model reported from the platform via set_network_state, checked in pump_download_queue before starting any pending row (cellular/metered/unknown fail closed, WiFi and Ethernet require NOT_METERED); blocked rows stay pending and re-pump on network change, with a waitingForNetwork event driving the "Waiting for WiFi" notice. Also wires the previously inert Smart Caching / Queue Pre-caching / WiFi Only settings toggles to CacheConfig Downloads UR-053 Done (pending device verification)
DR-081 /downloads split into a default Downloaded browse view and a secondary Transfers activity view, with a view switch and a Transfers badge shown only while transfers are active UI UR-055 Done
DR-082 Offline-scoped browse entry point in the repository client: browse downloaded content only (offline repository get_items/get_libraries — downloaded items plus their containers) independent of server reachability, without merging server catalog Storage UR-055 Done
DR-083 Downloaded browse reuses library grids, cards, and detail pages via the offline-scoped source; omits libraries/containers with no downloaded content; badges partially- vs fully-downloaded containers; play uses the local file; remove available at item/album/season/series level UI UR-055 Done
DR-084 Transfers view renders only in-flight rows (downloading/queued/paused/failed/waiting-for-WiFi) with Pause/Resume/Cancel/Retry; completed transfers leave the view and appear in Downloaded UI UR-055 Done
DR-085 Per-item on-disk size: stat downloaded files, aggregate to album/season/series subtotals and a device total, format in consistent rounded human units; surface size on cards and detail pages, the device total on the Downloaded surface, and a reclaim figure in the remove confirmation Downloads UR-056 Done
DR-086 Settings page persists each control on change via per-group writers (playerSetAudioSettings / playerSetVideoSettings / updateCacheConfig) rather than a batch Save action; slider controls persist on change (pointer release) not each input tick; no Save button, saving, or saveMessage state Settings UR-057 Done
DR-087 MediaCard gains an onLongPress prop with pointer-based long-press detection (~500 ms hold, cancelled on >10 px move so carousel scroll is unaffected, trailing click suppressed); home carousels wire tap→detail/focus routing and long-press→confirm→player; episode taps route to /library/<seriesId>?episode=<id>; the bare-episode detail page links to its parent series/season UI UR-058 Done
DR-088 Skip-to-next-episode marks the outgoing episode played (markAsPlayed) instead of reporting a stop position, and arms a one-shot suppression consumed by the player's stop handler so VideoPlayer's post-navigation unmount stop report cannot overwrite the 100% progress with the partial position UI UR-059 Done
DR-089 Continue Watching suppresses resume entries superseded by Next Up: an in-progress episode whose series has a next-up entry strictly later in series order (season, then episode) is dropped from the Home and TV rows; movies, series without a next-up entry, and items with unknown/mixed episode ordering are always kept UI UR-059 Done
DR-090 Relevance ranking in Rust (domain/search_rank.rs): results sort by match position (prefix → word-start → mid-word substring → no name match) then by media kind (containers before their contents), stably so the backend's own relevance breaks ties. Applied in repository_search to both the instant cache result and the merged cache+server union, so the list does not reshuffle when server results land Backend UR-060 Done
DR-091 Search result groups split TV into separate Shows and Episodes groups and add a People group (default order: Shows → Episodes → Movies → Songs → Albums → Artists → People); a stored tvShows order from before the split expands in place to shows+episodes so an upgrading user keeps their arrangement UI UR-060 Done
DR-092 Video tap gestures resolve in tapGestures.ts (pure, unit-tested) rather than inline in VideoPlayer.svelte: registerTap classifies each tap and the component acts on it immediately — togglePlayPause for a first tap, or seek (+30 s right / 10 s left) plus a re-toggle for a second tap inside DOUBLE_TAP_WINDOW_MS (300 ms). A consumed pair resets the state, and a swipe forgets the tap. The deferral this originally used was removed in DR-098, which also covers suppressing the compatibility click the browser synthesizes after a touch tap. resolveSeekTarget converts the delta to the absolute position the facade requires, clamped per DR-095 and chained off a still-in-flight pendingSeekTarget so back-to-back skips accumulate instead of all resolving against a not-yet-updated position UI UR-061 Done
DR-094 Frontend boundary tripwire (scripts/check-frontend-boundary.sh) detects Jellyfin item-type array literals anywhere in src/ rather than only inline at an includeItemTypes: query site, so a category→type mapping cannot evade the check by being assigned to a named const (the evasion that let the scoped-search leak pass CI); requires two adjacent type literals so single-type presentation and item.type === inspection stay legal, and caps the allowlist to force taxonomy into Rust instead of accumulating exceptions Tooling - Done
DR-098 Video tap gestures act immediately — no deferral, no timer, and only first/second taps exist. A first tap toggles play/pause; a second tap inside DOUBLE_TAP_WINDOW_MS seeks and toggles again, so the two toggles cancel and a double tap preserves the play state (playing → jump and keep playing; paused → jump and stay paused). This replaces a design that deferred the first tap behind a 300 ms timer so a second tap could cancel it: the timer cleared its own handle before invoking the toggle, which reopened the tapTimeout !== null guard in handleVideoClick meant to suppress the compatibility click Android's WebView synthesizes after a touch — the late click then toggled a second time, producing a pause/unpause loop (long-press was unaffected, which is what identified the tap path). Click suppression no longer depends on the timer: handleVideoClick ignores detail === 0 and any click within TOUCH_CLICK_SUPPRESS_MS of a touch tap. A swipe undoes the touchstart toggle exactly once (latched on swipeGestureActive) so brightness swipes never change play state. Click suppression is shared by every click target layered over the video via isSynthesizedTouchClick, not just the <video>: pausing renders a full-screen play-overlay button, so the synthesized click lands on that and an unguarded handler there resumed immediately — pausing appeared impossible while unpausing worked, because unpausing removes the overlay UI UR-061 Done
DR-099 The video seek bar is usable by touch. Two Android-only defects made dragging or tapping it move the thumb without moving playback. (a) Gesture hijack: the container-level gesture layer skips touchstart on a control (DR-098) but kept handling touchmove, so a seek-bar drag was measured against the previous gesture's start point — a huge bogus vertical delta that read as a brightness swipe, dimmed the screen to the 0.3 floor, and fired a spurious play/pause "correction" mid-drag. A gesture is now latched at touchstart (playerGestureActive) and touchmove ignores anything not latched, since re-checking the move target cannot recover a start point that was never recorded. (b) Commit signal: the seek was committed only from change, which Android's WebView does not reliably fire for a touch interaction on a range input — the thumb moved to the tapped position and no seek ever ran. touchend/mouseup now commit as well; input arms a one-shot latch so whichever release signal arrives first commits and the other is a no-op. seekRelative shares the same commitSeek entry point instead of fabricating a synthetic change event UI UR-005, UR-061 Done
DR-097 Transport authority (play/pause/toggle) lives in Rust for webview-rendered media, not just native. The controller tracks the state the HTML5 element reports (html5_playing, fed by report_html5_state, which now stores rather than only re-emitting); play/pause/toggle_playback consult it and drive the element by emitting a ControlCommand that playerEvents.handleControlCommand executes against the active adapter. A stopped/idle report clears it so the native backend (MPV/ExoPlayer) regains authority for music. The frontend facade no longer short-circuits transport into the adapter: adapter.toggle() previously decided play-vs-pause by reading el.paused off the DOM, a value that flips transiently while an element buffers or settles a seek — so two intents ~150 ms apart read different values, performed opposing actions, and self-sustained a play/pause loop needing no further input (observed on Android with a fully-buffered readyState=4 networkState=1 element). Same "backend decides, adapter executes the primitive" split as player_seek_video Player UR-005 Done
DR-096 Html5PlayerAdapter.play() is resilient to stall recovery: an in-flight attempt is memoised so concurrent callers (UI plus hls.js gap-controller recovery) share one element.play() instead of stacking calls, and an AbortError ("play() request was interrupted by a call to pause()") is logged at debug rather than pushed to host.onError. The browser raises it whenever a pending play promise is superseded by a pause/seek/source change, which hls.js does routinely while nudging past a stall — reporting it surfaced a player error roughly once per second for the whole stall and left the UI stuck showing paused Player UR-005 Done
DR-095 Seek targets clamp strictly inside the media (clampSeekTarget, END_SEEK_MARGIN_SECONDS = 6 s ≈ one HLS segment) instead of to the exact duration. Landing on the duration makes hls.js request the segment whose start time lies past the end of the media (e.g. a 6330.324 s item → segment 1055 starting at 6336.33 s), which Jellyfin never produces; the fetch times out and hls.js' gap-controller stalls at the last buffered position, presenting as "unpausing or skipping bounces straight back to paused". Applied on both seek paths — the relative-skip resolveSeekTarget and the seek-bar drag, whose range input max is the duration itself — and floored at 0 so media shorter than the margin still seeks to the start UI UR-061 Done
DR-093 Traceability coverage gate derives its requirement denominators from requirements.md at run time rather than hardcoded literals: countDefinedRequirements counts an ID only where it leads a markdown table row (ignoring the "Traces To" column and prose) and deduplicates IDs listed both in the definition tables and in the §3 traceability matrix; computeCoverage reports the intersection of traced and defined IDs so an ID traced in code but absent from requirements.md is surfaced as orphaned instead of inflating the ratio past 100%. UT/IT test identifiers are excluded as a separate taxonomy. CI and bun run traces:coverage share this computation and fail on both a sub-threshold and an impossible >100% result Tooling - Done

3. Traceability Matrix

User Requirements to Software Requirements

User Req Integration Requirements Development Requirements
UR-001 IR-001, IR-002 -
UR-002 IR-013 DR-003, DR-012, DR-013, DR-014
UR-003 IR-003, IR-004, IR-011 DR-002, DR-004, DR-010
UR-004 IR-003, IR-004, IR-008, IR-011 DR-002, DR-004, DR-006
UR-005 - DR-001, DR-005, DR-009
UR-006 IR-005, IR-006, IR-007, IR-008 -
UR-007 IR-010 DR-007, DR-008, DR-016
UR-008 IR-010 DR-007, DR-011
UR-009 IR-009, IR-010, IR-011 -
UR-010 IR-012, IR-021 DR-037, DR-059
UR-011 IR-013 DR-003, DR-015, DR-018
UR-012 IR-009, IR-014 -
UR-013 IR-013 DR-017
UR-014 IR-010 DR-014, DR-019
UR-015 - DR-005, DR-020
UR-016 - -
UR-017 - DR-014, DR-021
UR-018 IR-013 DR-015, DR-018
UR-019 IR-015 DR-022
UR-020 IR-016, IR-018 DR-023
UR-021 IR-016, IR-019 DR-024
UR-022 IR-017 DR-025
UR-023 IR-010 DR-026, DR-047, DR-048, DR-049
UR-024 IR-010 DR-027
UR-025 IR-015 DR-028
UR-026 - DR-029, DR-048, DR-050
UR-027 IR-020 DR-030
UR-028 - DR-031
UR-029 - DR-032
UR-030 IR-010 DR-033
UR-031 - DR-034
UR-032 - DR-035
UR-033 - DR-036
UR-034 IR-010, IR-024 DR-038, DR-039
UR-035 IR-022, IR-023 DR-040, DR-044
UR-036 IR-022, IR-023 DR-041
UR-037 IR-010 DR-042
UR-038 IR-010 DR-043
UR-039 - DR-045, DR-046
UR-040 IR-025 DR-051, DR-052
UR-041 IR-026 DR-053
UR-042 IR-009, IR-014 DR-054
UR-043 IR-027 DR-055
UR-044 - DR-056
UR-045 - DR-057
UR-046 IR-028 DR-058
UR-047 IR-013 DR-060
UR-048 - DR-061, DR-062
UR-049 IR-010 DR-063, DR-064, DR-065
UR-050 - DR-066, DR-067
UR-051 - DR-068, DR-069, DR-070
UR-052 IR-027 DR-078, DR-079, DR-080
UR-053 IR-029 DR-074
UR-054 - DR-075, DR-076, DR-077
UR-055 - DR-081, DR-082, DR-083, DR-084
UR-056 - DR-085
UR-057 - DR-086
UR-058 - DR-087
UR-060 - DR-090, DR-091
UR-061 - DR-092

4. Test Traceability

Unit Tests to Software Requirements

Test ID Test Description Traces To Status
UT-001 Player state transitions DR-001 Pending
UT-002 MediaItem source URL resolution DR-002, DR-003 Pending
UT-003 Queue next/previous navigation DR-005 Pending
UT-004 Queue shuffle order generation DR-005 Pending
UT-005 Queue repeat mode behavior DR-005 Pending
UT-006 Jellyfin authentication flow IR-009 Pending
UT-007 Jellyfin library items parsing IR-010 Pending
UT-008 Repository pattern online/offline switching DR-013 Pending
UT-009 Offline mutation queue persistence DR-014 Pending
UT-010 Download queue management DR-015 Done
UT-011 Resume position storage and retrieval DR-022 Pending
UT-012 Sleep timer countdown logic DR-029 Pending
UT-013 Playback progress reporting throttling DR-028 Pending
UT-014 Database open and in-memory mode IR-013, DR-012 Done
UT-015 Database migrations run successfully IR-013, DR-012 Done
UT-016 All database tables created IR-013, DR-012 Done
UT-017 FTS5 search table created IR-013, DR-012 Done
UT-018 Server CRUD operations IR-013, DR-012 Done
UT-019 User CRUD operations IR-013, DR-012 Done
UT-020 Cascade delete server removes users IR-013, DR-012 Done
UT-021 Item insert and FTS search IR-013, DR-012 Done
UT-022 User data playback position storage IR-013, DR-012, DR-022 Done
UT-023 Sync queue operations IR-013, DR-014 Done
UT-024 Downloads table operations IR-013, DR-015 Done
UT-025 Migrations are idempotent IR-013, DR-012 Done
UT-026 NullBackend volume default value DR-004 Done
UT-027 NullBackend set volume DR-004 Done
UT-028 NullBackend volume clamping (high/low) DR-004 Done
UT-029 NullBackend volume boundary values DR-004 Done
UT-030 PlayerController volume default DR-004, DR-009 Done
UT-031 PlayerController set volume DR-004, DR-009 Done
UT-032 PlayerController muted default DR-004, DR-009 Done
UT-033 PlayerController volume delegates to backend DR-004, DR-009 Done
UT-034 Download event serialization roundtrip DR-015 Done
UT-035 Download event completed serialization DR-015 Done
UT-036 Download event failed serialization DR-015 Done
UT-037 Download worker exponential backoff DR-015 Done
UT-038 Download worker error retryable check DR-015 Done
UT-039 Download manager creation DR-015 Done
UT-040 Download manager set max concurrent DR-015 Done
UT-041 Download info serialization DR-015 Done
UT-042 Download command filename sanitization DR-015, DR-018 Done
UT-043 Download command filename extension preservation DR-015, DR-018 Done
UT-044 Offline item serialization DR-017 Done
UT-045 Smart cache default config DR-015 Done
UT-046 Smart cache album affinity tracking DR-015 Done
UT-047 Smart cache queue precache config DR-015 Done
UT-048 Smart cache storage limit check DR-015 Done
UT-049 Playlist create (offline) DR-019, JA-019 Done
UT-050 Playlist delete (offline) DR-019, JA-019 Done
UT-051 Playlist rename (offline) DR-019, JA-019 Done
UT-052 Playlist get items (offline) DR-019, JA-019 Done
UT-053 Playlist add items (offline) DR-019, JA-020 Done
UT-054 Playlist remove items (offline) DR-019, JA-020 Done
UT-055 Playlist reorder items (offline) DR-019, JA-020 Done
UT-056 Playlist entry serialization DR-019, JA-019 Done
UT-057 Playlist Tauri command param naming (camelCase) DR-019, JA-019, JA-020 Done
UT-058 Playlist repository client methods DR-019, JA-019, JA-020 Done
UT-059 Audio-only stream URL builder for a video item (selected audio-stream index) JA-032, DR-052 Pending
UT-060 Background-audio handoff state machine (background→audio, foreground→video; no dual audio) DR-052 Pending
UT-061 Background-audio Tauri command param naming (camelCase) DR-052 Pending
UT-062 setBackgroundAudioEnabled reports whether the native bridge was actually reached (missing bridge, stale proxy, throwing method) so a dead bridge cannot look armed UR-040, IR-025, DR-051 Done
UT-067 Offline get_items gates the synced-catalog UNION on the catalog-browse flag (downloads only when off, full catalog when on) DR-078 Done
UT-068 Catalog visibility resolves to serverReachable || showServerCatalog, and is pushed to the backend on every change of either input DR-078, DR-079 Done
UT-069 isConnected follows backend reachability alone: false when the server is unreachable on a live link, true for a reachable server while navigator.onLine is false DR-079 Done
UT-070 Hybrid get_items returns an empty offline result as-is when the catalog-browse gate is off, without querying the server DR-080 Done
UT-066 WiFi-only download gate: cellular and metered WiFi blocked, unmetered WiFi/Ethernet allowed, unknown/none fail closed, desktop default ungated; plus the frontend network reporter (transport reporting, change subscription, teardown, fail-open queries) DR-074 Done
UT-071 Byte-size formatter: zero/negative/non-finite → "0 B"; decimal unit thresholds; 23 significant-figure banding; trailing-zero trimming; largest-unit cap DR-085 Done
UT-072 Downloaded-only browse returns a downloaded leaf and its container, filtered to the requested album parent; a non-downloaded sibling is omitted DR-082, DR-083 Done
UT-073 An empty downloaded-only browse is authoritative — no rows, no error — regardless of the catalog-browse flag DR-082 Done
UT-074 Only libraries with downloaded content are listed; an empty one is omitted DR-082 Done
UT-075 Disk usage reports a leaf's own size, a container's summed descendants, and reconciles the device total with the sum of leaves DR-085 Done
UT-076 Downloaded library browse lists album containers, not their individual tracks; drilling into the album returns the tracks DR-082, DR-083 Done
UT-077 Downloaded TV library browse lists the series, not seasons/episodes; drilling returns the season then the episode DR-082, DR-083 Done
UT-078 A downloaded leaf with no cached container (e.g. a movie) still surfaces at the library level DR-082, DR-083 Done
UT-079 Each EQ preset returns a 10-band gain curve within range; Flat is all zeros; Bass Boost lifts lows and leaves highs flat DR-030 Done
UT-080 with_equalizer_normalised clamps out-of-range gains and forces the band vector to exactly 10 entries (pad short, truncate long) DR-030 Done
UT-081 Old persisted AudioSettings JSON without EQ fields loads as disabled + flat DR-030 Done
UT-082 EQ fields serialize as camelCase (equalizerEnabled/equalizerBands) and round-trip DR-030 Done
UT-083 EQ filter entries are empty when disabled or when the curve is flat (clears the af filter) IR-020 Done
UT-084 Enabled EQ builds one peaking equalizer per non-zero band at the right frequency and gain inside a single lavfi chain IR-020 Done
UT-085 A first tap resolves to togglePlayPause immediately — no deferral and no timer DR-092, DR-098 Done
UT-086 A second tap inside the window seeks (+30 s right half, 10 s left half) with the matching feedback side and re-toggles play/pause, so the two toggles cancel and the play state is unchanged by a double tap DR-092, DR-098 Done
UT-087 A tap after the window, and the tap following a consumed pair, are each fresh first taps that toggle (there is no third-tap case); repeated double taps keep seeking; cancel() makes the next tap a first tap so an interpreted swipe cannot seek DR-092, DR-098 Done
UT-088 resolveSeekTarget applies the delta to the reported position, clamps into [0, duration - END_SEEK_MARGIN_SECONDS], chains off an in-flight pending target so rapid skips accumulate, and ignores that target once the player reports past it DR-092, DR-095 Done
UT-089 A touch drag on the video seek bar seeks to the dragged position, never toggles play/pause, and never alters brightness — the container gesture layer stays out of a control drag entirely DR-098, DR-099 Done
UT-090 The seek bar commits its seek on touchend even when the engine never fires change, and commits exactly once when both signals arrive DR-099 Done
UT-091 Transport intents (play/pause/toggle) reach the backend even while a video adapter is registered, and never call the adapter's own play/pause/toggle — the webview must not decide play-vs-pause from the DOM DR-097 Done

Integration Tests

Test ID Test Description Traces To Status
IT-001 End-to-end authentication with Jellyfin server IR-009, UR-009 Pending
IT-002 Library browsing and item loading IR-010, UR-007 Pending
IT-003 Audio playback via libmpv IR-003, UR-004 Pending
IT-004 Video playback via libmpv IR-003, UR-003 Pending
IT-005 MPRIS lockscreen controls on Linux IR-005, UR-006 Pending
IT-006 Offline mode with local database IR-013, UR-002 Pending
IT-007 Media download and local playback DR-015, UR-011 Pending
IT-008 Subtitle track selection via libmpv IR-018, UR-020 Pending
IT-009 Audio track selection via libmpv IR-019, UR-021 Pending
IT-010 Playback progress sync to Jellyfin IR-015, UR-025 Pending
IT-011 Resume playback from server position IR-015, UR-019 Pending
IT-012 Equalizer bands via libmpv IR-020, UR-027 Pending
IT-013 Background-audio handoff on Android: background/lock continues audio via native service and stops video decode; foreground resumes video at position IR-025, UR-040 Pending
IT-016 Offline library listing end-to-end: with the server unreachable, a library page lists only downloaded media with the toggle off, and additionally reveals greyed-out cached catalog entries with the toggle on UR-052, DR-078, DR-079, DR-080 Done
IT-017 A download queued from a greyed-out offline catalog entry persists and is resolved and started on reconnect UR-052, UR-011 Done

5. Technical Debt

Linux Keyring Integration Workaround

Issue: The keyring-rs crate (v3.x) has issues with retrieving credentials from the Linux Secret Service API, despite successfully saving them.

Symptoms:

  • Credentials are saved to the system keyring successfully (verified with secret-tool search)
  • Retrieval via the keyring-rs library fails with NoEntry error
  • Session restoration fails on app restart even though credentials exist

Root Cause: The keyring-rs library's Linux backend doesn't correctly retrieve entries from the Secret Service that it previously stored. This appears to be a bug in how the library interfaces with the Secret Service D-Bus API.

Current Workaround: We bypass the keyring-rs library on Linux and use direct system calls to secret-tool:

  • Save: secret-tool store --label <label> service <service> username <username>
  • Retrieve: secret-tool lookup service <service> username <username>
  • Delete: secret-tool clear service <service> username <username>

Implementation: See src-tauri/src/credentials.rs for the Linux-specific secret-tool save/get/delete paths.

Future Fix:

  • Monitor keyring-rs for bug fixes in future versions
  • Consider alternative secure storage libraries
  • Test if newer versions of keyring-rs (v4.x+) resolve the issue
  • Once fixed, remove the Linux-specific workaround and use the cross-platform keyring-rs API

Impact:

  • Low - The workaround is functionally equivalent to proper keyring integration
  • Credentials are stored securely in the system keyring
  • Session restoration works correctly
  • Only affects Linux; macOS and Windows use the standard keyring-rs implementation

Dependencies:

  • Requires secret-tool to be installed on Linux systems (part of libsecret-tools package)
  • Already available on most Linux distributions by default

Platform Playback Backend Parity (Linux vs Android)

Issue: The Linux (MPV) and Android (ExoPlayer) playback backends have diverged in feature implementation and architecture patterns.

Symptoms:

  • Audio settings (crossfade, gapless playback, volume normalization) work on Linux but not on Android
  • Position update frequency differs between platforms (Linux: 250ms polling, Android: on-demand callbacks)
  • Thread safety models differ (Linux: Arc<Mutex<>>, Android: global OnceLock statics)

Root Cause: The PlayerBackend trait defines optional audio settings methods with default empty implementations. The Linux MpvBackend overrides these with full MPV property commands, but ExoPlayerBackend uses the defaults.

Affected Files:

Feature Parity Matrix:

Feature Linux (MPV) Android (ExoPlayer) Status
Basic playback Parity
Volume control Parity
Seek Parity
Crossfade Not implemented (blocked on MPV)
Gapless playback ⚠️ Implemented, pending on-device verification
Volume normalization ⚠️ Implemented (LoudnessEnhancer — gain stage, approximate vs MPV's dynaudnorm), pending on-device verification
Equalizer (10-band) ⚠️ Implemented (resampled onto device bands), pending on-device verification
Position updates 250ms On-demand Inconsistent

Status (see docs/specs/android-audio-settings-parity.md):

  1. set_audio_settings() implemented in ExoPlayerBackend (JSON over JNI)
  2. Gapless via ExoPlayer's pauseAtEndOfMediaItems
  3. Volume normalization via LoudnessEnhancer
  4. Equalizer via android.media.audiofx.Equalizer, canonical 10 bands resampled onto the device's band centres
  5. Not yet verified on a physical device — the EQ/normalization effects depend on device-specific AudioEffect availability and band layouts
  6. Flip the trait's set_audio_settings default from Ok(()) to Err(not_implemented()) so a backend that omits it fails loudly instead of silently reporting success. Deferred until (5) confirms the Android path works
  7. Standardize position update frequency across platforms

Crossfade is deliberately absent: it is unimplemented on every platform and architecturally blocked on MPV, so building it on Android alone would invert the parity gap. (The previously suggested ConcatenatingMediaSource is also deprecated in current Media3.)

Impact:

  • Medium - Android users lack audio enhancement features advertised in requirements
  • User experience differs between platforms
  • UR-032 (Gapless), UR-033 (Normalization) and UR-027 (Equalizer) are now implemented on Android as well as Linux, pending on-device verification
  • UR-031 (Crossfade) works nowhere — see DR-034

Traces To: IR-004, UR-031, UR-032, UR-033, DR-034, DR-035, DR-036


Frontend Playback Code Duplication

Issue: Playback control handlers and state derivations are duplicated between AudioPlayer.svelte and MiniPlayer.svelte.

Symptoms:

  • Identical try-catch wrapped handler functions in both components (~44 lines duplicated)
  • Same $derived state merging logic for local/remote playback in both components
  • Position conversion (ticks ↔ seconds) scattered across multiple files

Affected Files:

Duplicated Code:

// These handlers are identical in both AudioPlayer and MiniPlayer:
handlePlayPause(), handleNext(), handlePrevious(),
handleToggleShuffle(), handleCycleRepeat(), handleVolumeChange()

// These derived states use identical logic:
displayMedia, displayIsPlaying, displayPosition, displayDuration

Future Fix:

  1. Create src/lib/utils/playbackUnits.ts:

    export const TICKS_PER_SECOND = 10_000_000;
    export const secondsToTicks = (s: number) => Math.floor(s * TICKS_PER_SECOND);
    export const ticksToSeconds = (t: number) => t / TICKS_PER_SECOND;
    
  2. Create src/lib/composables/useMergedPlaybackState.svelte.ts:

    • Export displayMedia, displayIsPlaying, displayPosition, displayDuration
    • Single source of truth for merged local/remote state
  3. Simplify handler wrappers using a utility:

    export const withErrorHandler = (fn: () => Promise<void>, context: string) =>
      async () => { try { await fn(); } catch (e) { console.error(`${context}:`, e); } };
    

Impact:

  • Low - Code works correctly but violates DRY principle
  • Maintenance burden when logic needs to change
  • Risk of handlers diverging over time

Traces To: DR-009