feat: timer scrobble (12s), artist topsong star+menu, play count toggle
Scrobbling: - Replace end-of-track scrobble with 12s timer approach - Android: schedule on onTrackChanged, cancel on pause - Web: schedule in playCurrent, cancel in togglePlay pause + onEnded - Skip/new track resets the timer automatically (new song ID) Artist top songs: - Add star button and 3-dots context menu to every top song row - Topsong grid expanded to 5 columns to fit action buttons Settings — play count toggle: - New "Bibliothek" section with toggle switch - When on: shows play count in album/playlist song rows - Artist page always shows play count regardless of toggle - Persisted in ns_cfg localStorage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f79817410b
commit
7e07fef7ba
7 files changed
+57
-13
No files matched your search
+27
-7
@@ -17,6 +17,24 @@ let _lastDurationMs = 0; // ms — set by onPositionUpdate, used for seek calc
|
||||
let _skipping = false; // true briefly after a skip — suppresses the transient isPlaying:false
|
||||
let _skipTimer = null;
|
||||
|
||||
// ─── SCROBBLE ─────────────────────────────────────────────────────────────
|
||||
let _scrobbleTimer = null;
|
||||
let _scrobbleId = null;
|
||||
function _scheduleScrobble(id) {
|
||||
if (_scrobbleId === String(id)) return; // already scheduled for this track
|
||||
_cancelScrobble();
|
||||
_scrobbleId = String(id);
|
||||
_scrobbleTimer = setTimeout(() => {
|
||||
if (_scrobbleId) api('scrobble', `&id=${_scrobbleId}&time=${Date.now()}`).catch(() => {});
|
||||
_scrobbleId = null;
|
||||
}, 12_000);
|
||||
}
|
||||
function _cancelScrobble() {
|
||||
clearTimeout(_scrobbleTimer);
|
||||
_scrobbleTimer = null;
|
||||
_scrobbleId = null;
|
||||
}
|
||||
|
||||
// Unified duration / position / seek helpers — use native state when available,
|
||||
// fall back to HTMLAudioElement when not.
|
||||
const _dur = () => HAS_BRIDGE ? _lastDurationMs / 1000 : (S.audio.duration || 0);
|
||||
@@ -142,6 +160,7 @@ window.NSPlayer = {
|
||||
}
|
||||
const prev = _nativePlaying;
|
||||
_nativePlaying = state.isPlaying;
|
||||
if (!state.isPlaying && !state.isBuffering) _cancelScrobble();
|
||||
if (state.isPlaying && S.previewCurrent) stopPreview(false);
|
||||
if (state.isPlaying !== prev) {
|
||||
setPlayIcon(state.isPlaying);
|
||||
@@ -189,10 +208,6 @@ window.NSPlayer = {
|
||||
// doesn't match what native is actually playing, the queues are mismatched.
|
||||
// Allow through if reason='queue' (we triggered a setQueue) or if no IDs to compare.
|
||||
if (liveId && S.queue[idx]?.id && String(liveId) !== String(S.queue[idx].id) && p.reason !== 'queue') return;
|
||||
// Scrobble the track that just finished (auto-advance = it played to completion).
|
||||
if (p.reason === 'auto' && S.queue[S.qIdx]?.id) {
|
||||
api('scrobble', `&id=${S.queue[S.qIdx].id}&time=${Date.now()}`).catch(() => {});
|
||||
}
|
||||
const indexChanged = (idx !== S.qIdx);
|
||||
S.qIdx = idx;
|
||||
const s = S.queue[S.qIdx];
|
||||
@@ -200,6 +215,8 @@ window.NSPlayer = {
|
||||
updatePlayerUI(s, true);
|
||||
onNpSongChange(s.id);
|
||||
if (indexChanged) renderQueue();
|
||||
// Schedule scrobble — fires after 12s of uninterrupted playback.
|
||||
_scheduleScrobble(s.id);
|
||||
// Radio mode: top up the queue when running low.
|
||||
// fillRadioQueue sends addToQueue to native internally — no re-send here.
|
||||
if (S.radioMode && S.queue.length - S.qIdx < 5) fillRadioQueue();
|
||||
@@ -365,6 +382,7 @@ export function playCurrent() {
|
||||
updatePlayerUI(s, true);
|
||||
onNpSongChange(s.id);
|
||||
renderQueue();
|
||||
_scheduleScrobble(s.id); // 12s timer — cancelled on pause/skip
|
||||
|
||||
const src = (_preId === s.id && _preUrl) ? _preUrl : streamUrl(s.id);
|
||||
_clearPreload();
|
||||
@@ -516,7 +534,10 @@ export function cancelMusicAutoplay() { _playGen++; S.audio.pause(); }
|
||||
|
||||
export function togglePlay() {
|
||||
if (HAS_BRIDGE) { _nsBridge('togglePlay'); return; }
|
||||
if(S.audio.src) S.audio.paused ? S.audio.play() : S.audio.pause();
|
||||
if(S.audio.src) {
|
||||
if (S.audio.paused) { S.audio.play(); }
|
||||
else { S.audio.pause(); _cancelScrobble(); }
|
||||
}
|
||||
}
|
||||
|
||||
export const isPlaying = () => HAS_BRIDGE ? _nativePlaying : !S.audio.paused;
|
||||
@@ -618,8 +639,7 @@ export function nextSong() {
|
||||
|
||||
export function onEnded() {
|
||||
if(S.repeat === 'one') { S.audio.currentTime = 0; S.audio.play(); return; }
|
||||
// Scrobble the track that just finished (web path).
|
||||
if (S.queue[S.qIdx]?.id) api('scrobble', `&id=${S.queue[S.qIdx].id}&time=${Date.now()}`).catch(() => {});
|
||||
_cancelScrobble(); // timer irrelevant now — song already finished
|
||||
nextSong();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user