Video on Android/Linux renders in a webview <video> element, and the frontend facade short-circuited play/pause/toggle straight into the adapter whenever one was registered. Html5PlayerAdapter.toggle() then decided play-vs-pause by reading el.paused off the DOM, so the Rust controller never saw the intent and could not serialise competing ones. el.paused flips transiently while an element buffers or settles a seek. Two intents ~150ms apart therefore read *different* values and performed *opposing* actions — one playing, one pausing — which self-sustained a play/pause loop that needed no further input. On device this showed up as a fully healthy element (readyState=4, networkState=1, not seeking, not buffering, not ended) pausing itself roughly once a second, so unpausing or skipping ahead bounced straight back to paused. The root cause was that Rust held NO state for webview-rendered media: report_html5_state only re-emitted its argument, despite the comment above it claiming the controller was the single source of truth. It had nothing to decide a toggle from. Now report_html5_state tracks the reported state, and play/pause/toggle consult it and drive the element by emitting a ControlCommand — the same "backend decides, adapter executes the primitive" split player_seek_video already uses. A stopped/idle report clears the tracking so MPV/ExoPlayer regain authority for music playback. Tests cover the loop signature directly (repeated toggles must alternate, never repeat or oppose) plus a guard that one intent yields exactly one ControlCommand — which matters on Windows, where the backend is itself webview-based and could otherwise be driven twice.
592 lines
55 KiB
Markdown
592 lines
55 KiB
Markdown
# 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/](architecture/). For development workflows, see the
|
||
[README](../README.md) and [scripts/README.md](../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](ux-flows.md)) | 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](ux-flows.md)) | 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](ux-flows.md)) | 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](ux-flows.md)) | 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](ux-flows.md)) | 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](ux-flows.md)) | 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](ux-flows.md)) | 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](ux-flows.md)) | 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](ux-flows.md) and [§5B.1](ux-flows.md)) | 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. Because a double tap starts as a single tap, the single-tap play/pause is held back until the double-tap window has passed, so skipping never also pauses the video; 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_catalog` → `INCLUDE_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` returns `pending` for a first tap — the component defers `togglePlayPause` behind a `DOUBLE_TAP_WINDOW_MS` (300 ms) timer that a second tap cancels — or `seek` (+30 s right / −10 s left) for a second tap inside the window; a consumed second tap resets the state so a third tap starts fresh, and a swipe cancels the pending tap. The compatibility `click` the browser synthesizes after a touch tap is filtered in `handleVideoClick` so it cannot bypass the deferral. `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-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; 2–3 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 `pending`, not an immediate play/pause, and becomes `togglePlayPause` only once the double-tap window has elapsed | DR-092 | Done |
|
||
| UT-086 | A second tap inside the window seeks (+30 s right half, −10 s left half) with the matching feedback side, and clears the deferred play/pause so a double tap never pauses | DR-092 | Done |
|
||
| UT-087 | A tap after the window, and a third tap after a consumed double tap, each start a fresh pending tap; repeated double taps keep seeking; `cancel()` drops a pending tap so a swipe cannot pause | DR-092 | Done |
|
||
| UT-088 | `resolveSeekTarget` applies the delta to the reported position, clamps to `[0, duration]`, chains off an in-flight pending target so rapid skips accumulate, and ignores that target once the player reports past it | DR-092 | 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](../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**:
|
||
- [src-tauri/src/player/backend.rs](../src-tauri/src/player/backend.rs) - Trait with default empty implementations
|
||
- [src-tauri/src/player/mpv_backend.rs](../src-tauri/src/player/mpv_backend.rs) - Full audio settings support
|
||
- [src-tauri/src/player/android/mod.rs](../src-tauri/src/player/android/mod.rs) - Missing audio settings implementation
|
||
|
||
**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**:
|
||
- [src/lib/components/player/AudioPlayer.svelte](../src/lib/components/player/AudioPlayer.svelte) - Duplicate handlers
|
||
- [src/lib/components/player/MiniPlayer.svelte](../src/lib/components/player/MiniPlayer.svelte) - Duplicate handlers
|
||
- [src/lib/services/playbackControl.ts](../src/lib/services/playbackControl.ts) - Position conversion
|
||
- [src/lib/stores/playbackMode.ts](../src/lib/stores/playbackMode.ts) - Position conversion
|
||
- [src/lib/services/playbackReporting.ts](../src/lib/services/playbackReporting.ts) - Position conversion
|
||
|
||
**Duplicated Code**:
|
||
```typescript
|
||
// 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`:
|
||
```typescript
|
||
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:
|
||
```typescript
|
||
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
|