From cb79a376b392fe5b94354b6f1657eb74d0ef4e7a Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Tue, 28 Jul 2026 23:03:31 +0200 Subject: [PATCH] feat(android): implement audio settings (EQ, normalization, gapless) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExoPlayerBackend was the only backend not overriding the PlayerBackend trait's set_audio_settings/audio_settings defaults, so the Settings > Audio controls rendered on Android and silently did nothing — the default returns Ok(()) while applying nothing, so the failure was invisible. Rust owns what the values are (canonical 10-band ISO layout, preset curves, normalization presets); Kotlin owns when the AudioEffect objects exist, since that needs the live audio session id. - settings.rs: audio_settings_jni_payload() sanitises (crossfade clamped, band vector normalised) before serialising, so a malformed vector cannot reach the Kotlin parser. JSON rather than a wide JNI signature, matching how load() already passes subtitles — adding a field will not change the signature. - ExoPlayerBackend: set_audio_settings/audio_settings over JNI; ExoPlayerState gains the first command-side field (settings are pushed out, never reported). - JellyTauPlayer.kt: Equalizer, LoudnessEnhancer, and gapless via pauseAtEndOfMediaItems. Three details that are easy to get wrong: - Effects re-attach on onAudioSessionIdChanged. ExoPlayer rebuilds its audio sink on a format change, which invalidates effects bound to the old session; without this the EQ silently stops applying mid-queue. - All effect work is posted to mainHandler rather than run inline. AudioEffect construction from a player callback can re-enter the player and deadlock — the same shape as the AutoplayDecision lock-scrutinee bug. - Device equalizers expose a device-dependent band count (commonly 5) at fixed centres, so the canonical 10 bands are resampled by nearest centre frequency. resampleBands() is a pure @JvmStatic function so that mapping is testable without a device. Normalization is approximate, not parity: LoudnessEnhancer is a gain stage, not a true EBU R128 normalizer like MPV's dynaudnorm. Recorded as such rather than claimed as equivalent. Crossfade is deliberately excluded — unimplemented on every platform and blocked on mpv, so building it on Android alone would invert the parity gap. Tests written first and observed failing (cannot find function audio_settings_jni_payload) before the implementation: the payload contract is pinned by tests because a serde rename would otherwise silently break the Kotlin parser. Not yet verified on a physical device — AudioEffect availability and band layouts are device-specific. Requirements matrix marks these rows accordingly, and flipping the trait default to Err(not_implemented()) is deferred until that verification lands. --- docs/traceability.md | 1472 +++++++++-------- .../jellytau/player/JellyTauPlayer.kt | 192 +++ src-tauri/src/player/android/mod.rs | 56 + src-tauri/src/settings.rs | 70 + 4 files changed, 1099 insertions(+), 691 deletions(-) diff --git a/docs/traceability.md b/docs/traceability.md index e4b99217..45dea836 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -1,11 +1,11 @@ # Code Traceability Matrix -**Generated:** 7/28/2026, 1:33:13 AM +**Generated:** 7/28/2026, 10:39:16 PM ## Summary - **Total Files Scanned:** 299 -- **Total TRACES Found:** 313 +- **Total TRACES Found:** 318 - **Requirements Covered:** - User Requirements (UR): 58 - Integration Requirements (IR): 15 @@ -194,12 +194,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 2 file(s) -- **File:** [`src/lib/utils/backgroundAudio.ts`](src/lib/utils/backgroundAudio.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` - **File:** [`src/lib/utils/backgroundAudio.test.ts`](src/lib/utils/backgroundAudio.test.ts#L7) - **Line:** 7 - **Context:** `Unknown` +- **File:** [`src/lib/utils/backgroundAudio.ts`](src/lib/utils/backgroundAudio.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` ### IR-026 @@ -313,12 +313,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/player/adapters/index.ts`](src/lib/player/adapters/index.ts#L11) - **Line:** 11 - **Context:** `Unknown` -- **File:** [`src/lib/player/adapters/nativeAdapter.ts`](src/lib/player/adapters/nativeAdapter.ts#L16) - - **Line:** 16 - - **Context:** `Unknown` - **File:** [`src/lib/player/adapters/webviewAudioAdapter.ts`](src/lib/player/adapters/webviewAudioAdapter.ts#L18) - **Line:** 18 - **Context:** `Unknown` +- **File:** [`src/lib/player/adapters/nativeAdapter.ts`](src/lib/player/adapters/nativeAdapter.ts#L24) + - **Line:** 24 + - **Context:** `Unknown` - **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L39) - **Line:** 39 - **Context:** `pub fn playback_failed>(message: S) -> Self {` @@ -774,15 +774,15 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/player/adapters/html5Adapter.ts`](src/lib/player/adapters/html5Adapter.ts#L19) - **Line:** 19 - **Context:** `Unknown` -- **File:** [`src/lib/player/adapters/nativeAdapter.ts`](src/lib/player/adapters/nativeAdapter.ts#L16) - - **Line:** 16 - - **Context:** `Unknown` - **File:** [`src/lib/player/adapters/rustReportHost.ts`](src/lib/player/adapters/rustReportHost.ts#L13) - **Line:** 13 - **Context:** `Unknown` - **File:** [`src/lib/player/adapters/types.ts`](src/lib/player/adapters/types.ts#L15) - **Line:** 15 - **Context:** `Unknown` +- **File:** [`src/lib/player/adapters/nativeAdapter.ts`](src/lib/player/adapters/nativeAdapter.ts#L24) + - **Line:** 24 + - **Context:** `Unknown` - **File:** [`src-tauri/src/player/events.rs`](src-tauri/src/player/events.rs#L6) - **Line:** 6 - **Context:** `Unknown` @@ -830,7 +830,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### DR-030 -**Locations:** 13 file(s) +**Locations:** 18 file(s) - **File:** [`src/routes/settings/+page.svelte`](src/routes/settings/+page.svelte#L1) - **Line:** 1 @@ -841,6 +841,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1773) - **Line:** 1773 - **Context:** `export type DownloadsResponse = { downloads: DownloadInfo[]; stats: Do...` +- **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L545) + - **Line:** 545 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L583) + - **Line:** 583 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) - **Line:** 3 - **Context:** `Unknown` @@ -859,17 +865,26 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L140) - **Line:** 140 - **Context:** `pub fn with_crossfade_clamped(mut self) -> Self {` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L200) - - **Line:** 200 +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L193) + - **Line:** 193 - **Context:** `Unknown` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L229) - - **Line:** 229 +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L210) + - **Line:** 210 - **Context:** `Unknown` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L256) - - **Line:** 256 +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L234) + - **Line:** 234 - **Context:** `Unknown` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L267) - - **Line:** 267 +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L270) + - **Line:** 270 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L299) + - **Line:** 299 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L326) + - **Line:** 326 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L337) + - **Line:** 337 - **Context:** `Unknown` ### DR-032 @@ -904,19 +919,40 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### DR-035 -**Locations:** 2 file(s) +**Locations:** 7 file(s) +- **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L545) + - **Line:** 545 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L583) + - **Line:** 583 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) - **Line:** 3 - **Context:** `Unknown` - **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L193) + - **Line:** 193 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L210) + - **Line:** 210 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L234) + - **Line:** 234 + - **Context:** `Unknown` ### DR-036 -**Locations:** 7 file(s) +**Locations:** 12 file(s) +- **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L545) + - **Line:** 545 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L583) + - **Line:** 583 + - **Context:** `Unknown` - **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L584) - **Line:** 584 - **Context:** `Unknown` @@ -938,6 +974,15 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L193) + - **Line:** 193 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L210) + - **Line:** 210 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L234) + - **Line:** 234 + - **Context:** `Unknown` ### DR-037 @@ -1180,12 +1225,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src/lib/utils/backgroundAudio.ts`](src/lib/utils/backgroundAudio.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` - **File:** [`src/lib/utils/backgroundAudio.test.ts`](src/lib/utils/backgroundAudio.test.ts#L7) - **Line:** 7 - **Context:** `Unknown` +- **File:** [`src/lib/utils/backgroundAudio.ts`](src/lib/utils/backgroundAudio.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` ### DR-052 @@ -1829,12 +1874,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 2 file(s) -- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` - **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` ### JA-001 @@ -2205,9 +2250,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/player/adapters/index.ts`](src/lib/player/adapters/index.ts#L11) - **Line:** 11 - **Context:** `Unknown` -- **File:** [`src/lib/player/adapters/nativeAdapter.ts`](src/lib/player/adapters/nativeAdapter.ts#L16) - - **Line:** 16 - - **Context:** `Unknown` - **File:** [`src/lib/player/adapters/rustReportHost.ts`](src/lib/player/adapters/rustReportHost.ts#L13) - **Line:** 13 - **Context:** `Unknown` @@ -2217,6 +2259,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/player/adapters/webviewAudioAdapter.ts`](src/lib/player/adapters/webviewAudioAdapter.ts#L18) - **Line:** 18 - **Context:** `Unknown` +- **File:** [`src/lib/player/adapters/nativeAdapter.ts`](src/lib/player/adapters/nativeAdapter.ts#L24) + - **Line:** 24 + - **Context:** `Unknown` - **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) - **Line:** 8 - **Context:** `Unknown` @@ -2323,12 +2368,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/components/player/videoFit.ts`](src/lib/components/player/videoFit.ts#L7) - **Line:** 7 - **Context:** `Unknown` -- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` - **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` - **File:** [`src/lib/components/BottomUi.svelte`](src/lib/components/BottomUi.svelte#L16) - **Line:** 16 - **Context:** `Unknown` @@ -2410,9 +2455,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/player/adapters/html5Adapter.ts`](src/lib/player/adapters/html5Adapter.ts#L19) - **Line:** 19 - **Context:** `Unknown` -- **File:** [`src/lib/player/adapters/nativeAdapter.ts`](src/lib/player/adapters/nativeAdapter.ts#L16) - - **Line:** 16 - - **Context:** `Unknown` - **File:** [`src/lib/player/adapters/rustReportHost.ts`](src/lib/player/adapters/rustReportHost.ts#L13) - **Line:** 13 - **Context:** `Unknown` @@ -2422,6 +2464,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/player/adapters/webviewAudioAdapter.ts`](src/lib/player/adapters/webviewAudioAdapter.ts#L18) - **Line:** 18 - **Context:** `Unknown` +- **File:** [`src/lib/player/adapters/nativeAdapter.ts`](src/lib/player/adapters/nativeAdapter.ts#L24) + - **Line:** 24 + - **Context:** `Unknown` - **File:** [`src/lib/player/index.ts`](src/lib/player/index.ts#L15) - **Line:** 15 - **Context:** `Unknown` @@ -2533,17 +2578,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 1 - **Context:** `Unknown` -### UR-008 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/components/library/GenericMediaListPage.searchEvent.test.ts`](src/lib/components/library/GenericMediaListPage.searchEvent.test.ts#L12) - - **Line:** 12 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` - ### UR-009 **Locations:** 15 file(s) @@ -2594,56 +2628,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 1 - **Context:** `Unknown` -### UR-010 - -**Locations:** 15 file(s) - -- **File:** [`src/lib/components/sessions/SessionCard.svelte`](src/lib/components/sessions/SessionCard.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/components/sessions/SessionsList.svelte`](src/lib/components/sessions/SessionsList.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/components/sessions/RemoteControls.svelte`](src/lib/components/sessions/RemoteControls.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/components/sessions/CastButton.svelte`](src/lib/components/sessions/CastButton.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/components/sessions/SessionPickerModal.svelte`](src/lib/components/sessions/SessionPickerModal.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/sessions.test.ts`](src/lib/stores/sessions.test.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/sessions.ts`](src/lib/stores/sessions.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/lmsSync.ts`](src/lib/stores/lmsSync.ts#L12) - - **Line:** 12 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/playbackMode.ts`](src/lib/stores/playbackMode.ts#L9) - - **Line:** 9 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/playbackMode.test.ts`](src/lib/stores/playbackMode.test.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/playback_mode.rs`](src-tauri/src/commands/playback_mode.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/sessions.rs`](src-tauri/src/commands/sessions.rs#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/session_poller/mod.rs`](src-tauri/src/session_poller/mod.rs#L6) - - **Line:** 6 - - **Context:** `Unknown` - ### UR-011 **Locations:** 11 file(s) @@ -2682,92 +2666,13 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 441 - **Context:** `Unknown` -### UR-012 +### UR-014 -**Locations:** 9 file(s) +**Locations:** 1 file(s) -- **File:** [`src/lib/stores/auth.test.ts`](src/lib/stores/auth.test.ts#L4) +- **File:** [`src-tauri/src/commands/playlist.rs`](src-tauri/src/commands/playlist.rs#L4) - **Line:** 4 - **Context:** `Unknown` -- **File:** [`src/lib/stores/auth.ts`](src/lib/stores/auth.ts#L6) - - **Line:** 6 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/auth.ts`](src/lib/stores/auth.ts#L293) - - **Line:** 293 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/auth.ts`](src/lib/stores/auth.ts#L445) - - **Line:** 445 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/mod.rs`](src-tauri/src/commands/mod.rs#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/storage/db_service.rs`](src-tauri/src/storage/db_service.rs#L311) - - **Line:** 311 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/storage/mod.rs`](src-tauri/src/storage/mod.rs#L157) - - **Line:** 157 - - **Context:** `pub fn file_size(&self) -> Option {` -- **File:** [`src-tauri/src/credentials.rs`](src-tauri/src/credentials.rs#L10) - - **Line:** 10 - - **Context:** `Unknown` - -### UR-013 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` - -### UR-015 - -**Locations:** 13 file(s) - -- **File:** [`src/lib/stores/queue.test.ts`](src/lib/stores/queue.test.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L7) - - **Line:** 7 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L105) - - **Line:** 105 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L110) - - **Line:** 110 - - **Context:** `async function next() {` -- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L115) - - **Line:** 115 - - **Context:** `async function previous() {` -- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L120) - - **Line:** 120 - - **Context:** `async function skipTo(index: number) {` -- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L125) - - **Line:** 125 - - **Context:** `async function toggleShuffle() {` -- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L130) - - **Line:** 130 - - **Context:** `async function cycleRepeat() {` -- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L135) - - **Line:** 135 - - **Context:** `async function removeFromQueue(index: number) {` -- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L140) - - **Line:** 140 - - **Context:** `async function moveInQueue(fromIndex: number, toIndex: number) {` -- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L20) - - **Line:** 20 - - **Context:** `pub enum RepeatMode {` -- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) - - **Line:** 535 - - **Context:** `pub enum AddPosition {` -- **File:** [`src-tauri/src/commands/player/queue.rs`](src-tauri/src/commands/player/queue.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` ### UR-017 @@ -2801,20 +2706,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 5 - **Context:** `Unknown` -### UR-018 - -**Locations:** 3 file(s) - -- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L2027) - - **Line:** 2027 - - **Context:** `Unknown` - ### UR-019 **Locations:** 12 file(s) @@ -2856,6 +2747,43 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 157 - **Context:** `pub fn file_size(&self) -> Option {` +### UR-020 + +**Locations:** 4 file(s) + +- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/player/adapters/html5Adapter.ts`](src/lib/player/adapters/html5Adapter.ts#L19) + - **Line:** 19 + - **Context:** `Unknown` +- **File:** [`src/lib/player/adapters/types.ts`](src/lib/player/adapters/types.ts#L15) + - **Line:** 15 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### UR-021 + +**Locations:** 5 file(s) + +- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/player/adapters/html5Adapter.ts`](src/lib/player/adapters/html5Adapter.ts#L19) + - **Line:** 19 + - **Context:** `Unknown` +- **File:** [`src/lib/player/adapters/types.ts`](src/lib/player/adapters/types.ts#L15) + - **Line:** 15 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/storage/series_prefs.rs`](src-tauri/src/commands/storage/series_prefs.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` + ### UR-023 **Locations:** 21 file(s) @@ -2924,70 +2852,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 1 - **Context:** `Unknown` -### UR-024 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/stores/home.ts`](src/lib/stores/home.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/catalog.rs`](src-tauri/src/commands/catalog.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - -### UR-025 - -**Locations:** 16 file(s) - -- **File:** [`src/lib/utils/validation.test.ts`](src/lib/utils/validation.test.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` -- **File:** [`src/lib/services/playbackReporting.test.ts`](src/lib/services/playbackReporting.test.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` -- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` -- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L21) - - **Line:** 21 - - **Context:** `Unknown` -- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L89) - - **Line:** 89 - - **Context:** `Unknown` -- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L122) - - **Line:** 122 - - **Context:** `Unknown` -- **File:** [`src/lib/services/playerEvents.ts`](src/lib/services/playerEvents.ts#L156) - - **Line:** 156 - - **Context:** `Unknown` -- **File:** [`src/lib/services/syncService.ts`](src/lib/services/syncService.ts#L7) - - **Line:** 7 - - **Context:** `Unknown` -- **File:** [`src/lib/services/syncService.ts`](src/lib/services/syncService.ts#L64) - - **Line:** 64 - - **Context:** `Unknown` -- **File:** [`src/lib/services/skipReporting.ts`](src/lib/services/skipReporting.ts#L14) - - **Line:** 14 - - **Context:** `Unknown` -- **File:** [`src/lib/services/skipReporting.test.ts`](src/lib/services/skipReporting.test.ts#L9) - - **Line:** 9 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/mod.rs`](src-tauri/src/commands/mod.rs#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/playback_reporting.rs`](src-tauri/src/commands/playback_reporting.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/sync.rs`](src-tauri/src/commands/sync.rs#L5) - - **Line:** 5 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/storage/mod.rs`](src-tauri/src/storage/mod.rs#L157) - - **Line:** 157 - - **Context:** `pub fn file_size(&self) -> Option {` - ### UR-026 **Locations:** 17 file(s) @@ -3046,7 +2910,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### UR-027 -**Locations:** 18 file(s) +**Locations:** 23 file(s) - **File:** [`src/routes/settings/+page.svelte`](src/routes/settings/+page.svelte#L1) - **Line:** 1 @@ -3057,6 +2921,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1773) - **Line:** 1773 - **Context:** `export type DownloadsResponse = { downloads: DownloadInfo[]; stats: Do...` +- **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L545) + - **Line:** 545 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L583) + - **Line:** 583 + - **Context:** `Unknown` - **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L584) - **Line:** 584 - **Context:** `Unknown` @@ -3090,17 +2960,26 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L140) - **Line:** 140 - **Context:** `pub fn with_crossfade_clamped(mut self) -> Self {` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L200) - - **Line:** 200 +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L193) + - **Line:** 193 - **Context:** `Unknown` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L229) - - **Line:** 229 +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L210) + - **Line:** 210 - **Context:** `Unknown` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L256) - - **Line:** 256 +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L234) + - **Line:** 234 - **Context:** `Unknown` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L267) - - **Line:** 267 +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L270) + - **Line:** 270 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L299) + - **Line:** 299 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L326) + - **Line:** 326 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L337) + - **Line:** 337 - **Context:** `Unknown` ### UR-028 @@ -3134,17 +3013,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 2 - **Context:** `Unknown` -### UR-030 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/components/library/GenericMediaListPage.svelte`](src/lib/components/library/GenericMediaListPage.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` - ### UR-034 **Locations:** 10 file(s) @@ -3200,20 +3068,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 4 - **Context:** `Unknown` -### UR-036 - -**Locations:** 3 file(s) - -- **File:** [`src/lib/components/library/PersonDetailView.svelte`](src/lib/components/library/PersonDetailView.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/storage/people.rs`](src-tauri/src/commands/storage/people.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L4) - - **Line:** 4 - - **Context:** `Unknown` - ### UR-038 **Locations:** 1 file(s) @@ -3222,14 +3076,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 1 - **Context:** `Unknown` -### UR-039 - -**Locations:** 1 file(s) - -- **File:** [`src/lib/components/BottomNav.svelte`](src/lib/components/BottomNav.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` - ### UR-040 **Locations:** 22 file(s) @@ -3261,12 +3107,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src/lib/utils/backgroundAudio.ts`](src/lib/utils/backgroundAudio.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` - **File:** [`src/lib/utils/backgroundAudio.test.ts`](src/lib/utils/backgroundAudio.test.ts#L7) - **Line:** 7 - **Context:** `Unknown` +- **File:** [`src/lib/utils/backgroundAudio.ts`](src/lib/utils/backgroundAudio.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` - **File:** [`src/lib/player/backgroundAudio.commands.test.ts`](src/lib/player/backgroundAudio.commands.test.ts#L7) - **Line:** 7 - **Context:** `Unknown` @@ -3309,17 +3155,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 4 - **Context:** `Unknown` -### UR-043 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/connectivity.rs`](src-tauri/src/commands/connectivity.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - ### UR-048 **Locations:** 3 file(s) @@ -3378,78 +3213,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 93 - **Context:** `Unknown` -### UR-050 - -**Locations:** 7 file(s) - -- **File:** [`src/lib/components/search/SearchResults.svelte`](src/lib/components/search/SearchResults.svelte#L9) - - **Line:** 9 - - **Context:** `Unknown` -- **File:** [`src/lib/components/settings/SearchGroupOrderList.svelte`](src/lib/components/settings/SearchGroupOrderList.svelte#L8) - - **Line:** 8 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/searchGroupOrder.ts`](src/lib/stores/searchGroupOrder.ts#L6) - - **Line:** 6 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/searchGroupOrder.test.ts`](src/lib/stores/searchGroupOrder.test.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` -- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L9) - - **Line:** 9 - - **Context:** `Unknown` -- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L202) - - **Line:** 202 - - **Context:** `Unknown` -- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L252) - - **Line:** 252 - - **Context:** `Unknown` - -### UR-051 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/components/library/LibraryGrid.svelte`](src/lib/components/library/LibraryGrid.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` - -### UR-052 - -**Locations:** 10 file(s) - -- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L245) - - **Line:** 245 - - **Context:** `Unknown` -- **File:** [`src/lib/services/offlineCatalog.ts`](src/lib/services/offlineCatalog.ts#L14) - - **Line:** 14 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/catalog.rs`](src-tauri/src/commands/catalog.rs#L441) - - **Line:** 441 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L24) - - **Line:** 24 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L43) - - **Line:** 43 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2396) - - **Line:** 2396 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L8) - - **Line:** 8 - - **Context:** `Unknown` - ### UR-053 **Locations:** 17 file(s) @@ -3506,20 +3269,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 47 - **Context:** `Unknown` -### UR-054 - -**Locations:** 3 file(s) - -- **File:** [`src/lib/components/account/AccountMenu.svelte`](src/lib/components/account/AccountMenu.svelte#L7) - - **Line:** 7 - - **Context:** `Unknown` -- **File:** [`src/lib/components/AppHeader.svelte`](src/lib/components/AppHeader.svelte#L7) - - **Line:** 7 - - **Context:** `Unknown` -- **File:** [`src/lib/utils/layoutShell.ts`](src/lib/utils/layoutShell.ts#L80) - - **Line:** 80 - - **Context:** `Unknown` - ### UR-055 **Locations:** 27 file(s) @@ -3687,6 +3436,22 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 37 - **Context:** `Unknown` +### IT-003 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` + +### IT-004 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` + ### IT-013 **Locations:** 4 file(s) @@ -3704,266 +3469,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 675 - **Context:** `Unknown` -### UT-010 - -**Locations:** 1 file(s) - -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` - -### UT-024 - -**Locations:** 1 file(s) - -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` - -### UT-026 - -**Locations:** 2 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L251) - - **Line:** 251 - - **Context:** `Unknown` - -### UT-028 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - -### UT-030 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - -### UT-032 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) - - **Line:** 245 - - **Context:** `Unknown` - -### UT-052 - -**Locations:** 1 file(s) - -- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L315) - - **Line:** 315 - - **Context:** `Unknown` - -### UT-060 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/components/player/backgroundAudioHandoff.test.ts`](src/lib/components/player/backgroundAudioHandoff.test.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` -- **File:** [`src/lib/components/player/backgroundAudioHandoff.ts`](src/lib/components/player/backgroundAudioHandoff.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` - -### UT-061 - -**Locations:** 9 file(s) - -- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L434) - - **Line:** 434 - - **Context:** `Unknown` -- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L451) - - **Line:** 451 - - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L38) - - **Line:** 38 - - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L51) - - **Line:** 51 - - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1319) - - **Line:** 1319 - - **Context:** `Unknown` -- **File:** [`src/lib/player/backgroundAudio.commands.test.ts`](src/lib/player/backgroundAudio.commands.test.ts#L7) - - **Line:** 7 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L583) - - **Line:** 583 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L675) - - **Line:** 675 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L505) - - **Line:** 505 - - **Context:** `Unknown` - -### UR-059 - -**Locations:** 6 file(s) - -- **File:** [`src/lib/stores/continueWatchingFilter.ts`](src/lib/stores/continueWatchingFilter.ts#L12) - - **Line:** 12 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/continueWatchingFilter.test.ts`](src/lib/stores/continueWatchingFilter.test.ts#L9) - - **Line:** 9 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/home.ts`](src/lib/stores/home.ts#L2) - - **Line:** 2 - - **Context:** `Unknown` -- **File:** [`src/lib/stores/tv.ts`](src/lib/stores/tv.ts#L3) - - **Line:** 3 - - **Context:** `Unknown` -- **File:** [`src/lib/services/skipReporting.ts`](src/lib/services/skipReporting.ts#L14) - - **Line:** 14 - - **Context:** `Unknown` -- **File:** [`src/lib/services/skipReporting.test.ts`](src/lib/services/skipReporting.test.ts#L9) - - **Line:** 9 - - **Context:** `Unknown` - -### UR-061 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` -- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` - -### UT-071 - -**Locations:** 1 file(s) - -- **File:** [`src/lib/utils/formatBytes.test.ts`](src/lib/utils/formatBytes.test.ts#L4) - - **Line:** 4 - - **Context:** `Unknown` - -### UT-085 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L704) - - **Line:** 704 - - **Context:** `Unknown` - -### UT-086 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L714) - - **Line:** 714 - - **Context:** `Unknown` - -### UT-087 - -**Locations:** 2 file(s) - -- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L759) - - **Line:** 759 - - **Context:** `Unknown` - -### UT-088 - -**Locations:** 1 file(s) - -- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) - - **Line:** 10 - - **Context:** `Unknown` - -### UR-014 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/commands/playlist.rs`](src-tauri/src/commands/playlist.rs#L4) - - **Line:** 4 - - **Context:** `Unknown` - -### UR-020 - -**Locations:** 4 file(s) - -- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/player/adapters/html5Adapter.ts`](src/lib/player/adapters/html5Adapter.ts#L19) - - **Line:** 19 - - **Context:** `Unknown` -- **File:** [`src/lib/player/adapters/types.ts`](src/lib/player/adapters/types.ts#L15) - - **Line:** 15 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - - **Line:** 1 - - **Context:** `Unknown` - -### UR-021 - -**Locations:** 5 file(s) - -- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src/lib/player/adapters/html5Adapter.ts`](src/lib/player/adapters/html5Adapter.ts#L19) - - **Line:** 19 - - **Context:** `Unknown` -- **File:** [`src/lib/player/adapters/types.ts`](src/lib/player/adapters/types.ts#L15) - - **Line:** 15 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) - - **Line:** 1 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/storage/series_prefs.rs`](src-tauri/src/commands/storage/series_prefs.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - -### UR-060 - -**Locations:** 3 file(s) - -- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L117) - - **Line:** 117 - - **Context:** `Unknown` -- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L183) - - **Line:** 183 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/domain/search_rank.rs`](src-tauri/src/domain/search_rank.rs#L106) - - **Line:** 106 - - **Context:** `Unknown` - -### IT-003 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) - - **Line:** 8 - - **Context:** `Unknown` - -### IT-004 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/mpv_backend_test.rs`](src-tauri/src/player/mpv_backend_test.rs#L8) - - **Line:** 8 - - **Context:** `Unknown` - ### IT-016 **Locations:** 1 file(s) @@ -4133,6 +3638,17 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 157 - **Context:** `pub fn file_size(&self) -> Option {` +### UT-026 + +**Locations:** 2 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L251) + - **Line:** 251 + - **Context:** `Unknown` + ### UT-027 **Locations:** 1 file(s) @@ -4141,7 +3657,15 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 245 - **Context:** `Unknown` -### UT-029 +### UT-028 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### UT-030 **Locations:** 1 file(s) @@ -4157,7 +3681,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 245 - **Context:** `Unknown` -### UT-033 +### UT-032 **Locations:** 1 file(s) @@ -4165,14 +3689,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 245 - **Context:** `Unknown` -### UT-042 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L2027) - - **Line:** 2027 - - **Context:** `Unknown` - ### UT-051 **Locations:** 1 file(s) @@ -4195,6 +3711,60 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 2150 - **Context:** `Unknown` +### UT-060 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/player/backgroundAudioHandoff.test.ts`](src/lib/components/player/backgroundAudioHandoff.test.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/components/player/backgroundAudioHandoff.ts`](src/lib/components/player/backgroundAudioHandoff.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` + +### UT-061 + +**Locations:** 9 file(s) + +- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L434) + - **Line:** 434 + - **Context:** `Unknown` +- **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L451) + - **Line:** 451 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L38) + - **Line:** 38 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L51) + - **Line:** 51 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1319) + - **Line:** 1319 + - **Context:** `Unknown` +- **File:** [`src/lib/player/backgroundAudio.commands.test.ts`](src/lib/player/backgroundAudio.commands.test.ts#L7) + - **Line:** 7 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L583) + - **Line:** 583 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L675) + - **Line:** 675 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L505) + - **Line:** 505 + - **Context:** `Unknown` + +### UR-061 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` + ### UT-062 **Locations:** 1 file(s) @@ -4203,14 +3773,211 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 7 - **Context:** `Unknown` -### UT-066 +### UT-067 **Locations:** 1 file(s) -- **File:** [`src/lib/services/networkType.test.ts`](src/lib/services/networkType.test.ts#L4) +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2396) + - **Line:** 2396 + - **Context:** `Unknown` + +### UT-071 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/utils/formatBytes.test.ts`](src/lib/utils/formatBytes.test.ts#L4) - **Line:** 4 - **Context:** `Unknown` +### UT-085 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L704) + - **Line:** 704 + - **Context:** `Unknown` + +### UT-086 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L714) + - **Line:** 714 + - **Context:** `Unknown` + +### UT-087 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L759) + - **Line:** 759 + - **Context:** `Unknown` + +### UT-088 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/components/player/tapGestures.ts`](src/lib/components/player/tapGestures.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` + +### UR-008 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/library/GenericMediaListPage.searchEvent.test.ts`](src/lib/components/library/GenericMediaListPage.searchEvent.test.ts#L12) + - **Line:** 12 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` + +### UR-010 + +**Locations:** 15 file(s) + +- **File:** [`src/lib/components/sessions/SessionCard.svelte`](src/lib/components/sessions/SessionCard.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/components/sessions/SessionsList.svelte`](src/lib/components/sessions/SessionsList.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/components/sessions/RemoteControls.svelte`](src/lib/components/sessions/RemoteControls.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/components/sessions/CastButton.svelte`](src/lib/components/sessions/CastButton.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/components/sessions/SessionPickerModal.svelte`](src/lib/components/sessions/SessionPickerModal.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/sessions.test.ts`](src/lib/stores/sessions.test.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/sessions.ts`](src/lib/stores/sessions.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/lmsSync.ts`](src/lib/stores/lmsSync.ts#L12) + - **Line:** 12 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/playbackMode.ts`](src/lib/stores/playbackMode.ts#L9) + - **Line:** 9 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/playbackMode.test.ts`](src/lib/stores/playbackMode.test.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/remote.rs`](src-tauri/src/commands/player/remote.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/playback_mode.rs`](src-tauri/src/commands/playback_mode.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/sessions.rs`](src-tauri/src/commands/sessions.rs#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/session_poller/mod.rs`](src-tauri/src/session_poller/mod.rs#L6) + - **Line:** 6 + - **Context:** `Unknown` + +### UR-012 + +**Locations:** 9 file(s) + +- **File:** [`src/lib/stores/auth.test.ts`](src/lib/stores/auth.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/auth.ts`](src/lib/stores/auth.ts#L6) + - **Line:** 6 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/auth.ts`](src/lib/stores/auth.ts#L293) + - **Line:** 293 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/auth.ts`](src/lib/stores/auth.ts#L445) + - **Line:** 445 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/mod.rs`](src-tauri/src/commands/mod.rs#L2) + - **Line:** 2 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/storage/db_service.rs`](src-tauri/src/storage/db_service.rs#L311) + - **Line:** 311 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/storage/mod.rs`](src-tauri/src/storage/mod.rs#L157) + - **Line:** 157 + - **Context:** `pub fn file_size(&self) -> Option {` +- **File:** [`src-tauri/src/credentials.rs`](src-tauri/src/credentials.rs#L10) + - **Line:** 10 + - **Context:** `Unknown` + +### UR-013 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` + +### UR-015 + +**Locations:** 13 file(s) + +- **File:** [`src/lib/stores/queue.test.ts`](src/lib/stores/queue.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L7) + - **Line:** 7 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L105) + - **Line:** 105 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L110) + - **Line:** 110 + - **Context:** `async function next() {` +- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L115) + - **Line:** 115 + - **Context:** `async function previous() {` +- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L120) + - **Line:** 120 + - **Context:** `async function skipTo(index: number) {` +- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L125) + - **Line:** 125 + - **Context:** `async function toggleShuffle() {` +- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L130) + - **Line:** 130 + - **Context:** `async function cycleRepeat() {` +- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L135) + - **Line:** 135 + - **Context:** `async function removeFromQueue(index: number) {` +- **File:** [`src/lib/stores/queue.ts`](src/lib/stores/queue.ts#L140) + - **Line:** 140 + - **Context:** `async function moveInQueue(fromIndex: number, toIndex: number) {` +- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L20) + - **Line:** 20 + - **Context:** `pub enum RepeatMode {` +- **File:** [`src-tauri/src/player/queue.rs`](src-tauri/src/player/queue.rs#L535) + - **Line:** 535 + - **Context:** `pub enum AddPosition {` +- **File:** [`src-tauri/src/commands/player/queue.rs`](src-tauri/src/commands/player/queue.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` + ### UR-016 **Locations:** 2 file(s) @@ -4222,32 +3989,131 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 251 - **Context:** `Unknown` -### UR-031 +### UR-018 + +**Locations:** 3 file(s) + +- **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L2027) + - **Line:** 2027 + - **Context:** `Unknown` + +### UR-024 **Locations:** 2 file(s) -- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) +- **File:** [`src/lib/stores/home.ts`](src/lib/stores/home.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/catalog.rs`](src-tauri/src/commands/catalog.rs#L3) - **Line:** 3 - **Context:** `Unknown` -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) + +### UR-025 + +**Locations:** 16 file(s) + +- **File:** [`src/lib/utils/validation.test.ts`](src/lib/utils/validation.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src/lib/services/playbackReporting.test.ts`](src/lib/services/playbackReporting.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L21) + - **Line:** 21 + - **Context:** `Unknown` +- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L89) + - **Line:** 89 + - **Context:** `Unknown` +- **File:** [`src/lib/services/playbackReporting.ts`](src/lib/services/playbackReporting.ts#L122) + - **Line:** 122 + - **Context:** `Unknown` +- **File:** [`src/lib/services/playerEvents.ts`](src/lib/services/playerEvents.ts#L156) + - **Line:** 156 + - **Context:** `Unknown` +- **File:** [`src/lib/services/syncService.ts`](src/lib/services/syncService.ts#L7) + - **Line:** 7 + - **Context:** `Unknown` +- **File:** [`src/lib/services/syncService.ts`](src/lib/services/syncService.ts#L64) + - **Line:** 64 + - **Context:** `Unknown` +- **File:** [`src/lib/services/skipReporting.ts`](src/lib/services/skipReporting.ts#L14) + - **Line:** 14 + - **Context:** `Unknown` +- **File:** [`src/lib/services/skipReporting.test.ts`](src/lib/services/skipReporting.test.ts#L9) + - **Line:** 9 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/storage/mod.rs`](src-tauri/src/commands/storage/mod.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/mod.rs`](src-tauri/src/commands/mod.rs#L2) + - **Line:** 2 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/playback_reporting.rs`](src-tauri/src/commands/playback_reporting.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/sync.rs`](src-tauri/src/commands/sync.rs#L5) + - **Line:** 5 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/storage/mod.rs`](src-tauri/src/storage/mod.rs#L157) + - **Line:** 157 + - **Context:** `pub fn file_size(&self) -> Option {` + +### UR-030 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/library/GenericMediaListPage.svelte`](src/lib/components/library/GenericMediaListPage.svelte#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` ### UR-032 -**Locations:** 2 file(s) +**Locations:** 7 file(s) +- **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L545) + - **Line:** 545 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L583) + - **Line:** 583 + - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) - **Line:** 3 - **Context:** `Unknown` - **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L193) + - **Line:** 193 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L210) + - **Line:** 210 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L234) + - **Line:** 234 + - **Context:** `Unknown` ### UR-033 -**Locations:** 7 file(s) +**Locations:** 12 file(s) +- **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L545) + - **Line:** 545 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L583) + - **Line:** 583 + - **Context:** `Unknown` - **File:** [`src-tauri/src/player/mpv_backend.rs`](src-tauri/src/player/mpv_backend.rs#L584) - **Line:** 584 - **Context:** `Unknown` @@ -4269,6 +4135,227 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) - **Line:** 1 - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L193) + - **Line:** 193 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L210) + - **Line:** 210 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L234) + - **Line:** 234 + - **Context:** `Unknown` + +### UR-036 + +**Locations:** 3 file(s) + +- **File:** [`src/lib/components/library/PersonDetailView.svelte`](src/lib/components/library/PersonDetailView.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/storage/people.rs`](src-tauri/src/commands/storage/people.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L4) + - **Line:** 4 + - **Context:** `Unknown` + +### UR-039 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/components/BottomNav.svelte`](src/lib/components/BottomNav.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### UR-042 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/commands/auth.rs`](src-tauri/src/commands/auth.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` + +### UR-043 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/connectivity.rs`](src-tauri/src/commands/connectivity.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` + +### UR-050 + +**Locations:** 7 file(s) + +- **File:** [`src/lib/components/search/SearchResults.svelte`](src/lib/components/search/SearchResults.svelte#L9) + - **Line:** 9 + - **Context:** `Unknown` +- **File:** [`src/lib/components/settings/SearchGroupOrderList.svelte`](src/lib/components/settings/SearchGroupOrderList.svelte#L8) + - **Line:** 8 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/searchGroupOrder.ts`](src/lib/stores/searchGroupOrder.ts#L6) + - **Line:** 6 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/searchGroupOrder.test.ts`](src/lib/stores/searchGroupOrder.test.ts#L4) + - **Line:** 4 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L9) + - **Line:** 9 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L202) + - **Line:** 202 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L252) + - **Line:** 252 + - **Context:** `Unknown` + +### UR-051 + +**Locations:** 2 file(s) + +- **File:** [`src/lib/components/library/LibraryGrid.svelte`](src/lib/components/library/LibraryGrid.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` + +### UR-052 + +**Locations:** 10 file(s) + +- **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L1) + - **Line:** 1 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L10) + - **Line:** 10 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L245) + - **Line:** 245 + - **Context:** `Unknown` +- **File:** [`src/lib/services/offlineCatalog.ts`](src/lib/services/offlineCatalog.ts#L14) + - **Line:** 14 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/catalog.rs`](src-tauri/src/commands/catalog.rs#L441) + - **Line:** 441 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L24) + - **Line:** 24 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L43) + - **Line:** 43 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2396) + - **Line:** 2396 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/hybrid.rs`](src-tauri/src/repository/hybrid.rs#L8) + - **Line:** 8 + - **Context:** `Unknown` + +### UR-054 + +**Locations:** 3 file(s) + +- **File:** [`src/lib/components/account/AccountMenu.svelte`](src/lib/components/account/AccountMenu.svelte#L7) + - **Line:** 7 + - **Context:** `Unknown` +- **File:** [`src/lib/components/AppHeader.svelte`](src/lib/components/AppHeader.svelte#L7) + - **Line:** 7 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/layoutShell.ts`](src/lib/utils/layoutShell.ts#L80) + - **Line:** 80 + - **Context:** `Unknown` + +### UT-010 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` + +### UT-024 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` + +### UT-052 + +**Locations:** 1 file(s) + +- **File:** [`src/lib/stores/downloads.test.ts`](src/lib/stores/downloads.test.ts#L315) + - **Line:** 315 + - **Context:** `Unknown` + +### UR-059 + +**Locations:** 6 file(s) + +- **File:** [`src/lib/stores/continueWatchingFilter.ts`](src/lib/stores/continueWatchingFilter.ts#L12) + - **Line:** 12 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/continueWatchingFilter.test.ts`](src/lib/stores/continueWatchingFilter.test.ts#L9) + - **Line:** 9 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/home.ts`](src/lib/stores/home.ts#L2) + - **Line:** 2 + - **Context:** `Unknown` +- **File:** [`src/lib/stores/tv.ts`](src/lib/stores/tv.ts#L3) + - **Line:** 3 + - **Context:** `Unknown` +- **File:** [`src/lib/services/skipReporting.ts`](src/lib/services/skipReporting.ts#L14) + - **Line:** 14 + - **Context:** `Unknown` +- **File:** [`src/lib/services/skipReporting.test.ts`](src/lib/services/skipReporting.test.ts#L9) + - **Line:** 9 + - **Context:** `Unknown` + +### UR-060 + +**Locations:** 3 file(s) + +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L117) + - **Line:** 117 + - **Context:** `Unknown` +- **File:** [`src/lib/utils/searchScope.ts`](src/lib/utils/searchScope.ts#L183) + - **Line:** 183 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/domain/search_rank.rs`](src-tauri/src/domain/search_rank.rs#L106) + - **Line:** 106 + - **Context:** `Unknown` + +### UT-029 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### UT-033 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](src-tauri/src/player/backend.rs#L245) + - **Line:** 245 + - **Context:** `Unknown` + +### UT-042 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/commands/download/mod.rs`](src-tauri/src/commands/download/mod.rs#L2027) + - **Line:** 2027 + - **Context:** `Unknown` ### UT-043 @@ -4286,12 +4373,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 136 - **Context:** `Unknown` -### UT-067 +### UT-066 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/repository/offline.rs`](src-tauri/src/repository/offline.rs#L2396) - - **Line:** 2396 +- **File:** [`src/lib/services/networkType.test.ts`](src/lib/services/networkType.test.ts#L4) + - **Line:** 4 - **Context:** `Unknown` ### UT-072 @@ -4354,32 +4441,32 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 1 file(s) -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L200) - - **Line:** 200 +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L270) + - **Line:** 270 - **Context:** `Unknown` ### UT-080 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L229) - - **Line:** 229 +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L299) + - **Line:** 299 - **Context:** `Unknown` ### UT-081 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L256) - - **Line:** 256 +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L326) + - **Line:** 326 - **Context:** `Unknown` ### UT-082 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L267) - - **Line:** 267 +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L337) + - **Line:** 337 - **Context:** `Unknown` ### UT-083 @@ -4406,13 +4493,16 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **Line:** 3 - **Context:** `Unknown` -### UR-042 +### UR-031 -**Locations:** 1 file(s) +**Locations:** 2 file(s) -- **File:** [`src-tauri/src/commands/auth.rs`](src-tauri/src/commands/auth.rs#L3) +- **File:** [`src-tauri/src/commands/player/settings.rs`](src-tauri/src/commands/player/settings.rs#L3) - **Line:** 3 - **Context:** `Unknown` +- **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) + - **Line:** 1 + - **Context:** `Unknown` ### UR-044 diff --git a/src-tauri/android/src/main/java/com/dtourolle/jellytau/player/JellyTauPlayer.kt b/src-tauri/android/src/main/java/com/dtourolle/jellytau/player/JellyTauPlayer.kt index 0c2acf1c..4efdf9ed 100644 --- a/src-tauri/android/src/main/java/com/dtourolle/jellytau/player/JellyTauPlayer.kt +++ b/src-tauri/android/src/main/java/com/dtourolle/jellytau/player/JellyTauPlayer.kt @@ -36,6 +36,53 @@ class JellyTauPlayer(private val appContext: Context) { /** Position update interval in milliseconds */ private const val POSITION_UPDATE_INTERVAL_MS = 250L + /** AudioEffect priority. Positive = higher priority than the default. */ + private const val EFFECT_PRIORITY = 1000 + + /** + * Canonical 10-band ISO centre frequencies (Hz), mirroring EQ_BANDS in + * settings.rs. Kept in sync deliberately: Rust owns the band layout, this + * is only the lookup table used to map those gains onto whatever bands + * the device's equalizer actually has. + */ + private val CANONICAL_BAND_CENTRES_HZ = + intArrayOf(31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000) + + /** + * Map canonical band gains onto a device's band centres by nearest + * centre frequency. + * + * Pure function so it can be unit-tested without a device — device band + * counts vary (commonly 5) and getting this wrong silently mis-shapes the + * EQ curve rather than failing. + * + * TRACES: UR-027 | DR-030 + */ + @JvmStatic + fun resampleBands( + canonicalGains: FloatArray, + canonicalCentresHz: IntArray, + deviceCentresHz: IntArray + ): FloatArray { + if (canonicalGains.isEmpty() || deviceCentresHz.isEmpty()) { + return FloatArray(deviceCentresHz.size) + } + val usable = minOf(canonicalGains.size, canonicalCentresHz.size) + return FloatArray(deviceCentresHz.size) { d -> + val target = deviceCentresHz[d] + var nearest = 0 + var bestDelta = Int.MAX_VALUE + for (c in 0 until usable) { + val delta = kotlin.math.abs(canonicalCentresHz[c] - target) + if (delta < bestDelta) { + bestDelta = delta + nearest = c + } + } + canonicalGains[nearest] + } + } + /** Singleton instance for JNI access */ @Volatile private var instance: JellyTauPlayer? = null @@ -135,6 +182,18 @@ class JellyTauPlayer(private val appContext: Context) { private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private var positionUpdateJob: Job? = null + /** Graphic EQ bound to the current audio session, or null if not attached. */ + private var equalizer: android.media.audiofx.Equalizer? = null + + /** Loudness/normalization effect bound to the current audio session. */ + private var loudnessEnhancer: android.media.audiofx.LoudnessEnhancer? = null + + /** + * Last settings pushed from Rust, replayed when the audio session is rebuilt. + * Held as the raw payload so re-application needs no second parse contract. + */ + private var lastAudioSettings: org.json.JSONObject? = null + /** Current media ID being played */ private var currentMediaId: String? = null @@ -334,6 +393,11 @@ class JellyTauPlayer(private val appContext: Context) { override fun onAudioSessionIdChanged(audioSessionId: Int) { android.util.Log.d("JellyTauPlayer", "▶▶▶ AUDIO SESSION ID CHANGED: $audioSessionId") + // ExoPlayer rebuilt its audio sink (e.g. on a format change), so + // effects bound to the old session are dead. Re-attach, or the EQ + // silently stops applying mid-queue. + releaseAudioEffects() + lastAudioSettings?.let { applyAudioEffects(it) } } }) } @@ -416,6 +480,133 @@ class JellyTauPlayer(private val appContext: Context) { } } + /** + * Apply audio settings pushed from Rust as JSON. + * + * Rust owns *what* the values are (band layout, preset curves, normalization + * presets); this owns *when* the Android AudioEffect objects exist, since + * that needs the live audio session id and must survive a sink rebuild. + * + * Posted to the main handler rather than run inline: AudioEffect construction + * from a player callback can re-enter the player and deadlock. + * + * TRACES: UR-027, UR-032, UR-033 | DR-030, DR-035, DR-036 + */ + fun setAudioSettings(json: String) { + mainHandler.post { + try { + val settings = org.json.JSONObject(json) + lastAudioSettings = settings + + // Gapless: ExoPlayer is gapless by default for compatible + // formats, so honouring the setting means disabling it when off. + exoPlayer.pauseAtEndOfMediaItems = !settings.optBoolean("gaplessPlayback", true) + + applyAudioEffects(settings) + } catch (e: Exception) { + android.util.Log.e("JellyTauPlayer", "Failed to apply audio settings", e) + } + } + } + + /** Attach/update the EQ and loudness effects for the current audio session. */ + private fun applyAudioEffects(settings: org.json.JSONObject) { + val sessionId = exoPlayer.audioSessionId + if (sessionId == C.AUDIO_SESSION_ID_UNSET) { + // No sink yet; onAudioSessionIdChanged will re-drive this. + return + } + + try { + applyEqualizer(sessionId, settings) + } catch (e: Exception) { + android.util.Log.e("JellyTauPlayer", "Equalizer unavailable on this device", e) + } + + try { + applyNormalization(sessionId, settings) + } catch (e: Exception) { + android.util.Log.e("JellyTauPlayer", "LoudnessEnhancer unavailable on this device", e) + } + } + + private fun applyEqualizer(sessionId: Int, settings: org.json.JSONObject) { + val enabled = settings.optBoolean("equalizerEnabled", false) + + if (!enabled) { + equalizer?.enabled = false + return + } + + val eq = equalizer ?: android.media.audiofx.Equalizer(EFFECT_PRIORITY, sessionId).also { + equalizer = it + } + + val bandsJson = settings.optJSONArray("equalizerBands") + val canonicalGains = FloatArray(bandsJson?.length() ?: 0) { i -> + bandsJson!!.optDouble(i, 0.0).toFloat() + } + if (canonicalGains.isEmpty()) { + eq.enabled = false + return + } + + // The device's band count/centres are device-dependent (commonly 5) and + // will not match our canonical 10-band ISO layout, so resample. + val deviceBandCount = eq.numberOfBands.toInt() + val deviceCentresHz = IntArray(deviceBandCount) { i -> + eq.getCenterFreq(i.toShort()) / 1000 // device reports milliHertz + } + val levelRange = eq.bandLevelRange // millibels, [min, max] + + val resampled = resampleBands(canonicalGains, CANONICAL_BAND_CENTRES_HZ, deviceCentresHz) + + for (i in 0 until deviceBandCount) { + val millibels = (resampled[i] * 100f) + .coerceIn(levelRange[0].toFloat(), levelRange[1].toFloat()) + eq.setBandLevel(i.toShort(), millibels.toInt().toShort()) + } + eq.enabled = true + } + + private fun applyNormalization(sessionId: Int, settings: org.json.JSONObject) { + val enabled = settings.optBoolean("normalizeVolume", false) + + if (!enabled) { + loudnessEnhancer?.enabled = false + return + } + + val enhancer = loudnessEnhancer + ?: android.media.audiofx.LoudnessEnhancer(sessionId).also { loudnessEnhancer = it } + + // Approximate parity with the Linux dynaudnorm path: LoudnessEnhancer is + // a gain stage, not a true EBU R128 normalizer, so these are relative + // offsets preserving the Loud > Normal > Quiet ordering. + val targetGainMb = when (settings.optString("volumeLevel", "normal")) { + "loud" -> 600 + "quiet" -> -600 + else -> 0 + } + enhancer.setTargetGain(targetGainMb) + enhancer.enabled = true + } + + private fun releaseAudioEffects() { + try { + equalizer?.release() + } catch (e: Exception) { + android.util.Log.w("JellyTauPlayer", "Equalizer release failed", e) + } + try { + loudnessEnhancer?.release() + } catch (e: Exception) { + android.util.Log.w("JellyTauPlayer", "LoudnessEnhancer release failed", e) + } + equalizer = null + loudnessEnhancer = null + } + /** * Get the current playback position in seconds. */ @@ -756,6 +947,7 @@ class JellyTauPlayer(private val appContext: Context) { mainHandler.post { stopPositionUpdates() coroutineScope.cancel() + releaseAudioEffects() exoPlayer.release() instance = null } diff --git a/src-tauri/src/player/android/mod.rs b/src-tauri/src/player/android/mod.rs index ff508324..a081f367 100644 --- a/src-tauri/src/player/android/mod.rs +++ b/src-tauri/src/player/android/mod.rs @@ -18,6 +18,7 @@ use super::events::{PlayerStatusEvent, SharedEventEmitter}; use super::media::{MediaItem, MediaType}; use super::state::PlayerState; use crate::playback_reporting::{EventThrottler, PlaybackOperation, PlaybackReporter}; +use crate::settings::{audio_settings_jni_payload, AudioSettings}; use crate::utils::conversions::seconds_to_ticks; /// Global reference to the JavaVM for JNI callbacks @@ -148,6 +149,10 @@ struct ExoPlayerState { volume: f32, is_loaded: bool, current_media: Option, + /// Last applied audio settings. Unlike the fields above (which JNI callbacks + /// push *in*), this is commanded *out* — audio settings are never reported + /// by the player, so this is the authoritative copy for `audio_settings()`. + audio_settings: AudioSettings, } impl ExoPlayerState { @@ -159,6 +164,7 @@ impl ExoPlayerState { volume: 1.0, is_loaded: false, current_media: None, + audio_settings: AudioSettings::default(), } } } @@ -529,6 +535,56 @@ impl PlayerBackend for ExoPlayerBackend { self.shared_state.lock_safe().volume } + /// Apply audio settings to ExoPlayer (equalizer, normalization, gapless). + /// + /// Sent as JSON rather than a wide JNI signature so new fields do not change + /// the method signature — the same approach `load()` uses for subtitles. The + /// Kotlin side owns the *mechanics* (attaching AudioEffects to the audio + /// session); the canonical band layout and preset curves stay in Rust. + /// + /// TRACES: UR-027, UR-032, UR-033 | DR-030, DR-035, DR-036 + fn set_audio_settings(&mut self, settings: &AudioSettings) -> Result<(), PlayerError> { + let json = audio_settings_jni_payload(settings).map_err(|e| { + PlayerError::playback_failed(format!("Failed to serialize audio settings: {}", e)) + })?; + + let vm = JAVA_VM + .get() + .ok_or_else(|| PlayerError::playback_failed("JavaVM not initialized"))?; + + let mut env = vm + .attach_current_thread() + .map_err(|e| PlayerError::playback_failed(format!("Failed to attach thread: {}", e)))?; + + let json_jstring = env.new_string(&json).map_err(|e| { + PlayerError::playback_failed(format!("Failed to create settings string: {}", e)) + })?; + + env.call_method( + &self.player_ref, + "setAudioSettings", + "(Ljava/lang/String;)V", + &[JValue::Object(&json_jstring)], + ) + .map_err(|e| { + PlayerError::playback_failed(format!("Failed to call setAudioSettings: {}", e)) + })?; + + // Store the sanitised form so audio_settings() reflects what was applied, + // not what was requested. + self.shared_state.lock_safe().audio_settings = settings + .clone() + .with_crossfade_clamped() + .with_equalizer_normalised(); + + Ok(()) + } + + /// TRACES: UR-027, UR-032, UR-033 | DR-030, DR-035, DR-036 + fn audio_settings(&self) -> AudioSettings { + self.shared_state.lock_safe().audio_settings.clone() + } + fn set_audio_track(&mut self, stream_index: i32) -> Result<(), PlayerError> { let vm = JAVA_VM .get() diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 3d35a343..2ffbfcab 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -179,10 +179,80 @@ impl VideoSettings { } } +/// Serialise `AudioSettings` into the JSON payload handed to the Android player +/// over JNI. +/// +/// Sanitises first (crossfade clamped, band vector normalised) so a malformed +/// vector can never reach the Kotlin parser. JSON is used rather than a wide JNI +/// signature so that adding a field does not change the method signature — the +/// same approach `load()` already uses for subtitles. +/// +/// The emitted keys are camelCase (serde) and `volumeLevel` is lowercase; the +/// Kotlin side matches on those literals. Both are pinned by tests. +/// +/// TRACES: UR-027, UR-032, UR-033 | DR-030, DR-035, DR-036 +pub fn audio_settings_jni_payload(settings: &AudioSettings) -> Result { + let sanitised = settings + .clone() + .with_crossfade_clamped() + .with_equalizer_normalised(); + serde_json::to_string(&sanitised) +} + #[cfg(test)] mod tests { use super::*; + /// The JNI payload must sanitise before serialising: an over-long crossfade + /// is clamped and a wrong-length band vector is normalised to EQ_BANDS.len(). + /// Sending raw values would let a malformed vector reach the Kotlin parser. + /// + /// TRACES: UR-027, UR-032, UR-033 | DR-030, DR-035, DR-036 | UT-AUDIO-JNI-1 + #[test] + fn test_audio_settings_jni_payload_is_sanitised() { + let settings = AudioSettings { + crossfade_duration: 30.0, + equalizer_bands: vec![20.0, -30.0], + ..AudioSettings::default() + }; + + let json = audio_settings_jni_payload(&settings).expect("serialises"); + let v: serde_json::Value = serde_json::from_str(&json).expect("valid JSON"); + + assert_eq!(v["crossfadeDuration"], 12.0, "crossfade clamped to 12s"); + + let bands = v["equalizerBands"].as_array().expect("bands array"); + assert_eq!(bands.len(), EQ_BANDS.len(), "band vector normalised to 10"); + assert_eq!(bands[0], EQ_GAIN_MAX as f64, "gain clamped to +12dB"); + assert_eq!(bands[1], EQ_GAIN_MIN as f64, "gain clamped to -12dB"); + } + + /// The Kotlin side parses these exact keys. camelCase is what serde emits + /// for AudioSettings; a rename here silently breaks the Android parser, + /// which is why the contract is pinned by a test rather than by convention. + /// + /// TRACES: UR-027, UR-032, UR-033 | DR-030, DR-035, DR-036 | UT-AUDIO-JNI-2 + #[test] + fn test_audio_settings_jni_payload_key_contract() { + let json = audio_settings_jni_payload(&AudioSettings::default()).expect("serialises"); + let v: serde_json::Value = serde_json::from_str(&json).expect("valid JSON"); + + for key in [ + "crossfadeDuration", + "gaplessPlayback", + "normalizeVolume", + "volumeLevel", + "equalizerEnabled", + "equalizerBands", + ] { + assert!(v.get(key).is_some(), "JNI payload must carry `{key}`"); + } + + // VolumeLevel is #[serde(rename_all = "lowercase")]; Kotlin matches on + // these literals. + assert_eq!(v["volumeLevel"], "normal"); + } + #[test] fn test_default_settings() { let settings = AudioSettings::default();