From a2211a62d291ffb9f08c2056292f0835044c2168 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 6 Sep 2026 23:25:06 +0200 Subject: [PATCH] refactor(web+android): nur Warteschlange und Titel sichern, keine Sekunden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Das Mitschreiben der Abspielposition war zu viel des Guten: es bedeutete staendiges Schreiben waehrend der Wiedergabe fuer einen Gewinn, den man kaum bemerkt. Gesichert werden jetzt nur Warteschlange und aktueller Titel, der dann von vorn beginnt. Der eigentliche Gewinn bleibt und war der Ausloeser: unter Android war die Now-Playing-Leiste nach dem Oeffnen leer, bis man einmal auf Play drueckte — gesichert war nur der letzte Song, nicht die Warteschlange. Das Wiederherstellen fuellt den Zustand jetzt ueber syncState(), also ohne Abspielen. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 19 +++++----- .../com/novasonic/app/data/MusicRepository.kt | 3 +- .../com/novasonic/app/data/PrefsStore.kt | 18 +++++----- .../novasonic/app/player/PlayerController.kt | 10 +++--- src/main.js | 7 +--- src/player/player.js | 35 +------------------ 6 files changed, 25 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c86367..de91adc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,16 +72,15 @@ Versioning: two-part `MAJOR.MINOR` — each release bumps the minor (features an ## [2.4] — 2026-09-06 ### Added -- **Wiedergabe fortsetzen, wo du aufgehört hast (Web + Android)** — bisher überlebte im Web nur der - zuletzt gespielte Titel und die Warteschlange, aber **nicht die Position im Titel**; Android - sicherte allein den letzten Song, ohne Warteschlange. Jetzt merken sich beide Warteschlange, - Listenposition und Sekunde im Titel. Web schreibt gedrosselt beim Abspielen sowie sicher beim - Pausieren, Wegwischen und Wechseln des Tabs (`pagehide`/`visibilitychange` statt `beforeunload`, - das am Handy unzuverlässig ist). Android sichert bei jedem Titelwechsel und beim Pausieren und - stellt die Warteschlange nach dem Verbinden wieder her — **vorbereitet, aber nicht abgespielt**: - ein Druck auf Play macht weiter. Lag die Position im letzten Rest des Titels, beginnt er von vorn, - statt zwei Sekunden Abspann zu spielen. Gespeichert werden nur Song-Ids; verschwundene Titel - werden beim Wiederherstellen übersprungen. +- **Zuletzt Gehörtes überlebt den Neustart (Web + Android)** — Android sicherte bisher nur den + letzten Song, ohne Warteschlange: die Now-Playing-Leiste blieb nach dem Öffnen **leer, bis man + einmal auf Play drückte**. Jetzt merken sich beide Plattformen Warteschlange und aktuellen Titel + und stellen sie beim Start wieder her — Android **vorbereitet, aber nicht abgespielt**, damit + einen die App beim Öffnen nicht überfällt; läuft schon etwas, passiert nichts. Der Titel beginnt + von vorn: die Sekunde im Titel wird bewusst **nicht** gespeichert, das spart dauerndes Schreiben + während der Wiedergabe. Gesichert werden nur Song-Ids, weil Titel und Cover sich durch + Metadaten-Bearbeitung ändern können; verschwundene Titel werden übersprungen. + - **Streitpunkte im Musik-Duell (Web + Android)** — gemeinsame Künstler, sortiert nach dem größten **Ungleichgewicht** statt nach der Summe: wo einer 120 Plays hat und der andere 3. Ein Balken zeigt das Kräfteverhältnis. Mindestens vier gemeinsame Plays und 50 % Unwucht, damit ein einzelner diff --git a/android/app/src/main/kotlin/com/novasonic/app/data/MusicRepository.kt b/android/app/src/main/kotlin/com/novasonic/app/data/MusicRepository.kt index 57ea095..060de14 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/data/MusicRepository.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/data/MusicRepository.kt @@ -169,8 +169,7 @@ class MusicRepository(private val context: Context) { suspend fun saveTheme(themeId: String) = prefs.saveTheme(themeId) - suspend fun saveQueueState(ids: List, index: Int, positionMs: Long) = - prefs.saveQueueState(ids, index, positionMs) + suspend fun saveQueueState(ids: List, index: Int) = prefs.saveQueueState(ids, index) suspend fun readQueueState() = prefs.readQueueState() suspend fun saveLastSong(song: com.novasonic.app.api.model.Song) = prefs.saveLastSong(song.id, song.title, song.artist ?: "", song.coverArt) diff --git a/android/app/src/main/kotlin/com/novasonic/app/data/PrefsStore.kt b/android/app/src/main/kotlin/com/novasonic/app/data/PrefsStore.kt index 26ca013..89fd19d 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/data/PrefsStore.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/data/PrefsStore.kt @@ -4,7 +4,6 @@ import android.content.Context import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow @@ -32,10 +31,11 @@ class PrefsStore(private val context: Context) { val KEY_LAST_SONG_TITLE = stringPreferencesKey("last_song_title") val KEY_LAST_SONG_ARTIST = stringPreferencesKey("last_song_artist") val KEY_LAST_SONG_COVER = stringPreferencesKey("last_song_cover") - // Warteschlange fortsetzen: Song-Ids, Position in der Liste und Position im Titel. + // Warteschlange fortsetzen: Song-Ids und Position in der Liste. Bewusst OHNE Sekunde im + // Titel — der zuletzt gehoerte Titel beginnt von vorn, das genuegt und spart staendiges + // Schreiben waehrend der Wiedergabe. val KEY_QUEUE_IDS = stringPreferencesKey("queue_ids") val KEY_QUEUE_INDEX = intPreferencesKey("queue_index") - val KEY_QUEUE_POSITION = longPreferencesKey("queue_position_ms") } data class Credentials(val serverUrl: String, val username: String, val password: String) { @@ -148,15 +148,14 @@ class PrefsStore(private val context: Context) { * Sichert die Warteschlange als Id-Liste samt Position. Nur Ids, keine vollstaendigen Songs: * Titel und Cover koennen sich aendern (Metadaten-Bearbeitung), die Id ist der stabile Bezug. */ - suspend fun saveQueueState(ids: List, index: Int, positionMs: Long) { + suspend fun saveQueueState(ids: List, index: Int) { context.dataStore.edit { prefs -> - prefs[KEY_QUEUE_IDS] = ids.joinToString(",") - prefs[KEY_QUEUE_INDEX] = index.coerceAtLeast(0) - prefs[KEY_QUEUE_POSITION] = positionMs.coerceAtLeast(0) + prefs[KEY_QUEUE_IDS] = ids.joinToString(",") + prefs[KEY_QUEUE_INDEX] = index.coerceAtLeast(0) } } - data class QueueState(val ids: List, val index: Int, val positionMs: Long) + data class QueueState(val ids: List, val index: Int) suspend fun readQueueState(): QueueState? { val prefs = context.dataStore.data.first() @@ -164,8 +163,7 @@ class PrefsStore(private val context: Context) { if (ids.isEmpty()) return null return QueueState( ids = ids, - index = (prefs[KEY_QUEUE_INDEX] ?: 0).coerceIn(0, ids.lastIndex), - positionMs = prefs[KEY_QUEUE_POSITION] ?: 0L, + index = (prefs[KEY_QUEUE_INDEX] ?: 0).coerceIn(0, ids.lastIndex), ) } diff --git a/android/app/src/main/kotlin/com/novasonic/app/player/PlayerController.kt b/android/app/src/main/kotlin/com/novasonic/app/player/PlayerController.kt index f433b26..7d64f4b 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/player/PlayerController.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/player/PlayerController.kt @@ -62,16 +62,16 @@ class PlayerController(private val context: Context, private val repo: MusicRepo } /** - * Haelt Warteschlange, Listenposition und Position im Titel fest. ABS-Hoerbuecher bleiben aussen - * vor — die haben ihren eigenen Fortschritt auf dem Server. + * Haelt Warteschlange und Listenposition fest — ohne die Sekunde im Titel: der zuletzt gehoerte + * Titel beginnt von vorn. ABS-Hoerbuecher bleiben aussen vor, die haben ihren eigenen + * Fortschritt auf dem Server. */ private fun persistQueueState() { val ctrl = controller ?: return val ids = queue.map { it.id } if (ids.isEmpty() || ids.any { it.startsWith("abs:") }) return val index = ctrl.currentMediaItemIndex - val pos = ctrl.currentPosition - scope.launch { runCatching { repo.saveQueueState(ids, index, pos) } } + scope.launch { runCatching { repo.saveQueueState(ids, index) } } } /** @@ -89,7 +89,7 @@ class PlayerController(private val context: Context, private val repo: MusicRepo if (songs.isEmpty()) return@launch val index = zustand.index.coerceIn(0, songs.lastIndex) queue = songs - ctrl.setMediaItems(songs.map { it.toMediaItem(repo) }, index, zustand.positionMs) + ctrl.setMediaItems(songs.map { it.toMediaItem(repo) }, index, 0L) ctrl.prepare() syncState() } diff --git a/src/main.js b/src/main.js index 7b9cd4f..300a78f 100644 --- a/src/main.js +++ b/src/main.js @@ -13,7 +13,7 @@ import { toggleShuffle, toggleRepeat, toggleQueue, setVolume, seekTo, initProgressScrub, updateProgress, updatePreviewProgress, stopPreview, toggleRadioMode, starCurrent, downloadCurrent, renderQueue, playPreview, - toggleStar, dlSongLocal, _applyRadioBtn, _applyShuffleRepeat, _restoreLastPlayed, persistPosition, setPlayIcon, + toggleStar, dlSongLocal, _applyRadioBtn, _applyShuffleRepeat, _restoreLastPlayed, setPlayIcon, npToggleStar, updateMediaSession, updateNpColorMode, reconcileNativeState, initKeyboardShortcuts, } from './player/player.js'; import { @@ -158,9 +158,6 @@ window.addEventListener('load', () => { let _errRetries = 0, _errTimer = null; function bindAudioEl(el) { el.addEventListener('timeupdate', updateProgress); - // Position mitschreiben (gedrosselt), damit ein Neuladen dort weitermacht, wo du warst. - el.addEventListener('timeupdate', () => persistPosition()); - el.addEventListener('pause', () => persistPosition(true)); el.addEventListener('ended', onEnded); el.addEventListener('play', () => { S._trackChanging = false; @@ -318,8 +315,6 @@ window.addEventListener('load', () => { } }); -window.addEventListener('pagehide', () => persistPosition(true)); -document.addEventListener('visibilitychange', () => { if (document.hidden) persistPosition(true); }); window.addEventListener('popstate', (e) => _navOnPopState(e.state)); document.addEventListener('keydown', e => { if(e.key !== 'Backspace') return; diff --git a/src/player/player.js b/src/player/player.js index 8c23718..1bfcf70 100644 --- a/src/player/player.js +++ b/src/player/player.js @@ -1364,25 +1364,6 @@ export function reconcileNativeState() { } catch {} } -// Abspielposition mitschreiben. Der Eintrag entsteht beim Titelwechsel; hier wird nur die Position -// nachgetragen, damit ein Neuladen dort weitermacht, wo du warst — nicht am Anfang des Titels. -// Gedrosselt, weil timeupdate mehrmals pro Sekunde feuert und localStorage synchron schreibt. -let _posLastWrite = 0; -export function persistPosition(force = false) { - if (HAS_BRIDGE) return; // nativ verwaltet Media3 den Zustand - const jetzt = Date.now(); - if (!force && jetzt - _posLastWrite < 5000) return; - _posLastWrite = jetzt; - try { - const roh = localStorage.getItem('ns_last_played'); - if (!roh) return; - const daten = JSON.parse(roh); - daten.position = S.audio?.currentTime || 0; - daten.savedAt = Math.floor(jetzt / 1000); - localStorage.setItem('ns_last_played', JSON.stringify(daten)); - } catch {} -} - export function _restoreLastPlayed() { try { const saved = JSON.parse(localStorage.getItem('ns_last_played') || 'null'); @@ -1407,20 +1388,6 @@ export function _restoreLastPlayed() { // Web path: prime the audio element so play button works immediately. S.audio.src = streamUrl(s.id); S.audio.load(); - // Gespeicherte Position wiederherstellen, sobald die Dauer bekannt ist. Lag sie im letzten - // Rest des Titels, faengt der Titel lieber von vorn an — sonst spielt man beim Fortsetzen - // zwei Sekunden Abspann und springt weiter. - const pos = Number(saved.position) || 0; - if (pos > 1) { - const setzen = () => { - const rest = (S.audio.duration || 0) - pos; - if (S.audio.duration && rest > 5) { - try { S.audio.currentTime = pos; } catch {} - updateProgress(); - } - S.audio.removeEventListener('loadedmetadata', setzen); - }; - S.audio.addEventListener('loadedmetadata', setzen); - } + } catch {} }