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
+10
@@ -661,6 +661,16 @@
|
|||||||
<span>Romaji anzeigen</span>
|
<span>Romaji anzeigen</span>
|
||||||
<div id="s-show-romaji" class="toggle" onclick="this.classList.toggle('on')"></div>
|
<div id="s-show-romaji" class="toggle" onclick="this.classList.toggle('on')"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="edit-pl-toggle-row" style="margin-top:12px">
|
||||||
|
<span>Scrobble senden nach</span>
|
||||||
|
<select id="s-scrobble-mode" style="background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-family:var(--font);font-size:13px;cursor:pointer">
|
||||||
|
<option value="15s">15 Sekunden</option>
|
||||||
|
<option value="30s">30 Sekunden</option>
|
||||||
|
<option value="1/3">1/3 der Song-Dauer</option>
|
||||||
|
<option value="2/3">2/3 der Song-Dauer</option>
|
||||||
|
<option value="end">Song-Ende</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||||
|
|||||||
+21
-6
@@ -20,20 +20,33 @@ let _bridgeReady = false; // true once onReady has fired; onQueueEnded is ign
|
|||||||
// ─── SCROBBLE ─────────────────────────────────────────────────────────────
|
// ─── SCROBBLE ─────────────────────────────────────────────────────────────
|
||||||
let _scrobbleTimer = null;
|
let _scrobbleTimer = null;
|
||||||
let _scrobbleId = null;
|
let _scrobbleId = null;
|
||||||
function _scheduleScrobble(id) {
|
function _scheduleScrobble(id, durSec) {
|
||||||
if (_scrobbleId === String(id)) return; // already scheduled for this track
|
if (_scrobbleId === String(id)) return;
|
||||||
_cancelScrobble();
|
_cancelScrobble();
|
||||||
_scrobbleId = String(id);
|
_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(() => {
|
_scrobbleTimer = setTimeout(() => {
|
||||||
if (_scrobbleId) api('scrobble', `&id=${_scrobbleId}&time=${Date.now()}`).catch(() => {});
|
if (_scrobbleId) api('scrobble', `&id=${_scrobbleId}&time=${Date.now()}`).catch(() => {});
|
||||||
_scrobbleId = null;
|
_scrobbleId = null;
|
||||||
}, 15_000);
|
}, delay);
|
||||||
}
|
}
|
||||||
function _cancelScrobble() {
|
function _cancelScrobble() {
|
||||||
clearTimeout(_scrobbleTimer);
|
clearTimeout(_scrobbleTimer);
|
||||||
_scrobbleTimer = null;
|
_scrobbleTimer = null;
|
||||||
_scrobbleId = 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,
|
// Unified duration / position / seek helpers — use native state when available,
|
||||||
// fall back to HTMLAudioElement when not.
|
// fall back to HTMLAudioElement when not.
|
||||||
@@ -192,7 +205,7 @@ window.NSPlayer = {
|
|||||||
try {
|
try {
|
||||||
const s = S.queue[S.qIdx];
|
const s = S.queue[S.qIdx];
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
_scheduleScrobble(s.id);
|
_scheduleScrobble(s.id, s.duration);
|
||||||
if (S.radioMode && S.queue.length - S.qIdx < 5) fillRadioQueue();
|
if (S.radioMode && S.queue.length - S.qIdx < 5) fillRadioQueue();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
},
|
},
|
||||||
@@ -230,6 +243,7 @@ window.NSPlayer = {
|
|||||||
if (!_bridgeReady || Date.now() - _readyTs < 1500) return;
|
if (!_bridgeReady || Date.now() - _readyTs < 1500) return;
|
||||||
_nativePlaying = false;
|
_nativePlaying = false;
|
||||||
setPlayIcon(false);
|
setPlayIcon(false);
|
||||||
|
if (S.cfg.scrobbleMode === 'end') _scrobbleNow(S.queue[S.qIdx]);
|
||||||
onEnded(); // handles repeat, radio top-up, and queue advance
|
onEnded(); // handles repeat, radio top-up, and queue advance
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -376,7 +390,7 @@ export function playCurrent() {
|
|||||||
updatePlayerUI(s, true);
|
updatePlayerUI(s, true);
|
||||||
onNpSongChange(s.id);
|
onNpSongChange(s.id);
|
||||||
renderQueue();
|
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);
|
const src = (_preId === s.id && _preUrl) ? _preUrl : streamUrl(s.id);
|
||||||
_clearPreload();
|
_clearPreload();
|
||||||
@@ -691,7 +705,8 @@ export function nextSong() {
|
|||||||
|
|
||||||
export function onEnded() {
|
export function onEnded() {
|
||||||
if(S.repeat === 'one') { S.audio.currentTime = 0; S.audio.play(); return; }
|
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();
|
nextSong();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function openSettings() {
|
|||||||
document.getElementById('s-show-playcount').classList.toggle('on', !!S.cfg.showPlayCount);
|
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-show-romaji').classList.toggle('on', !!S.cfg.showRomaji);
|
||||||
document.getElementById('s-cover-color-bg').classList.toggle('on', !!S.cfg.coverColorBg);
|
document.getElementById('s-cover-color-bg').classList.toggle('on', !!S.cfg.coverColorBg);
|
||||||
|
document.getElementById('s-scrobble-mode').value = S.cfg.scrobbleMode || '15s';
|
||||||
_renderThemePicker();
|
_renderThemePicker();
|
||||||
document.getElementById('settings-modal').classList.add('open');
|
document.getElementById('settings-modal').classList.add('open');
|
||||||
checkBackend();
|
checkBackend();
|
||||||
@@ -131,6 +132,7 @@ export function saveSettings() {
|
|||||||
const romajiChanged = S.cfg.showRomaji !== document.getElementById('s-show-romaji').classList.contains('on');
|
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.showRomaji = document.getElementById('s-show-romaji').classList.contains('on');
|
||||||
S.cfg.coverColorBg = document.getElementById('s-cover-color-bg').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();
|
updateNpColorMode();
|
||||||
localStorage.setItem('ns_cfg', JSON.stringify(S.cfg));
|
localStorage.setItem('ns_cfg', JSON.stringify(S.cfg));
|
||||||
closeSettings(); toast('✅ Gespeichert');
|
closeSettings(); toast('✅ Gespeichert');
|
||||||
|
|||||||
Reference in new issue
Block a user