feat: configurable scrobble timing (15s / 30s / 1/3 / 2/3 / end)
Settings dropdown: 15s (default), 30s, 1/3 of duration, 2/3 of duration, song end. Duration-based modes use s.duration from Navidrome metadata (always available). 'end' mode skips the timer and fires the scrobble in onEnded (web) / onQueueEnded (native). Skipping a song never scrobbles in any mode — _cancelScrobble() still runs on skip/pause. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
acb15d8c00
commit
6391857fda
3 files changed
+33
-6
No files matched your search
+21
-6
@@ -20,20 +20,33 @@ let _bridgeReady = false; // true once onReady has fired; onQueueEnded is ign
|
||||
// ─── SCROBBLE ─────────────────────────────────────────────────────────────
|
||||
let _scrobbleTimer = null;
|
||||
let _scrobbleId = null;
|
||||
function _scheduleScrobble(id) {
|
||||
if (_scrobbleId === String(id)) return; // already scheduled for this track
|
||||
function _scheduleScrobble(id, durSec) {
|
||||
if (_scrobbleId === String(id)) return;
|
||||
_cancelScrobble();
|
||||
_scrobbleId = String(id);
|
||||
const mode = S.cfg.scrobbleMode || '15s';
|
||||
if (mode === 'end') return; // fired explicitly in onEnded / onQueueEnded
|
||||
let delay;
|
||||
const dur = (durSec || 0) * 1000;
|
||||
switch (mode) {
|
||||
case '30s': delay = 30_000; break;
|
||||
case '1/3': delay = dur > 0 ? Math.floor(dur / 3) : 30_000; break;
|
||||
case '2/3': delay = dur > 0 ? Math.floor(dur * 2 / 3) : 60_000; break;
|
||||
default: delay = 15_000; break;
|
||||
}
|
||||
_scrobbleTimer = setTimeout(() => {
|
||||
if (_scrobbleId) api('scrobble', `&id=${_scrobbleId}&time=${Date.now()}`).catch(() => {});
|
||||
_scrobbleId = null;
|
||||
}, 15_000);
|
||||
}, delay);
|
||||
}
|
||||
function _cancelScrobble() {
|
||||
clearTimeout(_scrobbleTimer);
|
||||
_scrobbleTimer = null;
|
||||
_scrobbleId = null;
|
||||
}
|
||||
function _scrobbleNow(s) {
|
||||
if (s) api('scrobble', `&id=${s.id}&time=${Date.now()}`).catch(() => {});
|
||||
}
|
||||
|
||||
// Unified duration / position / seek helpers — use native state when available,
|
||||
// fall back to HTMLAudioElement when not.
|
||||
@@ -192,7 +205,7 @@ window.NSPlayer = {
|
||||
try {
|
||||
const s = S.queue[S.qIdx];
|
||||
if (!s) return;
|
||||
_scheduleScrobble(s.id);
|
||||
_scheduleScrobble(s.id, s.duration);
|
||||
if (S.radioMode && S.queue.length - S.qIdx < 5) fillRadioQueue();
|
||||
} catch (e) {}
|
||||
},
|
||||
@@ -230,6 +243,7 @@ window.NSPlayer = {
|
||||
if (!_bridgeReady || Date.now() - _readyTs < 1500) return;
|
||||
_nativePlaying = false;
|
||||
setPlayIcon(false);
|
||||
if (S.cfg.scrobbleMode === 'end') _scrobbleNow(S.queue[S.qIdx]);
|
||||
onEnded(); // handles repeat, radio top-up, and queue advance
|
||||
},
|
||||
|
||||
@@ -376,7 +390,7 @@ export function playCurrent() {
|
||||
updatePlayerUI(s, true);
|
||||
onNpSongChange(s.id);
|
||||
renderQueue();
|
||||
_scheduleScrobble(s.id); // 12s timer — cancelled on pause/skip
|
||||
_scheduleScrobble(s.id, s.duration);
|
||||
|
||||
const src = (_preId === s.id && _preUrl) ? _preUrl : streamUrl(s.id);
|
||||
_clearPreload();
|
||||
@@ -691,7 +705,8 @@ export function nextSong() {
|
||||
|
||||
export function onEnded() {
|
||||
if(S.repeat === 'one') { S.audio.currentTime = 0; S.audio.play(); return; }
|
||||
_cancelScrobble(); // timer irrelevant now — song already finished
|
||||
if (!HAS_BRIDGE && S.cfg.scrobbleMode === 'end') _scrobbleNow(S.queue[S.qIdx]);
|
||||
_cancelScrobble();
|
||||
nextSong();
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export function openSettings() {
|
||||
document.getElementById('s-show-playcount').classList.toggle('on', !!S.cfg.showPlayCount);
|
||||
document.getElementById('s-show-romaji').classList.toggle('on', !!S.cfg.showRomaji);
|
||||
document.getElementById('s-cover-color-bg').classList.toggle('on', !!S.cfg.coverColorBg);
|
||||
document.getElementById('s-scrobble-mode').value = S.cfg.scrobbleMode || '15s';
|
||||
_renderThemePicker();
|
||||
document.getElementById('settings-modal').classList.add('open');
|
||||
checkBackend();
|
||||
@@ -131,6 +132,7 @@ export function saveSettings() {
|
||||
const romajiChanged = S.cfg.showRomaji !== document.getElementById('s-show-romaji').classList.contains('on');
|
||||
S.cfg.showRomaji = document.getElementById('s-show-romaji').classList.contains('on');
|
||||
S.cfg.coverColorBg = document.getElementById('s-cover-color-bg').classList.contains('on');
|
||||
S.cfg.scrobbleMode = document.getElementById('s-scrobble-mode').value || '15s';
|
||||
updateNpColorMode();
|
||||
localStorage.setItem('ns_cfg', JSON.stringify(S.cfg));
|
||||
closeSettings(); toast('✅ Gespeichert');
|
||||
|
||||
Reference in new issue
Block a user