Skip to content

Repository files navigation

ScrobbleFix

A TidaLuna plugin that repairs Last.fm scrobbling in the TIDAL desktop client.

If you play an album or a queue straight through, TIDAL's built-in Last.fm integration scrobbles almost nothing. This plugin takes scrobbling over and measures play time correctly.

Install

Add this URL under Luna Settings → Plugin Store, then install ScrobbleFix from the store card that appears:

https://github.com/ZakME/ScrobbleFix/releases/download/latest/store.json

Open Luna Settings → ScrobbleFix and press Connect. If TIDAL is already linked to Last.fm this adopts that session immediately; otherwise it opens Last.fm in your browser to authorise. More detail below.

The bug in TIDAL

TIDAL's own scrobbler is a redux saga that runs on playbackControls/ENDED and applies Last.fm's rule "scrobble once half a track, or four minutes, has been played". It reads the play time from state.accumulatedPlaybackTime.storedTimes[productId].

That store never sees most of your listening. Its reducer only banks elapsed wall-clock time in three situations, and none of them is "audio is playing":

  • playbackControls/SET_PLAYBACK_STATE to any state other than PLAYING (pause, stall, stop)
  • playbackControls/SEEK
  • playbackControls/ENDED

Each of those adds now - lastPlayingTimestamp. The problem is what resets that clock. On playbackControls/MEDIA_PRODUCT_TRANSITION the reducer sets lastPlayingTimestamp = now whenever playback is already PLAYING — and for a gapless or crossfaded queue transition, TIDAL's player starts the next track (dispatching MEDIA_PRODUCT_TRANSITION for track B) before the outgoing element's ended handler dispatches ENDED for track A.

So for a track played from start to finish without touching the controls:

  1. MEDIA_PRODUCT_TRANSITION (track B) → lastPlayingTimestamp = now, clock reset
  2. ENDED (track A) → storedTimes[A] += now - lastPlayingTimestamp0 ms
  3. The scrobble saga divides duration by ~0 seconds played, fails the 50% / 4 minute check, and returns

Two supporting bugs make it worse. The saga reads the track length from state.playbackControls.playbackContext.actualDuration, which by then already describes the next track, so even the threshold arithmetic and the duration sent to Last.fm are about the wrong song. And the SEEK branch never resets lastPlayingTimestamp, so seeking double-counts time.

The visible symptom matches exactly: continuous playback never scrobbles, while pausing part-way through a track sometimes does — because pausing is one of the few things that actually banks time.

What this plugin does

  • Derives play time from reported playback position (playbackControls/TIME_UPDATE) rather than wall-clock timestamps, so pauses, stalls, seeks and queue transitions are all handled without depending on the order redux actions arrive in.
  • Scrobbles once the real threshold is met: half the track, or four minutes, whichever comes first, and never for tracks under 30 seconds.
  • Skips videos and TIDAL's "Advertisement" placeholders, matching what TIDAL's own scrobbler does.
  • Sends timestamp as the moment the track started, and duration for the track that actually played.
  • Sends track.updateNowPlaying when a track genuinely starts producing audio.
  • Queues scrobbles that fail to send (offline, Last.fm outage) and retries them automatically. Last.fm accepts backdated scrobbles, so nothing is lost.
  • Reuses the Last.fm session TIDAL already holds, so if TIDAL is linked to Last.fm you do not need to authorise anything again.
  • Keeps TIDAL's built-in scrobbler switched off, because when its accounting does fire it submits the same track a second time with a different timestamp.

No Last.fm API key of your own is needed; the plugin looks up the credentials the TIDAL client already ships with, at runtime, rather than hardcoding them.

About TIDAL's own Last.fm settings screen

While suppression is on, TIDAL's Settings → Last.fm page will show as not connected. That is expected: the plugin clears TIDAL's connection flag to stop its saga. Your stored session key is left untouched, so turning suppression off in the plugin settings hands scrobbling straight back to TIDAL.

Install in full

  1. Install TidaLuna.
  2. In TIDAL, open Luna Settings → Plugin Store and add this store URL:
https://github.com/ZakME/ScrobbleFix/releases/download/latest/store.json
  1. Install ScrobbleFix from the store card that appears.
  2. Open Luna Settings → ScrobbleFix and press Connect. If TIDAL is already linked to Last.fm this adopts that session immediately; otherwise it opens Last.fm in your browser to authorise.

Then play a track past its halfway point. The Diagnostics panel shows what is being measured in real time, so you can watch the heard-time climb and the scrobble fire.

From source

Requires Node 24 and pnpm (corepack enable).

pnpm install
pnpm run watch

watch builds to ./dist and serves it on port 3000. TidaLuna looks for a dev store on that exact port, so a [DEV] store card appears under Luna Settings → Plugin Store; install ScrobbleFix from there. Enable Live reload on the plugin and it picks up each rebuild within a second.

Settings

Setting Default Purpose
Scrobble tracks on Submit listens once the threshold is reached.
Update now playing on Show the current track on your profile.
Suppress TIDAL's built-in scrobbler on Prevents duplicate scrobbles.
Look up MusicBrainz IDs off Better matching for obscure releases, one extra request per track.
Verbose logging off Records every position sample in the event log.

Privacy and security

  • Your Last.fm session key never leaves your machine, except in requests to Last.fm itself. It is not sent anywhere else and does not appear in the event log or in the text produced by Copy diagnostics, which is written to be safe to paste into a public bug report. That text contains no account name and no session key.
  • The session key is stored in TidaLuna's plugin storage in plain text, in the same IndexedDB that TIDAL uses for its own copy. Any other TidaLuna plugin you install can read it, so treat installing plugins as a trust decision. Encrypting it locally would not help, since the plugin would need to keep the decryption key next to it.
  • Signing out from the plugin settings deletes the stored key. It does not touch the copy TIDAL keeps. To revoke access entirely, remove the application from your Last.fm applications page.
  • The authorisation link is only placed on your clipboard if a browser could not be opened. That link embeds a one-time request token which, before you approve it, is enough for someone else to obtain a session key on your account — because the exchange is signed with TIDAL's API secret, which ships publicly in the client. Do not share the link.
  • No API key of your own is needed. The plugin reads the Last.fm API credentials that the TIDAL client already ships with, at runtime rather than hardcoded, so nothing sensitive lives in this repository and a credential rotation on TIDAL's side does not require a plugin update. The trade-off is that scrobbles are attributed to TIDAL's registered application rather than to a ScrobbleFix one.

Reusing this in another client

docs/tidal-scrobbling.md documents the bug and the correct implementation independently of TidaLuna: how to measure play time without depending on event ordering, Last.fm's thresholds and signing rules, the error codes worth retrying, and the traps (casting session ids, TIME_UPDATE not being a position ticker). Useful if you are writing your own TIDAL client rather than a plugin.

Development notes

tools/ holds the scripts used to reverse-engineer the bug from TIDAL's shipped bundle: fetch-tidal-assets.mjs downloads every chunk of the web player, and grep-assets.mjs, find-defs.mjs and resolve-import.mjs search it and map minified identifiers back to their source modules. Every claim in The bug in TIDAL was checked against the shipped code with these.

tools/md5.test.mjs checks the bundled MD5 (needed for Last.fm request signing, and absent from SubtleCrypto) against Node's crypto. tools/check-bundle.mjs parses the built plugin and reports the host modules it expects, since the bundle itself can only run inside TIDAL.

pnpm test
pnpm run typecheck
node --experimental-vm-modules tools/check-bundle.mjs

typecheck reports only first-party diagnostics. The luna dependency is consumed as TypeScript source and its own dependencies (@mui, musicbrainz-api, ...) are not installed here, so it produces errors that cannot be fixed from this repo; tools/typecheck.mjs filters those out and fails on anything under plugins/ or tools/.

About

TidaLuna plugin that repairs Last.fm scrobbling in the TIDAL desktop client

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages