Something went wrong. Try again.
[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
Something went wrong. Try again.
5.8 kB · 125 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126import 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<true | string | undefined> { 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<true | string | undefined> { 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<PlayObject[]> => { 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<PlayObject[]> => { 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<PlayObject[]> => { 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);}