[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.

refactor(atproto): Remove unused oauth implementation

Oauth will need a larger, ground-up rewrite if/when its eventually done. Remove the old, unusued implementation for now to reduce complexity.

FoxxMD (Jun 4, 2026, 5:54 PM UTC) 2445336e 41d181e0

+3 -178
-100
src/backend/common/vendor/atproto/ATProtoOauthApiClient.ts
··· 1 - import { AbstractApiOptions } from "../../infrastructure/Atomic.js"; 2 - import { TealClientData } from "../../infrastructure/config/client/tealfm.js"; 3 - import { 4 - NodeOAuthClient, 5 - NodeSavedStateStore, 6 - NodeSavedSessionStore, 7 - type OAuthClientMetadataInput, 8 - OAuthSession, 9 - } from "@atproto/oauth-client-node"; 10 - import { Agent } from "@atproto/api"; 11 - import { ATProtoAuthenticatedApiClient } from "./ATProtoAuthenticatedApiClient.js"; 12 - 13 - export class ATProtoOauthApiClient extends ATProtoAuthenticatedApiClient { 14 - 15 - declare config: TealClientData; 16 - 17 - oauthClient?: NodeOAuthClient; 18 - oauthSession: OAuthSession; 19 - 20 - 21 - constructor(name: any, config: TealClientData, options: AbstractApiOptions) { 22 - super(name, config, options); 23 - this.logger.verbose('Will use oauth for session'); 24 - } 25 - 26 - initClient = async () => { 27 - const sessionStore: NodeSavedSessionStore = { 28 - set: (k: string, state) => this.cache.cacheAuth.set(`session-${this.name}-${k}`, state).then(() => null), 29 - get: (k: string) => this.cache.cacheAuth.get(`session-${this.name}-${k}`), 30 - del: (k: string) => this.cache.cacheAuth.delete(`session-${this.name}-${k}`).then(() => null) 31 - } 32 - 33 - const stateStore: NodeSavedStateStore = { 34 - set: (k: string, state) => this.cache.cacheAuth.set(`state-${this.name}-${k}`, state).then(() => null), 35 - get: (k: string) => this.cache.cacheAuth.get(`state-${this.name}-${k}`), 36 - del: (k: string) => this.cache.cacheAuth.delete(`state-${this.name}-${k}`).then(() => null) 37 - } 38 - 39 - try { 40 - this.oauthClient = new NodeOAuthClient({ 41 - clientMetadata: this.getMetadata(), 42 - stateStore, 43 - sessionStore 44 - }); 45 - } catch (e) { 46 - throw new Error('Could not build oauth client', { cause: e }); 47 - } 48 - } 49 - 50 - restoreSession = async (): Promise<boolean> => { 51 - const did = await this.cache.cacheAuth.get<string>(`did-${this.name}`); 52 - if (did === undefined) { 53 - this.logger.debug('No did has been stored yet'); 54 - return false; 55 - } 56 - try { 57 - this.oauthSession = await this.oauthClient.restore(did); 58 - return true; 59 - } catch (e) { 60 - this.logger.warn(new Error('Could not restore oauth session', { cause: e })); 61 - return false; 62 - } 63 - } 64 - 65 - 66 - createAuthorizeUrl = async (handle: string) => { 67 - const url = await this.oauthClient.authorize(handle.replace('@', '')); 68 - return url.toString(); 69 - } 70 - 71 - handleCallback = async (params: URLSearchParams): Promise<boolean> => { 72 - const { session } = await this.oauthClient.callback(params); 73 - this.oauthSession = session; 74 - this.agent = new Agent(session); 75 - await this.cache.cacheAuth.set(`did-${this.name}`, session.did); 76 - return true; 77 - } 78 - 79 - getMetadata() { 80 - return generateMetadata(this.name, this.config.baseUri); 81 - } 82 - 83 - } 84 - 85 - export const generateMetadata = (name, baseUrl): OAuthClientMetadataInput => { 86 - return { 87 - client_name: name, 88 - client_id: `${baseUrl}/client-metadata.json`, 89 - client_uri: `${baseUrl}`, 90 - redirect_uris: [`${baseUrl}/oauth/callback`], 91 - policy_uri: `${baseUrl}/policy`, 92 - tos_uri: `${baseUrl}/tos`, 93 - scope: "atproto transition:generic", 94 - grant_types: ["authorization_code", "refresh_token"], 95 - response_types: ["code"], 96 - application_type: "web", 97 - token_endpoint_auth_method: "none", 98 - dpop_bound_access_tokens: true, 99 - }; 100 - }
+1 -2
src/backend/common/vendor/teal/TealApiClient.ts
··· 8 8 import { ListRecord, RecordOptions, TealClientData } from "../../infrastructure/config/client/tealfm.js"; 9 9 import AbstractApiClient from "../AbstractApiClient.js"; 10 10 import { ATProtoAppApiClient } from "../atproto/ATProtoAppApiClient.js"; 11 - import { ATProtoOauthApiClient } from "../atproto/ATProtoOauthApiClient.js"; 12 11 import { Duration } from "dayjs/plugin/duration.js"; 13 12 import { FmTealAlphaActorStatus, FmTealAlphaFeedPlay } from "./lexicons/index.js"; 14 13 import { ScrobbleSubmitError } from "../../errors/MSErrors.js"; ··· 34 33 if(config.appPassword !== undefined) { 35 34 this.client = new ATProtoAppApiClient(name, config, {...options, logger: this.logger}); 36 35 } else if(config.baseUri !== undefined) { 37 - this.client = new ATProtoOauthApiClient(name, config, {...options, logger: this.logger}); 36 + throw new Error('Oauth is not yet implemented'); 38 37 } else { 39 38 throw new Error(`Must define either 'baseUri' or 'appPassword' in configuration!`); 40 39 }
+1 -2
src/backend/scrobblers/TealfmScrobbler.ts
··· 15 15 import { nowPlayingUpdateByPlayDuration, shouldClearNPStatus } from "./AbstractScrobbleClient.js"; 16 16 import { TealClientConfig } from "../common/infrastructure/config/client/tealfm.js"; 17 17 import { ATProtoAppApiClient } from "../common/vendor/atproto/ATProtoAppApiClient.js"; 18 - import { ATProtoOauthApiClient } from "../common/vendor/atproto/ATProtoOauthApiClient.js"; 19 18 import { playToRecord, TealApiClient } from "../common/vendor/teal/TealApiClient.js"; 20 19 import { playToStatusRecord } from "../common/vendor/teal/TealApiClient.js"; 21 20 import { nowPlayingExpirationDuration } from "../common/vendor/teal/TealApiClient.js"; ··· 85 84 } 86 85 87 86 async getAuthorizeUrl(): Promise<string> { 88 - return await (this.client.client as ATProtoOauthApiClient).createAuthorizeUrl(this.config.data.identifier); 87 + throw new Error('Oauth is not yet implemented'); 89 88 } 90 89 91 90 doAuthentication = async () => {
-71
src/backend/server/auth.ts
··· 8 8 import ScrobbleSources from "../sources/ScrobbleSources.js"; 9 9 import SpotifySource from "../sources/SpotifySource.js"; 10 10 import YTMusicSource from "../sources/YTMusicSource.js"; 11 - import { sortAndDeduplicateDiagnostics } from "typescript"; 12 - import { source } from "common-tags"; 13 - import TealScrobbler from "../scrobblers/TealfmScrobbler.js"; 14 - import { parseRegexSingle } from "@foxxmd/regex-buddy-core"; 15 - import { ATProtoOauthApiClient } from "../common/vendor/atproto/ATProtoOauthApiClient.js"; 16 11 import LibrefmScrobbler from "../scrobblers/LibrefmScrobbler.js"; 17 12 import LibrefmSource from "../sources/LibrefmSource.js"; 18 13 import e from "express"; ··· 154 149 return res.send(responseContent); 155 150 } 156 151 }); 157 - 158 - app.get(/(\/api\/tealfm\/.*)/, async function (req, res) { 159 - 160 - const clients = scrobbleClients.getByType('tealfm') as TealScrobbler[]; 161 - if (clients.length === 0) { 162 - logger.warn('Received callback to Teal OAuth but no TealFM scrobble clients are configured'); 163 - } 164 - 165 - // const { 166 - // query: { 167 - // state 168 - // } = {} 169 - // } = req; 170 - 171 - // const name = (state as string).replace('tfm',''); 172 - 173 - // const validClient = clients.find(x => x.name === name); 174 - 175 - // if (validClient === undefined) { 176 - // logger.warn(`No Tealfm scrobble matched => URL: ${req.originalUrl} | State: ${state}`); 177 - // } 178 - 179 - const intents = getTealUrlIntent(req.originalUrl); 180 - 181 - if(intents === undefined) { 182 - logger.warn(`Tealfm url was not formed correctly. Should be '/api/tealfm/SCROBBLER_NAME/SOME_ACTION' but found ${req.originalUrl}`); 183 - return res.status(404); 184 - } 185 - 186 - const validClient = clients.find(x => x.name === intents[0]); 187 - if(validClient === undefined) { 188 - logger.warn(`No Tealfm client found with the name ${intents[0]}. Url: ${req.originalUrl}`); 189 - return res.status(404); 190 - } 191 - 192 - if(intents[1].includes('login')) { 193 - const { 194 - query: { 195 - handle 196 - } = {} 197 - } = req; 198 - const url = await (validClient.client.client as ATProtoOauthApiClient).createAuthorizeUrl(handle as string); 199 - res.redirect(url) 200 - } 201 - 202 - if(intents[1].includes('client-metadata.json')) { 203 - return res.json((validClient.client.client as ATProtoOauthApiClient).getMetadata()); 204 - } 205 - 206 - if(intents[1].includes('oauth/callback')) { 207 - const result = await (validClient.client.client as ATProtoOauthApiClient).handleCallback(new URLSearchParams(req.query as Record<string, string>)); 208 - if(result) { 209 - return res.status(200); 210 - } 211 - return res.status(500); 212 - } 213 - }); 214 - } 215 - 216 - const TEAL_NAME_REGEX = new RegExp(/\/api\/tealfm\/([^\/])\/(.*)/); 217 - const getTealUrlIntent = (url: string): [string, string] | undefined => { 218 - const res = parseRegexSingle(TEAL_NAME_REGEX, url); 219 - if(res === undefined) { 220 - return undefined; 221 - } 222 - return res.groups as [string, string]; 223 152 }
+1 -3
src/backend/sources/TealfmSource.ts
··· 4 4 import { FormatPlayObjectOptions, InternalConfig } from "../common/infrastructure/Atomic.js"; 5 5 import { RecentlyPlayedOptions } from "./AbstractSource.js"; 6 6 import MemorySource from "./MemorySource.js"; 7 - import { AbstractATProtoApiClient } from "../common/vendor/atproto/AbstractATProtoApiClient.js"; 8 - import { listRecordToPlay, TealApiClient } from "../common/vendor/teal/TealApiClient.js"; 7 + import { TealApiClient } from "../common/vendor/teal/TealApiClient.js"; 9 8 import { recordToPlay } from "../common/vendor/teal/TealApiClient.js"; 10 9 import { TealSourceConfig } from "../common/infrastructure/config/source/tealfm.js"; 11 10 import { ATProtoAppApiClient } from "../common/vendor/atproto/ATProtoAppApiClient.js"; 12 - import { ATProtoOauthApiClient } from "../common/vendor/atproto/ATProtoOauthApiClient.js"; 13 11 import { parseArrayFromMaybeString } from "../utils/StringUtils.js"; 14 12 15 13 export default class TealfmSource extends MemorySource {