diff --git a/specs/components/supplement/types.d.ts b/specs/components/supplement/types.d.ts index c73bd007..01812c6e 100644 --- a/specs/components/supplement/types.d.ts +++ b/specs/components/supplement/types.d.ts @@ -14,6 +14,7 @@ export type ScrobbleActions = { /** * @param {Track} track * @param {number} startedAt Unix timestamp in milliseconds + * @param {{ duration?: number }} [options] duration in milliseconds */ - scrobble(track: Track, startedAt: number): Promise; + scrobble(track: Track, startedAt: number, options?: { duration?: number }): Promise; }; diff --git a/src/components/configurator/scrobbles/element.js b/src/components/configurator/scrobbles/element.js index a59edd88..10aad871 100644 --- a/src/components/configurator/scrobbles/element.js +++ b/src/components/configurator/scrobbles/element.js @@ -29,10 +29,11 @@ class ScrobblesConfigurator extends DiffuseElement { /** * @param {Track} track * @param {number} startedAt Unix timestamp in milliseconds + * @param {{ duration?: number }} [options] duration in milliseconds */ - async scrobble(track, startedAt) { + async scrobble(track, startedAt, options) { return await Promise.all( - this.#activeScrobblers().map((s) => s.scrobble(track, startedAt)), + this.#activeScrobblers().map((s) => s.scrobble(track, startedAt, options)), ); } diff --git a/src/components/orchestrator/scrobble-audio/element.js b/src/components/orchestrator/scrobble-audio/element.js index bc710b54..95159758 100644 --- a/src/components/orchestrator/scrobble-audio/element.js +++ b/src/components/orchestrator/scrobble-audio/element.js @@ -204,7 +204,7 @@ class ScrobbleAudioOrchestrator extends BroadcastableDiffuseElement { const startedAt = this.#startedAt; try { - await this.scrobble?.scrobble(track, startedAt); + await this.scrobble?.scrobble(track, startedAt, { duration: Math.round(durationSec * 1000) }); } catch (err) { console.warn("Scrobble failed", err); this.#scrobbled = false; diff --git a/src/components/supplement/last.fm/element.js b/src/components/supplement/last.fm/element.js index 6823ee98..0a7d7eb8 100644 --- a/src/components/supplement/last.fm/element.js +++ b/src/components/supplement/last.fm/element.js @@ -188,8 +188,9 @@ class LastFmScrobbler extends BroadcastableDiffuseElement { /** * @param {Track} track * @param {number} startedAt Unix timestamp in milliseconds + * @param {{ duration?: number }} [_options] duration in milliseconds */ - async scrobble(track, startedAt) { + async scrobble(track, startedAt, _options) { const tags = track.tags ?? {}; /** @type {Record} */ const params = { diff --git a/src/components/supplement/listenbrainz/element.js b/src/components/supplement/listenbrainz/element.js index b7a30e14..72e6c2c2 100644 --- a/src/components/supplement/listenbrainz/element.js +++ b/src/components/supplement/listenbrainz/element.js @@ -130,8 +130,9 @@ class ListenBrainzScrobbler extends BroadcastableDiffuseElement { /** * @param {Track} track * @param {number} startedAt Unix timestamp in milliseconds + * @param {{ duration?: number }} [_options] duration in milliseconds */ - async scrobble(track, startedAt) { + async scrobble(track, startedAt, _options) { return this.#submit("single", [ { listened_at: Math.floor(startedAt / 1000), diff --git a/src/components/supplement/rocksky/element.js b/src/components/supplement/rocksky/element.js index a6a88192..a8de00f8 100644 --- a/src/components/supplement/rocksky/element.js +++ b/src/components/supplement/rocksky/element.js @@ -169,8 +169,9 @@ class RockskyScrobbler extends BroadcastableDiffuseElement { /** * @param {Track} track * @param {number} startedAt Unix timestamp in milliseconds + * @param {{ duration?: number }} [options] duration in milliseconds */ - async scrobble(track, startedAt) { + async scrobble(track, startedAt, { duration: durationMs } = {}) { if (!this.#connected.value) return; const did = localStorage.getItem(DID_STORAGE_KEY); @@ -180,31 +181,25 @@ class RockskyScrobbler extends BroadcastableDiffuseElement { const agent = new OAuthUserAgent(session); const tags = track.tags ?? {}; + const duration = track.stats?.duration ?? durationMs; - // All five fields are required by the app.rocksky.scrobble lexicon. - if ( - !tags.title || - !tags.artist || - !tags.album || - !tags.albumartist || - track.stats?.duration == null - ) return; + // duration has no meaningful fallback value; skip rather than create an invalid record + if (duration == null) return; /** @type {Record} */ const record = { $type: "app.rocksky.scrobble", - title: tags.title, - artist: tags.artist, - album: tags.album, - albumArtist: tags.albumartist, - duration: track.stats.duration, + createdAt: new Date(startedAt).toISOString(), + title: tags.title || "Unknown", + artist: tags.artist || "Unknown", + album: tags.album || "Unknown", + albumArtist: tags.albumartist || tags.artist || "Unknown", + duration, }; if (tags.track?.no != null) record.trackNumber = tags.track.no; if (tags.disc?.no != null) record.discNumber = tags.disc.no; - record.createdAt = new Date(startedAt).toISOString(); - const response = await agent.handle("/xrpc/com.atproto.repo.putRecord", { method: "POST", headers: { "Content-Type": "application/json" },