From 2d7319fa52f38e350dae051b08e0a48212964fe1 Mon Sep 17 00:00:00 2001 From: Ewan Croft Date: Thu, 13 Aug 2026 14:54:25 +0100 Subject: [PATCH] fix(tourmaline): one-hit wonders means one song, not one play MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit oneHitWondersCount counted artists scrobbled exactly once total, which excludes anyone you've replayed the same single track by dozens of times — despite the UI already labelling this "One-hit wonders", the idiomatic meaning (and lastfm-stats-web's definition, which this app's z-score/Eddington code is already ported from) is "known for exactly one song", regardless of play count. Tracks each artist's set of distinct track names during aggregation and counts artists where that set has exactly one member. --- packages/tourmaline/src/lib/analysis/aggregator.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/tourmaline/src/lib/analysis/aggregator.ts b/packages/tourmaline/src/lib/analysis/aggregator.ts index 1b1073e..afb1dfd 100644 --- a/packages/tourmaline/src/lib/analysis/aggregator.ts +++ b/packages/tourmaline/src/lib/analysis/aggregator.ts @@ -121,6 +121,8 @@ export class Aggregator { private firstListenMap = new Map(); // artist → earliest YYYY-MM-DD private trackFirstListen = new Map(); // trackKey → earliest YYYY-MM-DD private albumFirstListen = new Map(); // albumKey → earliest YYYY-MM-DD + /** artist → distinct track names played, for the "one-hit wonder" definition (one song, any number of plays). */ + private artistTracks = new Map>(); private minutesCount = 0; private total = 0; // ── New tracking fields ───────────────────────────────────────────────────── @@ -188,6 +190,11 @@ export class Aggregator { if (artistTs) artistTs.push(date.getTime()); else this.artistTimestamps.set(artistName, [date.getTime()]); + // Per-artist distinct track set, for the "one-hit wonder" definition + const tracksForArtist = this.artistTracks.get(artistName); + if (tracksForArtist) tracksForArtist.add(scrobble.trackName); + else this.artistTracks.set(artistName, new Set([scrobble.trackName])); + const trackKey = TRACK_KEY(scrobble); const existing = this.trackCounts.get(trackKey); if (existing) { @@ -322,8 +329,11 @@ export class Aggregator { const daysScrobbledPercentage = (daysScrobbled / daysSinceFirstScrobble) * 100; - const oneHitWondersCount = [...this.artistCounts.values()].filter( - (c) => c === 1, + // "One-hit wonder": known for exactly one song, however many times it's + // been replayed — not an artist merely heard once (that would exclude + // someone you've played the same single track by 50 times). + const oneHitWondersCount = [...this.artistTracks.values()].filter( + (tracks) => tracks.size === 1, ).length; const oneHitWondersPercentage = this.artistCounts.size > 0 -- 2.51.2