[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(tealfm): Allow DID identifier

FoxxMD (Dec 11, 2025, 4:51 PM UTC) 7eabd7f4 88b81dcd

+33 -16
+12 -6
docsite/docs/configuration/clients/tealfm.mdx
··· 39 39 To view your teal.fm scrobbles you can: 40 40 41 41 * use [wrapped.baileytownsend.dev](https://wrapped.baileytownsend.dev/) which gives a simple stats overview of your scrobbles 42 + * use [teal-slice.wisp.place](https://tealfm-slice.wisp.place/) for a preview of teal.fm 42 43 * use [atproto.at](https://atproto.at) to view the raw data (convenience URLs are generated in multi-scrobbler for you to do this) 43 44 44 45 ::: ··· 54 55 55 56 ## Configuration 56 57 57 - ### Handle 58 + ### Identifier 58 59 59 - The handle used with multi-scrobbler should be your **full** ATProto handle, including TLD. 60 + The ATPRoto identifier used with multi-scrobbler should be either: 61 + 62 + * A valid [DID](https://atproto.com/specs/did#at-protocol-did-identifier-syntax), starting with `did:plc:...` or `did:web:...` 63 + * Your **full** ATProto handle, including TLD 64 + 65 + If using a handle: 60 66 61 67 * For regular Bluesky account this will be like: `alice.bsky.social` 62 - * For Bluesky accounts that [use their website as their account](https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial) this is your domain: `mydomain.com` 63 - * For non-Bluesky users, you probably already know your handle 68 + * For Bluesky accounts that [use their domain as their account](https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial) this is your domain: `mydomain.com` 69 + * For non-Bluesky-PDS users, you probably already know your handle 64 70 65 - If you do not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value. 71 + If your identifier does not look like a DID and does not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value. 66 72 67 73 <Config config="TealClientConfig" fileContent={JsonConfig} client name="tealfm"> 68 74 | Environmental Variable | Required? | Default | Description | 69 75 | :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- | 70 - | TEALFM_IDENTIFIER | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` | 76 + | TEALFM_IDENTIFIER | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` | 71 77 | TEALFM_APP_PW | Yes | | Bluesky/ATProto network App Password | 72 78 </Config>
+17 -9
src/backend/common/vendor/bluesky/BlueSkyAppApiClient.ts
··· 14 14 WellKnownHandleResolver, 15 15 } from "@atcute/identity-resolver"; 16 16 import { AtprotoDid, DidDocument } from "@atproto/oauth-client-node"; 17 - import { identifierToAtProtoHandle } from "./bsUtils.js"; 17 + import { identifierToAtProtoHandle, isDID } from "./bsUtils.js"; 18 18 19 19 interface HandleData { 20 20 did: string ··· 23 23 24 24 export class BlueSkyAppApiClient extends AbstractBlueSkyApiClient { 25 25 26 - declare config: TealClientData; 26 + declare config: TealClientData & {did?: AtprotoDid}; 27 27 appSession?: CredentialSession; 28 28 appPwAuth: boolean 29 29 ··· 31 31 constructor(name: any, config: TealClientData, options: AbstractApiOptions) { 32 32 super(name, config, options); 33 33 this.logger.verbose(`Using App Password auth for session`); 34 - this.config.identifier = identifierToAtProtoHandle(this.config.identifier, {logger: this.logger, defaultDomain: 'bsky.social'}); 34 + const cleanIdentifier = this.config.identifier; 35 + if(isDID(cleanIdentifier)) { 36 + this.logger.debug(`Identifier ${cleanIdentifier} looks like a DID, skipping parsing as a handle.`); 37 + this.config.did = cleanIdentifier; 38 + } else { 39 + this.config.identifier = identifierToAtProtoHandle(this.config.identifier, {logger: this.logger, defaultDomain: 'bsky.social'}); 40 + } 35 41 } 36 42 37 43 async initClient(): Promise<void> { ··· 64 70 }, 65 71 }); 66 72 67 - let did: AtprotoDid; 68 - try { 69 - did = await handleResolver.resolve(this.config.identifier as `${string}.${string}`); 70 - this.logger.debug(`Resolved ${did}`); 71 - } catch (e) { 72 - throw new Error('Unable to resolve handle', { cause: e }); 73 + let did: AtprotoDid = this.config.did; 74 + if(did === undefined) { 75 + try { 76 + did = await handleResolver.resolve(this.config.identifier as `${string}.${string}`); 77 + this.logger.debug(`Resolved ${did}`); 78 + } catch (e) { 79 + throw new Error('Unable to resolve handle', { cause: e }); 80 + } 73 81 } 74 82 75 83 const docResolver = new CompositeDidDocumentResolver({
+4 -1
src/backend/common/vendor/bluesky/bsUtils.ts
··· 3 3 import { Logger } from "@foxxmd/logging"; 4 4 import { parseRegexSingle } from "@foxxmd/regex-buddy-core"; 5 5 import { MaybeLogger } from "../../logging.js"; 6 - import { BskyAgent } from "@atproto/api"; 6 + import { AtprotoDid } from "@atproto/oauth-client-node"; 7 7 8 8 export const HANDLE_REGEX = new RegExp(/.+\..+/); 9 9 export const ATSIGN_REGEX = new RegExp(/^@(.+)/); 10 + export const DID_REGEX = new RegExp(/did:(?:plc|web):.+/); 10 11 11 12 export interface HandleOptions { 12 13 logger?: Logger 13 14 defaultDomain?: string 14 15 } 16 + 17 + export const isDID = (str: string): str is AtprotoDid => DID_REGEX.test(str); 15 18 16 19 export const identifierToAtProtoHandle = (str: string, options: HandleOptions = {}): Handle => { 17 20 if(isHandle(str)) {