From ba2bd0cc8a00886eff4309a546bf3b43b1a1f8f4 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Thu, 4 Jun 2026 15:12:37 +0000 Subject: [PATCH] refactor(teal): Refactor atproto api implementation out of tight teal coupling --- .../atproto/AbstractATProtoApiClient.ts | 204 +---------------- .../common/vendor/teal/TealApiClient.ts | 214 ++++++++++++++++++ src/backend/scrobblers/TealfmScrobbler.ts | 43 ++-- src/backend/server/auth.ts | 6 +- src/backend/sources/TealfmSource.ts | 23 +- src/backend/tests/tealfm/tealfm.test.ts | 3 +- 6 files changed, 258 insertions(+), 235 deletions(-) create mode 100644 src/backend/common/vendor/teal/TealApiClient.ts diff --git a/src/backend/common/vendor/atproto/AbstractATProtoApiClient.ts b/src/backend/common/vendor/atproto/AbstractATProtoApiClient.ts index da5bad1d..e0e83771 100644 --- a/src/backend/common/vendor/atproto/AbstractATProtoApiClient.ts +++ b/src/backend/common/vendor/atproto/AbstractATProtoApiClient.ts @@ -1,27 +1,13 @@ import { getRoot } from "../../../ioc.js"; -import { AbstractApiOptions, PagelessListensTimeRangeOptions, PagelessTimeRangeListens, PagelessTimeRangeListensResult } from "../../infrastructure/Atomic.js"; -import { ListRecord, TealClientData } from "../../infrastructure/config/client/tealfm.js"; +import { AbstractApiOptions } from "../../infrastructure/Atomic.js"; +import { TealClientData } from "../../infrastructure/config/client/tealfm.js"; import AbstractApiClient from "../AbstractApiClient.js"; -import { Agent, ComAtprotoRepoCreateRecord, ComAtprotoRepoListRecords, ComAtprotoRepoPutRecord } from "@atproto/api"; +import { Agent, ComAtprotoRepoListRecords } from "@atproto/api"; import { MSCache } from "../../Cache.js"; -import { BrainzMeta, PlayObject, PlayObjectLifecycleless, ScrobbleActionResult, UnixTimestamp, MBID, SourcePlayerObj } from "../../../../core/Atomic.js"; -import { musicServiceToCononical } from '../listenbrainz/lzUtils.js'; -import { parseRegexSingle } from "@foxxmd/regex-buddy-core"; -import { RecordOptions } from "../../infrastructure/config/client/tealfm.js"; -import dayjs, { Dayjs, ManipulateType } from "dayjs"; -import { getScrobbleTsSOCDateWithContext, usecToUnix } from "../../../utils/TimeUtils.js"; -import { removeUndefinedKeys } from "../../../utils.js"; -import { baseFormatPlayObj } from "../../../utils/PlayTransformUtils.js"; -import { ScrobbleSubmitError } from "../../errors/MSErrors.js"; import { UpstreamError } from "../../errors/UpstreamError.js"; -import { decodeTid, generateTID } from '@ewanc26/tid'; -import { Duration } from "dayjs/plugin/duration.js"; import { streamBodyProgress } from "../../../utils/NetworkUtils.js"; -import { FmTealAlphaActorStatus, FmTealAlphaFeedPlay } from "../teal/lexicons/index.js"; -export abstract class AbstractBlueSkyApiClient extends AbstractApiClient implements PagelessTimeRangeListens { - - declare config: TealClientData; +export abstract class AbstractBlueSkyApiClient extends AbstractApiClient { agent!: Agent; @@ -37,42 +23,13 @@ export abstract class AbstractBlueSkyApiClient extends AbstractApiClient impleme abstract restoreSession(): Promise; - async createScrobbleRecord(record: FmTealAlphaFeedPlay.Main): Promise { - const input: ComAtprotoRepoCreateRecord.InputSchema = { - repo: this.agent.sessionManager.did, - collection: "fm.teal.alpha.feed.play", - record - }; - try { - const resp = await this.agent.com.atproto.repo.createRecord(input); - return {payload: input, response: resp.data}; - } catch (e) { - throw new ScrobbleSubmitError(`Failed to create record for scrobble`, { cause: e, payload: input, response: 'response' in e ? e.response : undefined }); - } - } - - async updateStatusRecord(record: FmTealAlphaActorStatus.Main): Promise { - const input: ComAtprotoRepoPutRecord.InputSchema = { - repo: this.agent.sessionManager.did, - collection: "fm.teal.alpha.actor.status", - rkey: "self", - record - }; - try { - const resp = await this.agent.com.atproto.repo.putRecord(input); - return {payload: input, response: resp.data}; - } catch (e) { - throw new ScrobbleSubmitError(`Failed to update status record for scrobble`, { cause: e, payload: input, response: 'response' in e ? e.response : undefined }); - } - } - - async listScrobbleRecord(options: {limit?: number, cursor?: string} = {}): Promise { + async listRecord(collection: string, options: {limit?: number, cursor?: string} = {}): Promise { const {limit = 20, cursor} = options; try { // records are returned newest to oldest const response = await this.agent.com.atproto.repo.listRecords({ repo: this.agent.sessionManager.did, - collection: "fm.teal.alpha.feed.play", + collection, limit, cursor // cursor TID is EXCLUSIVE IE first record returned will be the first older than cursor }); @@ -103,153 +60,4 @@ export abstract class AbstractBlueSkyApiClient extends AbstractApiClient impleme fileHint: 'repo CAR' }); } - - async getPagelessTimeRangeListens(params: PagelessListensTimeRangeOptions): Promise { - const {to, limit} = params; - - let cursor: string; - if(to !== undefined) { - cursor = generateTID(dayjs.unix(to).toISOString()); - } - - const resp = await this.listScrobbleRecord({cursor, limit}); - let fromTS: UnixTimestamp; - if(resp.data.cursor !== undefined) { - const { timestampUs } = decodeTid(resp.data.cursor); - fromTS = usecToUnix(timestampUs); - } - - const plays = (resp.data.records as unknown as ListRecord[]).map(x => listRecordToPlay(x)); - - return {data: plays, meta: {to, from: fromTS, limit}}; - } - - getPaginatedUnitOfTime(): ManipulateType { - return 'second'; - } -} - -export const playToRecord = (play: PlayObject): FmTealAlphaFeedPlay.Main => { - - const record: FmTealAlphaFeedPlay.Main = { - $type: "fm.teal.alpha.feed.play", - trackName: play.data.track, - artists: play.data.artists.map(x => removeUndefinedKeys({ artistName: x.name, artistMbId: mbidUriOrUndefined(x.mbid as MBID) })), - duration: Math.round(play.data.duration), - playedTime: getScrobbleTsSOCDateWithContext(play)[0].toISOString(), - releaseName: play.data.album, - submissionClientAgent: `multi-scrobbler/${getRoot().items.version}`, - musicServiceBaseDomain: musicServiceToCononical(play.meta.musicService) ?? play.meta.musicService, - isrc: play.data.isrc, - trackMbId: mbidUriOrUndefined(play.data.meta?.brainz?.track as MBID), - recordingMbId: mbidUriOrUndefined(play.data.meta?.brainz?.recording as MBID), - releaseMbId: mbidUriOrUndefined(play.data.meta?.brainz?.album as MBID) - }; - - return record; -} - -type MBIDURI = `mbid:${MBID}`; - -const mbidUriOrUndefined = (mbid?: MBID): undefined | MBIDURI => { - if(mbid === undefined) { - return undefined; - } - return mbidToUri(mbid); -} -export const mbidToUri = (mbid: MBID): MBIDURI => { - return `mbid:${mbid}`; -} - -export const playToStatusRecord = (play: PlayObject, notPlaying: boolean, position?: number): FmTealAlphaActorStatus.Main => { - const { $type, ...item } = notPlaying - ? { trackName: "", artists: [] } - : playToRecord(play); - - let expiry: Dayjs; - if(notPlaying) { - // if clearing status we set expiration as one minute in the past - expiry = dayjs().subtract(1, 'minute'); - } else { - expiry = dayjs().add(nowPlayingExpirationDuration({play, position})); - } - - return { - $type: "fm.teal.alpha.actor.status", - time: dayjs().toISOString(), - expiry: expiry.toISOString(), - item: { - artists: [], - ...item - } - }; -} - -export const nowPlayingExpirationDuration = (data: Pick): Duration => { - let expiry: Dayjs = dayjs().add(10, 'minute'); - - const { - position, - play - } = data; - - // if we have position and duration then expiration is set as calculated end of listening session - if(position !== undefined && play?.data.duration !== undefined) { - expiry = dayjs().add(play.data.duration - position, 'second'); - } else if(play?.data.duration !== undefined) { - // else if we have duration but not position then use track duration - expiry = dayjs().add(play.data.duration, 'second'); - } - - // otherwise use 10 minutes - return dayjs.duration(expiry.diff(dayjs(), 'ms')); } - -export const listRecordToPlay = (listRecord: ListRecord): PlayObject => { - const opts: RecordOptions = {}; - const uriRes = parseRegexSingle(ATPROTO_URI_REGEX, listRecord.uri); - if (uriRes !== undefined) { - opts.web = `https://atproto.at/viewer?uri=${uriRes.named.resource}`; - opts.playId = uriRes.named.tid; - opts.user = uriRes.named.did; - } - return recordToPlay(listRecord.value, opts); -} - -export const recordToPlay = (record: FmTealAlphaFeedPlay.Main, options: RecordOptions = {}): PlayObject => { - - const play: PlayObjectLifecycleless = { - data: { - track: record.trackName, - artists: record.artists.filter(x => x.artistName !== undefined).map(x => ({name: x.artistName, mbid: x.artistMbId})), - duration: record.duration, - playDate: dayjs(record.playedTime), - album: record.releaseName, - isrc: record.isrc - }, - meta: { - source: 'tealfm', - parsedFrom: 'history', - musicService: record.musicServiceBaseDomain, - playId: options.playId, - url: { - web: options.web - }, - user: options.user - } - }; - - const brainz = removeUndefinedKeys({ - recording: record.recordingMbId, - album: record.releaseMbId, - artist: record.artists.filter(x => x.artistMbId !== undefined).length > 0 ? record.artists.filter(x => x.artistMbId !== undefined).map(x => x.artistMbId) : undefined - }); - - if(brainz !== undefined) { - play.data.meta = {brainz}; - } - - return baseFormatPlayObj(record, play); -}; -export const ATPROTO_URI_REGEX = new RegExp(/at:\/\/(?(?did.*?)\/fm.teal.alpha.feed.play\/(?.*))/); - diff --git a/src/backend/common/vendor/teal/TealApiClient.ts b/src/backend/common/vendor/teal/TealApiClient.ts new file mode 100644 index 00000000..af223396 --- /dev/null +++ b/src/backend/common/vendor/teal/TealApiClient.ts @@ -0,0 +1,214 @@ +import dayjs, { Dayjs, ManipulateType } from "dayjs"; +import { PlayObject, PlayObjectLifecycleless, BrainzMeta, SourcePlayerObj, MBID, ScrobbleActionResult, UnixTimestamp } from "../../../../core/Atomic.js"; +import { getRoot } from "../../../ioc.js"; +import { removeUndefinedKeys } from "../../../utils.js"; +import { baseFormatPlayObj } from "../../../utils/PlayTransformUtils.js"; +import { MSCache } from "../../Cache.js"; +import { AbstractApiOptions, PagelessListensTimeRangeOptions, PagelessTimeRangeListens, PagelessTimeRangeListensResult } from "../../infrastructure/Atomic.js"; +import { ListRecord, RecordOptions, TealClientData } from "../../infrastructure/config/client/tealfm.js"; +import AbstractApiClient from "../AbstractApiClient.js"; +import { AbstractBlueSkyApiClient } from "../atproto/AbstractATProtoApiClient.js"; +import { BlueSkyAppApiClient } from "../atproto/ATProtoAppApiClient.js"; +import { BlueSkyOauthApiClient } from "../atproto/ATProtoOauthApiClient.js"; +import { Duration } from "dayjs/plugin/duration.js"; +import { FmTealAlphaActorStatus, FmTealAlphaFeedPlay } from "./lexicons/index.js"; +import { ScrobbleSubmitError } from "../../errors/MSErrors.js"; +import { ComAtprotoRepoCreateRecord, ComAtprotoRepoPutRecord } from "@atproto/api"; +import { getScrobbleTsSOCDateWithContext, usecToUnix } from "../../../utils/TimeUtils.js"; +import { musicServiceToCononical } from "../listenbrainz/lzUtils.js"; +import { parseRegexSingle } from "@foxxmd/regex-buddy-core"; +import { decodeTid, generateTID } from "@ewanc26/tid"; + +export class TealApiClient extends AbstractApiClient implements PagelessTimeRangeListens { + + declare config: TealClientData; + + declare client: AbstractBlueSkyApiClient; + + cache: MSCache; + + constructor(name: any, config: TealClientData, options: AbstractApiOptions) { + super('teal', name, config, options); + + if(config.appPassword !== undefined) { + this.client = new BlueSkyAppApiClient(name, config, {...options, logger: this.logger}); + } else if(config.baseUri !== undefined) { + this.client = new BlueSkyOauthApiClient(name, config, {...options, logger: this.logger}); + } else { + throw new Error(`Must define either 'baseUri' or 'appPassword' in configuration!`); + } + + this.cache = getRoot().items.cache(); + } + + + async createScrobbleRecord(record: FmTealAlphaFeedPlay.Main): Promise { + const input: ComAtprotoRepoCreateRecord.InputSchema = { + repo: this.client.agent.sessionManager.did, + collection: "fm.teal.alpha.feed.play", + record + }; + try { + const resp = await this.client.agent.com.atproto.repo.createRecord(input); + return {payload: input, response: resp.data}; + } catch (e) { + throw new ScrobbleSubmitError(`Failed to create record for scrobble`, { cause: e, payload: input, response: 'response' in e ? e.response : undefined }); + } + } + + async updateStatusRecord(record: FmTealAlphaActorStatus.Main): Promise { + const input: ComAtprotoRepoPutRecord.InputSchema = { + repo: this.client.agent.sessionManager.did, + collection: "fm.teal.alpha.actor.status", + rkey: "self", + record + }; + try { + const resp = await this.client.agent.com.atproto.repo.putRecord(input); + return {payload: input, response: resp.data}; + } catch (e) { + throw new ScrobbleSubmitError(`Failed to update status record for scrobble`, { cause: e, payload: input, response: 'response' in e ? e.response : undefined }); + } + } + + getPaginatedUnitOfTime(): ManipulateType { + return 'second'; + } + + async getPagelessTimeRangeListens(params: PagelessListensTimeRangeOptions): Promise { + const {to, limit} = params; + + let cursor: string; + if(to !== undefined) { + cursor = generateTID(dayjs.unix(to).toISOString()); + } + + const resp = await this.client.listRecord("fm.teal.alpha.feed.play", {cursor, limit}); + let fromTS: UnixTimestamp; + if(resp.data.cursor !== undefined) { + const { timestampUs } = decodeTid(resp.data.cursor); + fromTS = usecToUnix(timestampUs); + } + + const plays = (resp.data.records as unknown as ListRecord[]).map(x => listRecordToPlay(x)); + + return {data: plays, meta: {to, from: fromTS, limit}}; + } +} + +export const recordToPlay = (record: FmTealAlphaFeedPlay.Main, options: RecordOptions = {}): PlayObject => { + + const play: PlayObjectLifecycleless = { + data: { + track: record.trackName, + artists: record.artists.filter(x => x.artistName !== undefined).map(x => ({ name: x.artistName, mbid: x.artistMbId })), + duration: record.duration, + playDate: dayjs(record.playedTime), + album: record.releaseName, + isrc: record.isrc + }, + meta: { + source: 'tealfm', + parsedFrom: 'history', + musicService: record.musicServiceBaseDomain, + playId: options.playId, + url: { + web: options.web + }, + user: options.user + } + }; + + const brainz = removeUndefinedKeys({ + recording: record.recordingMbId, + album: record.releaseMbId, + artist: record.artists.filter(x => x.artistMbId !== undefined).length > 0 ? record.artists.filter(x => x.artistMbId !== undefined).map(x => x.artistMbId) : undefined + }); + + if (brainz !== undefined) { + play.data.meta = { brainz }; + } + + return baseFormatPlayObj(record, play); +};export const nowPlayingExpirationDuration = (data: Pick): Duration => { + let expiry: Dayjs = dayjs().add(10, 'minute'); + + const { + position, play + } = data; + + // if we have position and duration then expiration is set as calculated end of listening session + if (position !== undefined && play?.data.duration !== undefined) { + expiry = dayjs().add(play.data.duration - position, 'second'); + } else if (play?.data.duration !== undefined) { + // else if we have duration but not position then use track duration + expiry = dayjs().add(play.data.duration, 'second'); + } + + // otherwise use 10 minutes + return dayjs.duration(expiry.diff(dayjs(), 'ms')); +}; +export const playToStatusRecord = (play: PlayObject, notPlaying: boolean, position?: number): FmTealAlphaActorStatus.Main => { + const { $type, ...item } = notPlaying + ? { trackName: "", artists: [] } + : playToRecord(play); + + let expiry: Dayjs; + if (notPlaying) { + // if clearing status we set expiration as one minute in the past + expiry = dayjs().subtract(1, 'minute'); + } else { + expiry = dayjs().add(nowPlayingExpirationDuration({ play, position })); + } + + return { + $type: "fm.teal.alpha.actor.status", + time: dayjs().toISOString(), + expiry: expiry.toISOString(), + item: { + artists: [], + ...item + } + }; +}; +export const mbidToUri = (mbid: MBID): MBIDURI => { + return `mbid:${mbid}`; +}; +export const mbidUriOrUndefined = (mbid?: MBID): undefined | MBIDURI => { + if (mbid === undefined) { + return undefined; + } + return mbidToUri(mbid); +}; +export type MBIDURI = `mbid:${MBID}`; +export const playToRecord = (play: PlayObject): FmTealAlphaFeedPlay.Main => { + + const record: FmTealAlphaFeedPlay.Main = { + $type: "fm.teal.alpha.feed.play", + trackName: play.data.track, + artists: play.data.artists.map(x => removeUndefinedKeys({ artistName: x.name, artistMbId: mbidUriOrUndefined(x.mbid as MBID) })), + duration: Math.round(play.data.duration), + playedTime: getScrobbleTsSOCDateWithContext(play)[0].toISOString(), + releaseName: play.data.album, + submissionClientAgent: `multi-scrobbler/${getRoot().items.version}`, + musicServiceBaseDomain: musicServiceToCononical(play.meta.musicService) ?? play.meta.musicService, + isrc: play.data.isrc, + trackMbId: mbidUriOrUndefined(play.data.meta?.brainz?.track as MBID), + recordingMbId: mbidUriOrUndefined(play.data.meta?.brainz?.recording as MBID), + releaseMbId: mbidUriOrUndefined(play.data.meta?.brainz?.album as MBID) + }; + + return record; +}; +export const listRecordToPlay = (listRecord: ListRecord): PlayObject => { + const opts: RecordOptions = {}; + const uriRes = parseRegexSingle(ATPROTO_URI_REGEX, listRecord.uri); + if (uriRes !== undefined) { + opts.web = `https://atproto.at/viewer?uri=${uriRes.named.resource}`; + opts.playId = uriRes.named.tid; + opts.user = uriRes.named.did; + } + return recordToPlay(listRecord.value, opts); +}; +export const ATPROTO_URI_REGEX = new RegExp(/at:\/\/(?(?did.*?)\/fm.teal.alpha.feed.play\/(?.*))/); + diff --git a/src/backend/scrobblers/TealfmScrobbler.ts b/src/backend/scrobblers/TealfmScrobbler.ts index b30413c4..97aaea32 100644 --- a/src/backend/scrobblers/TealfmScrobbler.ts +++ b/src/backend/scrobblers/TealfmScrobbler.ts @@ -16,7 +16,11 @@ import { nowPlayingUpdateByPlayDuration, shouldClearNPStatus } from "./AbstractS import { TealClientConfig } from "../common/infrastructure/config/client/tealfm.js"; import { BlueSkyAppApiClient } from "../common/vendor/atproto/ATProtoAppApiClient.js"; import { BlueSkyOauthApiClient } from "../common/vendor/atproto/ATProtoOauthApiClient.js"; -import { AbstractBlueSkyApiClient, nowPlayingExpirationDuration, playToRecord, playToStatusRecord, recordToPlay } from "../common/vendor/atproto/AbstractATProtoApiClient.js"; +import { AbstractBlueSkyApiClient } from "../common/vendor/atproto/AbstractATProtoApiClient.js"; +import { playToRecord, TealApiClient } from "../common/vendor/teal/TealApiClient.js"; +import { playToStatusRecord } from "../common/vendor/teal/TealApiClient.js"; +import { nowPlayingExpirationDuration } from "../common/vendor/teal/TealApiClient.js"; +import { recordToPlay } from "../common/vendor/teal/TealApiClient.js"; import dayjs, { Dayjs } from "dayjs"; import { durationToHuman, isDebugMode } from "../utils.js"; import AbstractHistoricalScrobbleClient from "./AbstractHistoricalScrobbleClient.js"; @@ -36,21 +40,22 @@ export default class TealScrobbler extends AbstractHistoricalScrobbleClient { protected configDir: string; - client: AbstractBlueSkyApiClient; + client: TealApiClient; constructor(name: any, config: TealClientConfig, options: InternalConfigOptional & {[key: string]: any}, notifier: Notifiers, emitter: EventEmitter, logger: Logger) { super('tealfm', name, config, notifier, emitter, logger); this.MAX_INITIAL_SCROBBLES_FETCH = 20; this.scrobbleDelay = 1500; this.supportsNowPlaying = true; - if(config.data.appPassword !== undefined) { - this.client = new BlueSkyAppApiClient(name, config.data, {...options, logger}); - this.requiresAuthInteraction = false; - } else if(config.data.baseUri !== undefined) { - this.client = new BlueSkyOauthApiClient(name, config.data, {...options, logger}); - } else { - throw new Error(`Must define either 'baseUri' or 'appPassword' in configuration!`); - } + this.client = new TealApiClient(name, config.data, {...options, logger}); + // if(config.data.appPassword !== undefined) { + // this.client = new BlueSkyAppApiClient(name, config.data, {...options, logger}); + // this.requiresAuthInteraction = false; + // } else if(config.data.baseUri !== undefined) { + // this.client = new BlueSkyOauthApiClient(name, config.data, {...options, logger}); + // } else { + // throw new Error(`Must define either 'baseUri' or 'appPassword' in configuration!`); + // } this.nowPlayingMaxThreshold = nowPlayingUpdateByPlayDuration; this.nowPlayingMinThreshold = (_) => 20; this.configDir = options.configDir; @@ -72,14 +77,14 @@ export default class TealScrobbler extends AbstractHistoricalScrobbleClient { if (identifier === undefined) { throw new Error('Must provide an identifier'); } - await this.client.initClient(); + await this.client.client.initClient(); return true; } protected async doCheckConnection(): Promise { - if (this.client instanceof BlueSkyAppApiClient) { + if (this.client.client instanceof BlueSkyAppApiClient) { try { - return await this.client.checkPds(); + return await this.client.client.checkPds(); } catch (e) { throw e; } @@ -89,18 +94,18 @@ export default class TealScrobbler extends AbstractHistoricalScrobbleClient { } async getAuthorizeUrl(): Promise { - return await (this.client as BlueSkyOauthApiClient).createAuthorizeUrl(this.config.data.identifier); + return await (this.client.client as BlueSkyOauthApiClient).createAuthorizeUrl(this.config.data.identifier); } doAuthentication = async () => { try { - const sessionRes = await this.client.restoreSession(); + const sessionRes = await this.client.client.restoreSession(); if(sessionRes) { return true; } - if(this.client instanceof BlueSkyAppApiClient) { - const res = await this.client.appLogin(); + if(this.client.client instanceof BlueSkyAppApiClient) { + const res = await this.client.client.appLogin(); return res; } } catch (e) { @@ -206,7 +211,7 @@ export default class TealScrobbler extends AbstractHistoricalScrobbleClient { // TODO use `since` to get CAR diff instead of entire repo // can use last import date from migrations table const filename = path.resolve(this.configDir, `${this.getSafeExternalId()}-${dayjs().unix()}.car`); - await fsPromise.writeFile(filename, Buffer.from(((await this.client.getCAR())))); + await fsPromise.writeFile(filename, Buffer.from(((await this.client.client.getCAR())))); return filename; } @@ -222,7 +227,7 @@ export default class TealScrobbler extends AbstractHistoricalScrobbleClient { await using repo = fromStream(stream); - const did = this.client?.agent?.sessionManager?.did; + const did = this.client?.client?.agent?.sessionManager?.did; let batch: RepositoryCreatePlayHistoricalOpts[] = []; let allGood = true; diff --git a/src/backend/server/auth.ts b/src/backend/server/auth.ts index f31bccc0..d68ee971 100644 --- a/src/backend/server/auth.ts +++ b/src/backend/server/auth.ts @@ -195,16 +195,16 @@ export const setupAuthRoutes = (app: Express, logger: Logger, sourceMiddle: Expr handle } = {} } = req; - const url = await (validClient.client as BlueSkyOauthApiClient).createAuthorizeUrl(handle as string); + const url = await (validClient.client.client as BlueSkyOauthApiClient).createAuthorizeUrl(handle as string); res.redirect(url) } if(intents[1].includes('client-metadata.json')) { - return res.json((validClient.client as BlueSkyOauthApiClient).getMetadata()); + return res.json((validClient.client.client as BlueSkyOauthApiClient).getMetadata()); } if(intents[1].includes('oauth/callback')) { - const result = await (validClient.client as BlueSkyOauthApiClient).handleCallback(new URLSearchParams(req.query as Record)); + const result = await (validClient.client.client as BlueSkyOauthApiClient).handleCallback(new URLSearchParams(req.query as Record)); if(result) { return res.status(200); } diff --git a/src/backend/sources/TealfmSource.ts b/src/backend/sources/TealfmSource.ts index d38fd1b0..58ae0c0c 100644 --- a/src/backend/sources/TealfmSource.ts +++ b/src/backend/sources/TealfmSource.ts @@ -4,7 +4,9 @@ import { isNodeNetworkException } from "../common/errors/NodeErrors.js"; import { FormatPlayObjectOptions, InternalConfig } from "../common/infrastructure/Atomic.js"; import { RecentlyPlayedOptions } from "./AbstractSource.js"; import MemorySource from "./MemorySource.js"; -import { AbstractBlueSkyApiClient, listRecordToPlay, recordToPlay } from "../common/vendor/atproto/AbstractATProtoApiClient.js"; +import { AbstractBlueSkyApiClient } from "../common/vendor/atproto/AbstractATProtoApiClient.js"; +import { listRecordToPlay, TealApiClient } from "../common/vendor/teal/TealApiClient.js"; +import { recordToPlay } from "../common/vendor/teal/TealApiClient.js"; import { TealSourceConfig } from "../common/infrastructure/config/source/tealfm.js"; import { BlueSkyAppApiClient } from "../common/vendor/atproto/ATProtoAppApiClient.js"; import { BlueSkyOauthApiClient } from "../common/vendor/atproto/ATProtoOauthApiClient.js"; @@ -12,7 +14,7 @@ import { parseArrayFromMaybeString } from "../utils/StringUtils.js"; export default class TealfmSource extends MemorySource { - client: AbstractBlueSkyApiClient; + client: TealApiClient; requiresAuth = true; requiresAuthInteraction = false; @@ -32,14 +34,7 @@ export default class TealfmSource extends MemorySource { super('tealfm', name, {...config, data: {interval, maxInterval, ...restData}}, internal, emitter); this.canPoll = true; this.canBacklog = true; - if(config.data.appPassword !== undefined) { - this.client = new BlueSkyAppApiClient(name, config.data, {...internal, logger: internal.logger}); - this.requiresAuthInteraction = false; - } else if(config.data.baseUri !== undefined) { - this.client = new BlueSkyOauthApiClient(name, config.data, {...internal, logger: internal.logger}); - } else { - throw new Error(`Must define either 'baseUri' or 'appPassword' in configuration!`); - } + this.client = new TealApiClient(name, config.data, {...internal, logger: internal.logger}); this.playerSourceOfTruth = SOURCE_SOT.HISTORY; this.supportsUpstreamRecentlyPlayed = true this.SCROBBLE_BACKLOG_COUNT = 20; @@ -59,7 +54,7 @@ export default class TealfmSource extends MemorySource { throw new Error('Must provide an identifier'); } - await this.client.initClient(); + await this.client.client.initClient(); this.serviceAllow = parseArrayFromMaybeString(serviceAllow, {lower: true}); if(this.serviceAllow.length > 0) { @@ -88,12 +83,12 @@ export default class TealfmSource extends MemorySource { doAuthentication = async () => { try { - const sessionRes = await this.client.restoreSession(); + const sessionRes = await this.client.client.restoreSession(); if(sessionRes) { return true; } - if(this.client instanceof BlueSkyAppApiClient) { - return await this.client.appLogin(); + if(this.client.client instanceof BlueSkyAppApiClient) { + return await this.client.client.appLogin(); } } catch (e) { if(isNodeNetworkException(e)) { diff --git a/src/backend/tests/tealfm/tealfm.test.ts b/src/backend/tests/tealfm/tealfm.test.ts index 162c276a..70664b4a 100644 --- a/src/backend/tests/tealfm/tealfm.test.ts +++ b/src/backend/tests/tealfm/tealfm.test.ts @@ -2,7 +2,8 @@ import chai, { expect } from 'chai'; import asPromised from 'chai-as-promised'; import { after, before, describe, it } from 'mocha'; import { generateArtistCredits, generatePlay, generateTealPlayRecord, withBrainz } from "../../../core/PlayTestUtils.js"; -import { listRecordToPlay, playToRecord } from '../../common/vendor/atproto/AbstractATProtoApiClient.js'; +import { listRecordToPlay } from "../../common/vendor/teal/TealApiClient.js"; +import { playToRecord } from "../../common/vendor/teal/TealApiClient.js"; import dayjs from 'dayjs'; import { artistCreditsToNames } from '../../../core/StringUtils.js'; import TealScrobbler from '../../scrobblers/TealfmScrobbler.js'; -- 2.51.2