import type EventEmitter from "events"; import { COMPONENT_AUTH_TYPE, PARSED_FROM, type PlayObject, SOURCE_SOT } from "../../core/Atomic.ts"; import type {FormatPlayObjectOptions, InternalConfig, TimeRangeListensFetcher} from "../common/infrastructure/Atomic.ts"; import type {ComponentAuthType, PlayPlatformId} from '../../core/Atomic.ts'; import type {SourceType} from "../../core/Atomic.ts"; import type {LastfmSourceConfig} from "../common/infrastructure/config/source/lastfm.ts"; import LastfmApiClient, { formatPlayObj } from "../common/vendor/LastfmApiClient.ts"; import { sortByOldestPlayDate } from "../utils.ts"; import type {RecentlyPlayedOptions} from "./AbstractSource.ts"; import MemorySource from "./MemorySource.ts"; import type {Logger} from "@foxxmd/logging"; import type {PlayerStateOptions} from "./PlayerState/AbstractPlayerState.ts"; import { NowPlayingPlayerState } from "./PlayerState/NowPlayingPlayerState.ts"; import { createGetScrobblesForTimeRangeFunc } from "../utils/ListenFetchUtils.ts"; export default class LastfmSource extends MemorySource { api: LastfmApiClient; override authType: ComponentAuthType = COMPONENT_AUTH_TYPE.interactive; requiresAuth = true; requiresAuthInteraction = true; upstreamType: string = 'Last.fm'; getScrobblesForTimeRange: TimeRangeListensFetcher protected internalOptions: InternalConfig; declare config: LastfmSourceConfig; constructor(name: any, config: LastfmSourceConfig, internal: InternalConfig, emitter: EventEmitter, type: SourceType = 'lastfm') { const { data: { interval = 30, maxInterval = 60, ...restData } = {} } = config; super(type, name, {...config, data: {interval, maxInterval, ...restData}}, internal, emitter); this.canPoll = true; this.canBacklog = true; this.internalOptions = internal; this.supportsUpstreamRecentlyPlayed = true; this.supportsUpstreamNowPlaying = true; this.playerSourceOfTruth = SOURCE_SOT.HISTORY; // https://www.last.fm/api/show/user.getRecentTracks this.SCROBBLE_BACKLOG_COUNT = 200; this.logger.info(`Note: The player for this source is an analogue for the 'Now Playing' status exposed by ${this.type} which is NOT used for scrobbling. Instead, the 'recently played' or 'history' information provided by this source is used for scrobbles.`); } static formatPlayObj(obj: any, options: FormatPlayObjectOptions = {}): PlayObject { return formatPlayObj(obj, options); } protected async doBuildInitData(): Promise { this.api = new LastfmApiClient(this.name, this.config.data, {logger: this.logger, type: 'lastfm', ...this.internalOptions}); this.getScrobblesForTimeRange = createGetScrobblesForTimeRangeFunc(this.api, this.api.logger); return await this.api.initialize(); } protected async doCheckConnection(): Promise { try { await this.api.testConnection(); return true; } catch (e) { throw e; } } doAuthentication = async () => { try { return await this.api.testAuth(); } catch (e) { throw e; } } getLastfmRecentTrack = async(options: RecentlyPlayedOptions = {}): Promise<[PlayObject[], PlayObject[]]> => { const {limit = 20} = options; try { const {data: plays} = await this.api.getPaginatedTimeRangeListens({limit, cursor: 1}, {includeNowPlaying: true}); const mappedPlayed: PlayObject[] = plays.map(x => ({...x, meta: {...x.meta, sourceSOT: SOURCE_SOT.HISTORY, parsedFrom: PARSED_FROM.history}})); mappedPlayed.sort(sortByOldestPlayDate); // if the track is "now playing" it doesn't get a timestamp so we can't determine when it started playing // and don't want to accidentally count the same track at different timestamps by artificially assigning it 'now' as a timestamp // so we'll just ignore it in the context of recent tracks since really we only want "tracks that have already finished being played" anyway const history = mappedPlayed.filter(x => x.meta.nowPlaying !== true); const now = mappedPlayed.filter(x => x.meta.nowPlaying === true); return [history, now]; } catch (e) { throw e; } } getRecentlyPlayed = async(options: RecentlyPlayedOptions = {}): Promise => { try { this.setStatus('Checking for new Plays'); const [history, now] = await this.getLastfmRecentTrack(options); await this.processRecentPlays(now); return history; } catch (e) { throw e; } } getUpstreamRecentlyPlayed = async (options: RecentlyPlayedOptions = {}): Promise => { try { const [history, now] = await this.getLastfmRecentTrack(options); return history.map((x) => ({...x, meta: {...x.meta, parsedFrom: PARSED_FROM.history}})); } catch (e) { throw e; } } getUpstreamNowPlaying = async (): Promise => { try { const [history, now] = await this.getLastfmRecentTrack(); return now; } catch (e) { throw e; } } protected getBackloggedPlays = async (options: RecentlyPlayedOptions = {}) => await this.getRecentlyPlayed({formatted: true, ...options}) getNewPlayer = (logger: Logger, id: PlayPlatformId, opts: PlayerStateOptions) => new NowPlayingPlayerState(logger, id, opts); }