-
Notifications
You must be signed in to change notification settings - Fork 0
/
listen.ts
348 lines (327 loc) · 12.3 KB
/
listen.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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
/**
* Type definitions and utilities for ListenBrainz listens.
*
* @module
*/
/** Metadata of an audio track that was played. */
export interface Track {
/** Name of the recording artist. */
artist_name: string;
/** Name of the track. */
track_name: string;
/** The name of the release this recording was played from. */
release_name?: string;
/**
* Additional metadata you may have for a track.
* Any additional information allows ListenBrainz to better correlate your
* listen data to existing MusicBrainz-based data.
* If you have MusicBrainz IDs available, submit them!
*/
additional_info?: Partial<AdditionalTrackInfo>;
}
/**
* Additional metadata an audio track can have.
*
* Other unspecified fields that may be submitted here will not be removed, but
* ListenBrainz may decide to formally specify or scrub fields in the future.
*/
export interface AdditionalTrackInfo {
/**
* List of MusicBrainz Artist IDs, one or more IDs may be included here.
* If you have a complete MusicBrainz artist credit that contains multiple
* Artist IDs, include them all in this list.
*/
artist_mbids: string[];
/** MusicBrainz Release Group ID of the release group this recording was played from. */
release_group_mbid: string;
/** MusicBrainz Release ID of the release this recording was played from. */
release_mbid: string;
/** MusicBrainz Recording ID of the recording that was played. */
recording_mbid: string;
/** MusicBrainz Track ID associated with the recording that was played. */
track_mbid: string;
/** List of MusicBrainz Work IDs that may be associated with this recording. */
work_mbids: string[];
/** The tracknumber of the recording (first recording on a release is #1). */
tracknumber: number | string;
/** The ISRC code associated with the recording. */
isrc: string;
/**
* List of user-defined folksonomy tags to be associated with this recording.
* You may submit up to [`MAX_TAGS_PER_LISTEN`] tags and each tag may be up to
* [`MAX_TAG_SIZE`] characters large.
*
* [`MAX_TAGS_PER_LISTEN`]: https://listenbrainz.readthedocs.io/en/latest/users/api/core.html#listenbrainz.webserver.views.api_tools.MAX_TAGS_PER_LISTEN
* [`MAX_TAG_SIZE`]: https://listenbrainz.readthedocs.io/en/latest/users/api/core.html#listenbrainz.webserver.views.api_tools.MAX_TAG_SIZE
*/
tags: string[];
/**
* The name of the program being used to listen to music.
* Don’t include a version number here.
*/
media_player: string;
/** The version of the program being used to listen to music. */
media_player_version: string;
/**
* The name of the client that is being used to submit listens to ListenBrainz.
* If the media player has the ability to submit listens built-in then this
* value may be the same as {@linkcode media_player}.
* Don’t include a version number here.
*/
submission_client: string;
/** The version of the submission client. */
submission_client_version: string;
/**
* If the song being listened to comes from an online service, the canonical
* domain of this service (rather than a textual description or URL).
*
* This allows ListenBrainz to refer unambiguously to a service without
* worrying about capitalization or full/short names (such as the difference
* between “Internet Archive”, “The Internet Archive” or “Archive”).
*
* @example "archive.org"
*/
music_service: string;
/**
* If the song being listened to comes from an online service and you don’t
* know the canonical domain, a name that represents the service.
*/
music_service_name: string;
/**
* If the song of this listen comes from an online source, the URL to the
* place where it is available. This could be a Spotify URL
* (see {@linkcode spotify_id}), a YouTube video URL, a Soundcloud recording
* page URL, or the full URL to a public MP3 file.
* If there is a webpage for this song (e.g. Youtube page, Soundcloud page)
* do not try and resolve the URL to an actual audio resource.
*/
origin_url: string;
/**
* The duration of the track in milliseconds (integer).
* You should only include one of `duration_ms` or {@linkcode duration}.
*/
duration_ms: number;
/**
* The duration of the track in seconds (integer).
* You should only include one of {@linkcode duration_ms} or `duration`.
*/
duration: number;
/**
* The Spotify track URL associated with this recording.
*
* @example "http://open.spotify.com/track/1rrgWMXGCGHru5bIRxGFV0"
*/
spotify_id: string;
// The following properties are not officially documented, but used by LB.
/** Number of the medium on which the track can be found. */
discnumber: number;
/** Name of the track artist. */
artist_names: string[];
/** Name of the release artist. */
release_artist_name: string;
/** Names of the release artists. */
release_artist_names: string[];
/** The Spotify artist URLs associated with the recording artists. */
spotify_artist_ids: string[];
/** The Spotify album URL associated with the release. */
spotify_album_id: string;
/** The Spotify artist URLs associated with the album artists. */
spotify_album_artist_ids: string[];
/** The YouTube URL associated with the track. */
youtube_id: string;
/** MessyBrainz ID (should not be submitted). */
recording_msid: string;
/**
* Source of the listens.
* @deprecated Use {@linkcode media_player} or {@linkcode music_service} instead.
*/
listening_from: string;
/** Other unspecified fields can be submitted as well. */
[unspecified: string]: unknown;
}
/**
* Event of listening to a certain track at certain time.
*
* Listens should be submitted for tracks when the user has listened to half the
* track or 4 minutes of the track, whichever is lower.
* If the user hasn’t listened to 4 minutes or half the track, it doesn’t fully
* count as a listen and should not be submitted.
*/
export interface Listen {
/**
* Integer representing the Unix time when the track was listened to.
* This should be set to playback start time of the submitted track.
* The minimum accepted value for this field is [`LISTEN_MINIMUM_TS`].
*
* [`LISTEN_MINIMUM_TS`]: https://listenbrainz.readthedocs.io/en/latest/users/api/core.html#listenbrainz.listenstore.LISTEN_MINIMUM_TS
*/
listened_at: number;
/** Metadata of the track. */
track_metadata: Track;
}
/**
* Listening data which can be submitted to the ListenBrainz API.
*
* There are some limitations on the size of a submission.
* A request must be less than [`MAX_LISTEN_PAYLOAD_SIZE`] bytes, and you can
* only submit up to [`MAX_LISTENS_PER_REQUEST`] listens per request.
* Each listen may not exceed [`MAX_LISTEN_SIZE`] bytes in size.
*
* [`MAX_LISTEN_PAYLOAD_SIZE`]: https://listenbrainz.readthedocs.io/en/latest/users/api/core.html#listenbrainz.webserver.views.api_tools.MAX_LISTEN_PAYLOAD_SIZE
* [`MAX_LISTENS_PER_REQUEST`]: https://listenbrainz.readthedocs.io/en/latest/users/api/core.html#listenbrainz.webserver.views.api_tools.MAX_LISTENS_PER_REQUEST
* [`MAX_LISTEN_SIZE`]: https://listenbrainz.readthedocs.io/en/latest/users/api/core.html#listenbrainz.webserver.views.api_tools.MAX_LISTEN_SIZE
*/
export type ListenSubmission = {
/**
* Submit previously saved listens.
* Submitting multiple listens in one request is permitted.
*/
listen_type: "import";
payload: Listen[];
} | {
/**
* Submit single listen.
* Indicates that user just finished listening to track.
*/
listen_type: "single";
payload: [Listen];
} | {
/**
* Submit `playing_now` notification.
* Indicates that user just began listening to track.
*/
listen_type: "playing_now";
payload: [{
track_metadata: Track;
}];
};
/** Listen which has already been inserted into the database. */
export interface InsertedListen extends Listen {
/** Unix time when the track was inserted into the database (in seconds). */
inserted_at: number;
/** MessyBrainz ID (gets assigned to a hash of the track metadata). */
recording_msid: string;
/** MusicBrainz name of the user who submitted this listen. */
user_name: string;
/** Metadata of the track, which may have been mapped to MBIDs. */
track_metadata: Track | MappedTrack;
}
/** Track which has already been mapped to MBIDs by the server. */
export interface MappedTrack extends Track {
/** Mapping of a track to MusicBrainz identifiers. */
mbid_mapping: MusicBrainzMapping;
// brainzplayer_metadata?: { track_name: string; };
}
/** Mapping of a track to MusicBrainz identifiers. */
export interface MusicBrainzMapping {
/** Name of the mapped recording. */
recording_name?: string;
/** MusicBrainz Recording ID of the mapped recording. */
recording_mbid: string;
/** MusicBrainz Release ID of the mapped release. */
release_mbid: string;
/** List of MusicBrainz Artist IDs of the mapped recording’s artists. */
artist_mbids: string[];
/** MusicBrainz artist credit of the mapped recording. */
artists?: ArtistCredit[];
/** ID of the Cover Art Archive image. */
caa_id?: number;
/** MusicBrainz Release ID of the release whose cover art will be used. */
caa_release_mbid?: string;
/** Name of the mapped release group. */
release_group_name?: string;
/** MusicBrainz Release Group ID of the mapped release group. */
release_group_mbid?: string;
}
/** MusicBrainz artist with credited name, MBID and join phrase. */
export interface ArtistCredit {
/** Credited name of the artist. */
artist_credit_name: string;
/** MusicBrainz Artist ID of the artist. */
artist_mbid: string;
/** Join phrase between this artist and the next artist. */
join_phrase: string;
}
/** Uniquely identifiable listen of a user. */
export type UniqueListen = InsertedListen | {
listened_at: number;
recording_msid: string;
};
/**
* Creates a clean copy of the given listen.
* Removes all properties which can not be contained in a submitted listen.
*/
export function cleanListen(input: Listen | InsertedListen): Listen {
const meta = input.track_metadata;
return {
listened_at: input.listened_at,
track_metadata: {
artist_name: meta.artist_name,
track_name: meta.track_name,
release_name: meta.release_name,
additional_info: meta.additional_info,
},
};
}
/** Returns a string representation of the given listen (for logging). */
export function formatListen(
listen: Listen,
template: string = defaultListenTemplate,
): string {
const meta = listen.track_metadata;
const info = meta.additional_info ?? {};
return template.replaceAll(
/%(\w+)%/g,
(_, key) => {
if (key === "date") {
return new Date(listen.listened_at * 1000).toLocaleString("en-GB");
} else if (key === "duration") {
return formatDuration(
info.duration ??
(info.duration_ms ? info.duration_ms / 1000 : undefined),
);
} else {
const value = meta[key as keyof Track] ??
info[key as keyof AdditionalTrackInfo];
return value?.toString() ?? "";
}
},
);
}
/** Default template which is used by {@linkcode formatListen}. */
export const defaultListenTemplate =
"%date% | %duration% | %artist_name% | %track_name% | %release_name% | #%tracknumber%";
/** Formats the given duration in seconds as `mm:ss`. */
export function formatDuration(seconds?: number): string {
if (seconds) {
return `${Math.floor(seconds / 60)}:${padNum(seconds % 60, 2)}`;
} else {
return "?:??";
}
}
/** Checks whether the given JSON is a listen. */
// deno-lint-ignore no-explicit-any
export function isListen(json: any): json is Listen | InsertedListen {
const metadata = json.track_metadata;
return Number.isInteger(json.listened_at) &&
typeof metadata === "object" &&
typeof metadata.track_name === "string" &&
typeof metadata.artist_name === "string";
}
/**
* Sets the submission client name and version for the given track,
* if it is not already set or if the `overwrite` flag is enabled.
*/
export function setSubmissionClient(
track: Track,
client: { name: string; version: string; overwrite?: boolean },
) {
const info = track.additional_info ??= {};
if (client.overwrite || !info.submission_client) {
info.submission_client = client.name;
info.submission_client_version = client.version;
}
}
function padNum(value: number, maxLength: number) {
return value.toFixed().padStart(maxLength, "0");
}