-
Notifications
You must be signed in to change notification settings - Fork 14
/
LastfmSource.ts
155 lines (140 loc) · 6.19 KB
/
LastfmSource.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import dayjs from "dayjs";
import EventEmitter from "events";
import { TrackObject, UserGetRecentTracksResponse } from "lastfm-node-client";
import request from "superagent";
import { PlayObject, SOURCE_SOT } from "../../core/Atomic.js";
import { isNodeNetworkException } from "../common/errors/NodeErrors.js";
import { FormatPlayObjectOptions, InternalConfig } from "../common/infrastructure/Atomic.js";
import { LastfmSourceConfig } from "../common/infrastructure/config/source/lastfm.js";
import LastfmApiClient from "../common/vendor/LastfmApiClient.js";
import { sortByOldestPlayDate } from "../utils.js";
import { RecentlyPlayedOptions } from "./AbstractSource.js";
import MemorySource from "./MemorySource.js";
export default class LastfmSource extends MemorySource {
api: LastfmApiClient;
requiresAuth = true;
requiresAuthInteraction = true;
declare config: LastfmSourceConfig;
constructor(name: any, config: LastfmSourceConfig, internal: InternalConfig, emitter: EventEmitter) {
const {
data: {
interval = 15,
maxInterval = 60,
...restData
} = {}
} = config;
super('lastfm', name, {...config, data: {interval, maxInterval, ...restData}}, internal, emitter);
this.canPoll = true;
this.canBacklog = true;
this.supportsUpstreamRecentlyPlayed = true;
this.supportsUpstreamNowPlaying = true;
this.api = new LastfmApiClient(name, {...config.data, configDir: internal.configDir, localUrl: internal.localUrl}, {logger: this.logger});
this.playerSourceOfTruth = SOURCE_SOT.HISTORY;
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 LastfmApiClient.formatPlayObj(obj, options);
}
// initialize = async () => {
// this.initialized = await this.api.initialize();
// return this.initialized;
// }
protected async doBuildInitData(): Promise<true | string | undefined> {
return await this.api.initialize();
}
protected async doCheckConnection():Promise<true | string | undefined> {
try {
await request.get('http://ws.audioscrobbler.com/2.0/');
return true;
} catch (e) {
if(isNodeNetworkException(e)) {
throw new Error('Could not communicate with Last.fm API server', {cause: e});
} else if(e.status >= 500) {
throw new Error('Last.fm API server returning an unexpected response', {cause: e})
}
return true;
}
}
doAuthentication = async () => {
try {
return await this.api.testAuth();
} catch (e) {
throw e;
}
}
getLastfmRecentTrack = async(options: RecentlyPlayedOptions = {}): Promise<[PlayObject[], PlayObject[]]> => {
const {limit = 20} = options;
const resp = await this.api.callApi<UserGetRecentTracksResponse>((client: any) => client.userGetRecentTracks({
user: this.api.user,
sk: this.api.client.sessionKey,
limit,
extended: true
}));
const {
recenttracks: {
track: list = [],
}
} = resp;
const plays = list.reduce((acc: PlayObject[], x: TrackObject) => {
try {
const formatted = LastfmApiClient.formatPlayObj(x);
const {
data: {
track,
playDate,
},
meta: {
mbid,
nowPlaying,
}
} = formatted;
if(playDate === undefined) {
if(nowPlaying === true) {
formatted.data.playDate = dayjs();
return acc.concat(formatted);
}
this.logger.warn(`Last.fm recently scrobbled track did not contain a timestamp, omitting from time frame check`, {track, mbid});
return acc;
}
return acc.concat(formatted);
} catch (e) {
this.logger.warn('Failed to format Last.fm recently scrobbled track, omitting from time frame check', {error: e.message});
this.logger.debug('Full api response object:');
this.logger.debug(x);
return acc;
}
}, []).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 = plays.filter(x => x.meta.nowPlaying !== true);
const now = plays.filter(x => x.meta.nowPlaying === true);
return [history, now];
}
getRecentlyPlayed = async(options: RecentlyPlayedOptions = {}): Promise<PlayObject[]> => {
try {
const [history, now] = await this.getLastfmRecentTrack(options);
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;
} catch (e) {
throw e;
}
}
getUpstreamNowPlaying = async (): Promise<PlayObject[]> => {
try {
const [history, now] = await this.getLastfmRecentTrack();
return now;
} catch (e) {
throw e;
}
}
protected getBackloggedPlays = async () => await this.getRecentlyPlayed({formatted: true})
}