feat(scrobble): own timestamped play-log service (stats/Wrapped foundation)
Phase 1+2 of a self-hosted, multi-user listening archive — the basis for day/month/year stats and a yearly "Wrapped". Runs IN ADDITION to the existing Navidrome/Last.fm scrobble; never replaces it. Service (isolated, own container): - scrobble/scrobble-server.js — node:sqlite play-log (own volume), routes POST /scrobble/plays (idempotent by uuid), GET /scrobble/history (archive), DELETE /scrobble/plays?user= (test cleanup), GET /scrobble/health. Multi-user auth via Navidrome ping.view (cached) -> user not spoofable. - docker-compose: novasonic-scrobble service (node:22, port 3710) + volume; backend bumped node:20->22 (EOL); SCROBBLE_URL plumbing (entrypoint, .env.example). Capture (both platforms, offline-safe queue, idempotent batch flush): - Fires at the existing scrobbleMode counted-play moment — consistent with Last.fm, respects the user's setting (no separate 50%/4min rule). - Web: src/player/scrobbleLog.js (localStorage queue) + subsonicAuthParams() in api.js + hooks in player.js + flush init in main.js; S.cfg.scrobbleUrl. - Android: data/PlayLogClient.kt (filesDir queue, reused Ktor client) + repo.logPlay/flushPlayLog + PlayerController hook + NovaSonicApp flush + scrobbleUrl in PrefsStore/SettingsViewModel/SettingsScreen (with test button). Plan + design decisions documented in docs/scrobble-plan.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
e69fbaf75e
commit
71190488ca
19 files changed
+754
-16
No files matched your search
@@ -7,6 +7,14 @@ export function apiUrl(method, extra='') {
|
||||
const token = md5(S.cfg.pass + salt);
|
||||
return `${S.cfg.url}/rest/${method}?u=${encodeURIComponent(S.cfg.user)}&t=${token}&s=${salt}&v=1.16.1&c=NovaSonic&f=json${extra}`;
|
||||
}
|
||||
// Bare Subsonic token-auth params (u=..&t=..&s=..) for talking to non-Navidrome NovaSonic
|
||||
// services (e.g. the scrobble play-log), which verify the same creds via ping.view.
|
||||
export function subsonicAuthParams() {
|
||||
if (!S.cfg.pass || !S.cfg.user) return '';
|
||||
const salt = Math.random().toString(36).slice(2, 10);
|
||||
const token = md5(S.cfg.pass + salt);
|
||||
return `u=${encodeURIComponent(S.cfg.user)}&t=${token}&s=${salt}`;
|
||||
}
|
||||
export async function api(method, extra='') {
|
||||
const res = await fetch(apiUrl(method, extra));
|
||||
if(!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
Reference in new issue
Block a user