From 2d97613616b2014d771b94e4f43b83b07f6307bc Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Mon, 21 Sep 2026 20:22:21 +0000 Subject: [PATCH] feat(rocksky): Implement now playing --- .../docs/configuration/clients/clients.mdx | 2 +- src/backend/common/vendor/RockSkyApiClient.ts | 24 +++++++++++++++++-- src/backend/scrobblers/RockskyScrobbler.ts | 17 +++++++++++-- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/docsite/docs/configuration/clients/clients.mdx b/docsite/docs/configuration/clients/clients.mdx index eb7dd26c..cff6a4bc 100644 --- a/docsite/docs/configuration/clients/clients.mdx +++ b/docsite/docs/configuration/clients/clients.mdx @@ -20,7 +20,7 @@ A **Client** is an application that stores the historical information about what | [Listenbrainz](/configuration/clients/listenbrainz) | ✅ | | [Koito](/configuration/clients/koito) | ✅ | | [Maloja](/configuration/clients/maloja) | ❌ | -| [Rocksky](/configuration/clients/rocksky) | ❌ | +| [Rocksky](/configuration/clients/rocksky) | ✅ | | [Teal.fm](/configuration/clients/tealfm) | ✅ | ## Features diff --git a/src/backend/common/vendor/RockSkyApiClient.ts b/src/backend/common/vendor/RockSkyApiClient.ts index 3b8f9010..30e23ef6 100644 --- a/src/backend/common/vendor/RockSkyApiClient.ts +++ b/src/backend/common/vendor/RockSkyApiClient.ts @@ -9,8 +9,8 @@ import { isPortReachableConnect, normalizeWebAddress } from '../../utils/Network import type {ListenResponse, ListenType} from '../../../core/vendor/listenbrainz/interfaces.ts'; import { getATProtoIdentifier, identifierToAtProtoHandle, isDID } from './atproto/atUtils.ts'; import { baseFormatPlayObj } from "../../utils/PlayTransformUtils.ts"; -import { AuthError, ScrobbleSubmitError } from "../errors/MSErrors.ts"; -import { type CreateScrobbleInput, RockskyClient, Agent, type SongViewDetailed, type ScrobbleInput, type ScrobbleViewBasic, RockskyError } from "@rocksky/sdk"; +import { AuthError, ScrobbleSubmitError, SimpleError } from "../errors/MSErrors.ts"; +import { type CreateScrobbleInput, RockskyClient, Agent, type SongViewDetailed, type ScrobbleInput, type ScrobbleViewBasic, RockskyError, type ActorTrackView } from "@rocksky/sdk"; import { getRoot } from "../../ioc.ts"; import type { MSCache } from "../Cache.ts"; import type {ATProtoUserIdentifierData, HandleData} from "../infrastructure/config/client/atproto.ts"; @@ -220,6 +220,16 @@ export class RockSkyApiClient extends AbstractApiClient { } } + updateNowPlaying = async (play?: PlayObject): Promise => { + if(this.rsAgent === undefined) { + throw new SimpleError('Cannot set now playing when no using user/appPassword authentication'); + } + if(play === undefined) { + await this.rsAgent.clearNowPlaying(); + } + await this.rsAgent.setNowPlaying(playToActorTrackView(play)); + } + getRockskySongMatch = async (play: PlayObject): Promise => { const input = playToMatchSongInput(play); const inputHash = hashObject(removeUndefinedKeys(input)); @@ -478,6 +488,16 @@ export const playToRockskyAgentRecord = (play: PlayObject): ScrobbleInput => { return csi; } +export const playToActorTrackView = (play: PlayObject): ActorTrackView => removeUndefinedKeys({ + name: play.data.track, + artist: artistCreditsToNames(play.data.artists).join(', '), + album: play.data.album, + albumCoverUrl: play.meta.art?.track ?? play.meta.art?.album ?? play.meta.art?.artist, + durationMs: play.data.duration, + source: play.meta.musicService, + recordingMbId: play.data.meta?.brainz?.recording + }) + const ATPROTO_URI_REGEX = new RegExp(/at:\/\/(?(?did.*?)\/app\.rocksky\.scrobble\/(?.*))/); const rockskyUriToData = (str: string): { web?: string, playId?: string, user?: string } | undefined => { diff --git a/src/backend/scrobblers/RockskyScrobbler.ts b/src/backend/scrobblers/RockskyScrobbler.ts index b6e73f73..a42dc05e 100644 --- a/src/backend/scrobblers/RockskyScrobbler.ts +++ b/src/backend/scrobblers/RockskyScrobbler.ts @@ -22,6 +22,7 @@ import { Readable } from 'stream'; import { ATProtoUnauthenticatedApiClient } from "../common/vendor/atproto/ATProtoUnauthenticatedApiClient.ts"; import { playToRepositoryCreatePlayHistoricalOpts, type RepositoryCreatePlayHistoricalOpts } from "../common/database/drizzle/repositories/PlayHistoricalRepository.ts"; import { isAbortError } from "abort-controller-x"; +import { shouldClearNPStatus } from "./AbstractScrobbleClient.ts"; export default class RockskyScrobbler extends AbstractHistoricalScrobbleClient { @@ -29,6 +30,7 @@ export default class RockskyScrobbler extends AbstractHistoricalScrobbleClient { override authType: ComponentAuthType = COMPONENT_AUTH_TYPE.unattended; requiresAuth = true; requiresAuthInteraction = false; + override nowPlayingIsRealtime: boolean = true; declare config: RockSkyClientConfig; @@ -47,7 +49,7 @@ export default class RockskyScrobbler extends AbstractHistoricalScrobbleClient { // https://listenbrainz.readthedocs.io/en/latest/users/api/core.html#get--1-user-(user_name)-listens // 1000 is way too high. maxing at 100 this.MAX_INITIAL_SCROBBLES_FETCH = 100; - this.supportsNowPlaying = false; + this.supportsNowPlaying = true; // PDS rate limit for operations is ~2/sec this.scrobbleDelay = 2000; this.configDir = options.configDir; @@ -122,8 +124,19 @@ export default class RockskyScrobbler extends AbstractHistoricalScrobbleClient { } doPlayingNow = async (data: SourcePlayerObj) => { + + const isClearing = shouldClearNPStatus(data); + + // we can avoid additional calls to PDS for clearing a status if the status is about to expire, or is already expired. + // this will usually happen if a player stops playing the last track in a queue + // -- worth doing since PDS calls have a daily rate limit + if(isClearing && (this.statusExpiresSoon() || this.statusAlreadyExpired())) { + this.npLogger.debug(`Not calling status record update because status is about to expire (or has already), expiring ${durationToHuman(dayjs.duration(dayjs().diff(this.nowPlayingExpirationDate)))}`); + return; + } + try { - await this.api.submitListen(data.play, { listenType: 'playing_now' }); + await this.api.updateNowPlaying(isClearing ? undefined : data.play); } catch (e) { throw e; } -- 2.51.2