musicbrainz-api 0.10.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/lib/coverartarchive-api.d.ts +31 -0
- package/lib/coverartarchive-api.js +37 -0
- package/lib/digest-auth.d.ts +2 -2
- package/lib/digest-auth.js +6 -6
- package/lib/index.d.ts +2 -0
- package/lib/index.js +19 -0
- package/lib/musicbrainz-api.d.ts +17 -14
- package/lib/musicbrainz-api.js +35 -46
- package/lib/musicbrainz.types.d.ts +8 -1
- package/lib/rate-limiter.d.ts +2 -2
- package/lib/rate-limiter.js +5 -3
- package/lib/xml/xml-isrc-list.d.ts +1 -1
- package/lib/xml/xml-recording.d.ts +2 -2
- package/package.json +18 -7
- package/.idea/checkstyle-idea.xml +0 -16
- package/.idea/inspectionProfiles/Project_Default.xml +0 -7
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/lib/digest-auth.ts +0 -101
- package/lib/musicbrainz-api.ts +0 -794
- package/lib/musicbrainz.types.ts +0 -689
- package/lib/rate-limiter.ts +0 -34
- package/lib/xml/xml-isrc-list.ts +0 -20
- package/lib/xml/xml-isrc.ts +0 -15
- package/lib/xml/xml-metadata.ts +0 -30
- package/lib/xml/xml-recording.ts +0 -19
package/lib/musicbrainz-api.ts
DELETED
|
@@ -1,794 +0,0 @@
|
|
|
1
|
-
import * as assert from 'assert';
|
|
2
|
-
|
|
3
|
-
import { StatusCodes as HttpStatus, getReasonPhrase } from 'http-status-codes';
|
|
4
|
-
import * as Url from 'url';
|
|
5
|
-
import * as Debug from 'debug';
|
|
6
|
-
|
|
7
|
-
export { XmlMetadata } from './xml/xml-metadata';
|
|
8
|
-
export { XmlIsrc } from './xml/xml-isrc';
|
|
9
|
-
export { XmlIsrcList } from './xml/xml-isrc-list';
|
|
10
|
-
export { XmlRecording } from './xml/xml-recording';
|
|
11
|
-
|
|
12
|
-
import { XmlMetadata } from './xml/xml-metadata';
|
|
13
|
-
import { DigestAuth } from './digest-auth';
|
|
14
|
-
|
|
15
|
-
import { RateLimiter } from './rate-limiter';
|
|
16
|
-
import * as mb from './musicbrainz.types';
|
|
17
|
-
|
|
18
|
-
import got, { Options } from 'got';
|
|
19
|
-
import * as tough from 'tough-cookie';
|
|
20
|
-
|
|
21
|
-
export * from './musicbrainz.types';
|
|
22
|
-
|
|
23
|
-
import { promisify } from 'util';
|
|
24
|
-
|
|
25
|
-
const retries = 3;
|
|
26
|
-
|
|
27
|
-
/*
|
|
28
|
-
* https://musicbrainz.org/doc/Development/XML_Web_Service/Version_2#Subqueries
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
export type RelationsIncludes =
|
|
32
|
-
'area-rels'
|
|
33
|
-
| 'artist-rels'
|
|
34
|
-
| 'event-rels'
|
|
35
|
-
| 'instrument-rels'
|
|
36
|
-
| 'label-rels'
|
|
37
|
-
| 'place-rels'
|
|
38
|
-
| 'recording-rels'
|
|
39
|
-
| 'release-rels'
|
|
40
|
-
| 'release-group-rels'
|
|
41
|
-
| 'series-rels'
|
|
42
|
-
| 'url-rels'
|
|
43
|
-
| 'work-rels';
|
|
44
|
-
|
|
45
|
-
export type SubQueryIncludes =
|
|
46
|
-
/**
|
|
47
|
-
* include discids for all media in the releases
|
|
48
|
-
*/
|
|
49
|
-
'discids'
|
|
50
|
-
/**
|
|
51
|
-
* include media for all releases, this includes the # of tracks on each medium and its format.
|
|
52
|
-
*/
|
|
53
|
-
| 'media'
|
|
54
|
-
/**
|
|
55
|
-
* include isrcs for all recordings
|
|
56
|
-
*/
|
|
57
|
-
| 'isrcs'
|
|
58
|
-
/**
|
|
59
|
-
* include artists credits for all releases and recordings
|
|
60
|
-
*/
|
|
61
|
-
| 'artist-credits'
|
|
62
|
-
/**
|
|
63
|
-
* include only those releases where the artist appears on one of the tracks, only valid on artists in combination with `releases`
|
|
64
|
-
*/
|
|
65
|
-
| 'various-artists';
|
|
66
|
-
|
|
67
|
-
export type MiscIncludes =
|
|
68
|
-
'aliases'
|
|
69
|
-
| 'annotation'
|
|
70
|
-
| 'tags'
|
|
71
|
-
| 'genres'
|
|
72
|
-
| 'ratings'
|
|
73
|
-
| 'media';
|
|
74
|
-
|
|
75
|
-
export type AreaIncludes = MiscIncludes | RelationsIncludes;
|
|
76
|
-
|
|
77
|
-
export type ArtistIncludes =
|
|
78
|
-
MiscIncludes
|
|
79
|
-
| RelationsIncludes
|
|
80
|
-
| 'recordings'
|
|
81
|
-
| 'releases'
|
|
82
|
-
| 'release-groups'
|
|
83
|
-
| 'works';
|
|
84
|
-
|
|
85
|
-
export type CollectionIncludes =
|
|
86
|
-
MiscIncludes
|
|
87
|
-
| RelationsIncludes
|
|
88
|
-
| 'user-collections';
|
|
89
|
-
|
|
90
|
-
export type EventIncludes = MiscIncludes | RelationsIncludes;
|
|
91
|
-
|
|
92
|
-
export type GenreIncludes = MiscIncludes;
|
|
93
|
-
|
|
94
|
-
export type InstrumentIncludes = MiscIncludes | RelationsIncludes;
|
|
95
|
-
|
|
96
|
-
export type LabelIncludes =
|
|
97
|
-
MiscIncludes
|
|
98
|
-
| RelationsIncludes
|
|
99
|
-
| 'releases';
|
|
100
|
-
|
|
101
|
-
export type PlaceIncludes = MiscIncludes | RelationsIncludes;
|
|
102
|
-
|
|
103
|
-
export type RecordingIncludes =
|
|
104
|
-
MiscIncludes
|
|
105
|
-
| RelationsIncludes
|
|
106
|
-
| SubQueryIncludes
|
|
107
|
-
| 'artists'
|
|
108
|
-
| 'releases'
|
|
109
|
-
| 'isrcs';
|
|
110
|
-
|
|
111
|
-
export type ReleasesIncludes =
|
|
112
|
-
MiscIncludes
|
|
113
|
-
| SubQueryIncludes
|
|
114
|
-
| RelationsIncludes
|
|
115
|
-
| 'artists'
|
|
116
|
-
| 'collections'
|
|
117
|
-
| 'labels'
|
|
118
|
-
| 'recordings'
|
|
119
|
-
| 'release-groups';
|
|
120
|
-
|
|
121
|
-
export type ReleaseGroupIncludes =
|
|
122
|
-
MiscIncludes
|
|
123
|
-
| SubQueryIncludes
|
|
124
|
-
| RelationsIncludes
|
|
125
|
-
| 'artists'
|
|
126
|
-
| 'releases';
|
|
127
|
-
|
|
128
|
-
export type SeriesIncludes = MiscIncludes | RelationsIncludes;
|
|
129
|
-
|
|
130
|
-
export type WorkIncludes = MiscIncludes | RelationsIncludes;
|
|
131
|
-
|
|
132
|
-
export type UrlIncludes = RelationsIncludes;
|
|
133
|
-
|
|
134
|
-
const debug = Debug('musicbrainz-api');
|
|
135
|
-
|
|
136
|
-
export interface IFormData {
|
|
137
|
-
[key: string]: string | number;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export interface IMusicBrainzConfig {
|
|
141
|
-
botAccount?: {
|
|
142
|
-
username: string,
|
|
143
|
-
password: string
|
|
144
|
-
},
|
|
145
|
-
baseUrl?: string,
|
|
146
|
-
|
|
147
|
-
appName?: string,
|
|
148
|
-
appVersion?: string,
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* HTTP Proxy
|
|
152
|
-
*/
|
|
153
|
-
proxy?: string,
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* User e-mail address or application URL
|
|
157
|
-
*/
|
|
158
|
-
appContactInfo?: string
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export interface ISessionInformation {
|
|
162
|
-
csrf: {
|
|
163
|
-
sessionKey: string;
|
|
164
|
-
token: string;
|
|
165
|
-
}
|
|
166
|
-
loggedIn?: boolean;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export class MusicBrainzApi {
|
|
170
|
-
|
|
171
|
-
private static escapeText(text) {
|
|
172
|
-
let str = '';
|
|
173
|
-
for (const chr of text) {
|
|
174
|
-
// Escaping Special Characters: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
|
|
175
|
-
// ToDo: && ||
|
|
176
|
-
switch (chr) {
|
|
177
|
-
case '+':
|
|
178
|
-
case '-':
|
|
179
|
-
case '!':
|
|
180
|
-
case '(':
|
|
181
|
-
case ')':
|
|
182
|
-
case '{':
|
|
183
|
-
case '}':
|
|
184
|
-
case '[':
|
|
185
|
-
case ']':
|
|
186
|
-
case '^':
|
|
187
|
-
case '"':
|
|
188
|
-
case '~':
|
|
189
|
-
case '*':
|
|
190
|
-
case '?':
|
|
191
|
-
case ':':
|
|
192
|
-
case '\\':
|
|
193
|
-
case '/':
|
|
194
|
-
str += '\\';
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
str += chr;
|
|
198
|
-
}
|
|
199
|
-
return str;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
public readonly config: IMusicBrainzConfig = {
|
|
203
|
-
baseUrl: 'https://musicbrainz.org'
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
private rateLimiter: RateLimiter;
|
|
207
|
-
private options: Options;
|
|
208
|
-
private session: ISessionInformation;
|
|
209
|
-
|
|
210
|
-
public static fetchCsrf(html: string) {
|
|
211
|
-
return {
|
|
212
|
-
sessionKey: MusicBrainzApi.fetchValue(html, 'csrf_session_key'),
|
|
213
|
-
token: MusicBrainzApi.fetchValue(html, 'csrf_token')
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private static fetchValue(html: string, key: string) {
|
|
218
|
-
let pos = html.indexOf(`name="${key}"`);
|
|
219
|
-
if (pos >= 0) {
|
|
220
|
-
pos = html.indexOf('value="', pos + key.length + 7);
|
|
221
|
-
if (pos >= 0) {
|
|
222
|
-
pos += 7;
|
|
223
|
-
const endValuePos = html.indexOf('"', pos);
|
|
224
|
-
const value = html.substring(pos, endValuePos);
|
|
225
|
-
return value;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
private getCookies: (currentUrl: string | URL) => Promise<tough.Cookie[]>;
|
|
231
|
-
|
|
232
|
-
public constructor(_config?: IMusicBrainzConfig) {
|
|
233
|
-
|
|
234
|
-
Object.assign(this.config, _config);
|
|
235
|
-
|
|
236
|
-
const cookieJar = new tough.CookieJar();
|
|
237
|
-
this.getCookies = promisify(cookieJar.getCookies.bind(cookieJar));
|
|
238
|
-
|
|
239
|
-
this.options = {
|
|
240
|
-
prefixUrl: this.config.baseUrl,
|
|
241
|
-
timeout: 20 * 1000,
|
|
242
|
-
headers: {
|
|
243
|
-
'User-Agent': `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
|
|
244
|
-
},
|
|
245
|
-
cookieJar
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
this.rateLimiter = new RateLimiter(60, 50);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
public async restGet<T>(relUrl: string, query: { [key: string]: any; } = {}, attempt: number = 1): Promise<T> {
|
|
252
|
-
|
|
253
|
-
query.fmt = 'json';
|
|
254
|
-
|
|
255
|
-
let response: any;
|
|
256
|
-
await this.rateLimiter.limit();
|
|
257
|
-
do {
|
|
258
|
-
response = await got.get('ws/2' + relUrl, {
|
|
259
|
-
searchParams: query,
|
|
260
|
-
responseType: 'json',
|
|
261
|
-
...this.options
|
|
262
|
-
});
|
|
263
|
-
if (response.statusCode !== 503)
|
|
264
|
-
break;
|
|
265
|
-
debug('Rate limiter kicked in, slowing down...');
|
|
266
|
-
await RateLimiter.sleep(500);
|
|
267
|
-
} while (true);
|
|
268
|
-
|
|
269
|
-
switch (response.statusCode) {
|
|
270
|
-
case HttpStatus.OK:
|
|
271
|
-
return response.body;
|
|
272
|
-
|
|
273
|
-
case HttpStatus.BAD_REQUEST:
|
|
274
|
-
case HttpStatus.NOT_FOUND:
|
|
275
|
-
throw new Error(`Got response status ${response.statusCode}: ${getReasonPhrase(response.status)}`);
|
|
276
|
-
|
|
277
|
-
case HttpStatus.SERVICE_UNAVAILABLE: // 503
|
|
278
|
-
default:
|
|
279
|
-
const msg = `Got response status ${response.statusCode} on attempt #${attempt} (${getReasonPhrase(response.status)})`;
|
|
280
|
-
debug(msg);
|
|
281
|
-
if (attempt < retries) {
|
|
282
|
-
return this.restGet<T>(relUrl, query, attempt + 1);
|
|
283
|
-
} else
|
|
284
|
-
throw new Error(msg);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// -----------------------------------------------------------------------------------------------------------------
|
|
289
|
-
// Lookup functions
|
|
290
|
-
// -----------------------------------------------------------------------------------------------------------------
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Generic lookup function
|
|
294
|
-
* @param entity
|
|
295
|
-
* @param mbid
|
|
296
|
-
* @param inc
|
|
297
|
-
*/
|
|
298
|
-
public lookupEntity<T, I extends string = never>(entity: mb.EntityType, mbid: string, inc: I[] = []): Promise<T> {
|
|
299
|
-
return this.restGet<T>(`/${entity}/${mbid}`, {inc: inc.join(' ')});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Lookup area
|
|
304
|
-
* @param areaId Area MBID
|
|
305
|
-
* @param inc Sub-queries
|
|
306
|
-
*/
|
|
307
|
-
public lookupArea(areaId: string, inc: AreaIncludes[] = []): Promise<mb.IArea> {
|
|
308
|
-
return this.lookupEntity<mb.IArea, AreaIncludes>('area', areaId, inc);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Lookup artist
|
|
313
|
-
* @param artistId Artist MBID
|
|
314
|
-
* @param inc Sub-queries
|
|
315
|
-
*/
|
|
316
|
-
public lookupArtist(artistId: string, inc: ArtistIncludes[] = []): Promise<mb.IArtist> {
|
|
317
|
-
return this.lookupEntity<mb.IArtist, ArtistIncludes>('artist', artistId, inc);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Lookup collection
|
|
322
|
-
* @param collectionId Collection MBID
|
|
323
|
-
* @param inc List of additional information to be included about the entity. Any of the entities directly linked to the entity can be included.
|
|
324
|
-
*/
|
|
325
|
-
public lookupCollection(collectionId: string, inc: ArtistIncludes[] = []): Promise<mb.ICollection> {
|
|
326
|
-
return this.lookupEntity<mb.ICollection, ArtistIncludes>('collection', collectionId, inc);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Lookup instrument
|
|
331
|
-
* @param artistId Instrument MBID
|
|
332
|
-
* @param inc Sub-queries
|
|
333
|
-
*/
|
|
334
|
-
public lookupInstrument(instrumentId: string, inc: InstrumentIncludes[] = []): Promise<mb.IInstrument> {
|
|
335
|
-
return this.lookupEntity<mb.IInstrument, InstrumentIncludes>('instrument', instrumentId, inc);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Lookup label
|
|
340
|
-
* @param labelId Area MBID
|
|
341
|
-
* @param inc Sub-queries
|
|
342
|
-
*/
|
|
343
|
-
public lookupLabel(labelId: string, inc: LabelIncludes[] = []): Promise<mb.ILabel> {
|
|
344
|
-
return this.lookupEntity<mb.ILabel, LabelIncludes>('label', labelId, inc);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Lookup place
|
|
349
|
-
* @param placeId Area MBID
|
|
350
|
-
* @param inc Sub-queries
|
|
351
|
-
*/
|
|
352
|
-
public lookupPlace(placeId: string, inc: PlaceIncludes[] = []): Promise<mb.IPlace> {
|
|
353
|
-
return this.lookupEntity<mb.IPlace, PlaceIncludes>('place', placeId, inc);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Lookup release
|
|
358
|
-
* @param releaseId Release MBID
|
|
359
|
-
* @param inc Include: artist-credits, labels, recordings, release-groups, media, discids, isrcs (with recordings)
|
|
360
|
-
* ToDo: ['recordings', 'artists', 'artist-credits', 'isrcs', 'url-rels', 'release-groups']
|
|
361
|
-
*/
|
|
362
|
-
public lookupRelease(releaseId: string, inc: ReleasesIncludes[] = []): Promise<mb.IRelease> {
|
|
363
|
-
return this.lookupEntity<mb.IRelease, ReleasesIncludes>('release', releaseId, inc);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Lookup release-group
|
|
368
|
-
* @param releaseGroupId Release-group MBID
|
|
369
|
-
* @param inc Include: ToDo
|
|
370
|
-
*/
|
|
371
|
-
public lookupReleaseGroup(releaseGroupId: string, inc: ReleaseGroupIncludes[] = []): Promise<mb.IReleaseGroup> {
|
|
372
|
-
return this.lookupEntity<mb.IReleaseGroup, ReleaseGroupIncludes>('release-group', releaseGroupId, inc);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Lookup recording
|
|
377
|
-
* @param recordingId Label MBID
|
|
378
|
-
* @param inc Include: artist-credits, isrcs
|
|
379
|
-
*/
|
|
380
|
-
public lookupRecording(recordingId: string, inc: RecordingIncludes[] = []): Promise<mb.IRecording> {
|
|
381
|
-
return this.lookupEntity<mb.IRecording, RecordingIncludes>('recording', recordingId, inc);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Lookup work
|
|
386
|
-
* @param workId Work MBID
|
|
387
|
-
*/
|
|
388
|
-
public lookupWork(workId: string, inc: WorkIncludes[] = []): Promise<mb.IWork> {
|
|
389
|
-
return this.lookupEntity<mb.IWork, WorkIncludes>('work', workId, inc);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Lookup URL
|
|
394
|
-
* @param urlId URL MBID
|
|
395
|
-
*/
|
|
396
|
-
public lookupUrl(urlId: string, inc: UrlIncludes[] = []): Promise<mb.IUrl> {
|
|
397
|
-
return this.lookupEntity<mb.IUrl, UrlIncludes>('url', urlId, inc);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Lookup Event
|
|
402
|
-
* @param eventId Event MBID
|
|
403
|
-
* @param eventIncludes List of sub-queries to enable
|
|
404
|
-
*/
|
|
405
|
-
public lookupEvent(eventId: string, eventIncludes: EventIncludes[] = []): Promise<mb.IEvent> {
|
|
406
|
-
return this.lookupEntity<mb.IEvent, EventIncludes>('event', eventId, eventIncludes);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// -----------------------------------------------------------------------------------------------------------------
|
|
410
|
-
// Browse functions
|
|
411
|
-
// -----------------------------------------------------------------------------------------------------------------
|
|
412
|
-
// https://wiki.musicbrainz.org/MusicBrainz_API#Browse
|
|
413
|
-
// https://wiki.musicbrainz.org/MusicBrainz_API#Linked_entities
|
|
414
|
-
// For example: http://musicbrainz.org/ws/2/release?label=47e718e1-7ee4-460c-b1cc-1192a841c6e5&offset=12&limit=2
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Generic browse function
|
|
418
|
-
* https://wiki.musicbrainz.org/Development/JSON_Web_Service#Browse_Requests
|
|
419
|
-
* @param entity MusicBrainz entity
|
|
420
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
421
|
-
*/
|
|
422
|
-
public browseEntity<T>(entity: mb.EntityType, query?: { [key: string]: any; }): Promise<T> {
|
|
423
|
-
return this.restGet<T>(`/${entity}`, query);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Browse areas
|
|
428
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
429
|
-
*/
|
|
430
|
-
public browseAreas(query?: mb.IBrowseAreasQuery): Promise<mb.IBrowseAreasResult> {
|
|
431
|
-
return this.browseEntity<mb.IBrowseAreasResult>('area', query);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Browse artists
|
|
436
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
437
|
-
*/
|
|
438
|
-
public browseArtists(query?: mb.IBrowseArtistsQuery): Promise<mb.IBrowseArtistsResult> {
|
|
439
|
-
return this.browseEntity<mb.IBrowseArtistsResult>('artist', query);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Browse collections
|
|
444
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
445
|
-
*/
|
|
446
|
-
public browseCollections(query?: mb.IBrowseCollectionsQuery): Promise<mb.IBrowseCollectionsResult> {
|
|
447
|
-
return this.browseEntity<mb.IBrowseCollectionsResult>('collection', query);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Browse events
|
|
452
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
453
|
-
*/
|
|
454
|
-
public browseEvents(query?: mb.IBrowseEventsQuery): Promise<mb.IBrowseEventsResult> {
|
|
455
|
-
return this.browseEntity<mb.IBrowseEventsResult>('event', query);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Browse instruments
|
|
460
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
461
|
-
*/
|
|
462
|
-
public browseInstruments(query?: mb.IBrowseInstrumentsQuery): Promise<mb.IBrowseInstrumentsResult> {
|
|
463
|
-
return this.browseEntity<mb.IBrowseInstrumentsResult>('instrument', query);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Browse labels
|
|
468
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
469
|
-
*/
|
|
470
|
-
public browseLabels(query?: mb.IBrowseLabelsQuery): Promise<mb.IBrowseLabelsResult> {
|
|
471
|
-
return this.browseEntity<mb.IBrowseLabelsResult>('label', query);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Browse places
|
|
476
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
477
|
-
*/
|
|
478
|
-
public browsePlaces(query?: mb.IBrowsePlacesQuery): Promise<mb.IBrowsePlacesResult> {
|
|
479
|
-
return this.browseEntity<mb.IBrowsePlacesResult>('place', query);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Browse recordings
|
|
484
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
485
|
-
*/
|
|
486
|
-
public browseRecordings(query?: mb.IBrowseRecordingsQuery): Promise<mb.IBrowseRecordingsResult> {
|
|
487
|
-
return this.browseEntity<mb.IBrowseRecordingsResult>('recording', query);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Browse releases
|
|
492
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
493
|
-
*/
|
|
494
|
-
public browseReleases(query?: mb.IBrowseReleasesQuery): Promise<mb.IBrowseReleasesResult> {
|
|
495
|
-
return this.browseEntity<mb.IBrowseReleasesResult>('release', query);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Browse release-groups
|
|
500
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
501
|
-
*/
|
|
502
|
-
public browseReleaseGroups(query?: mb.IReleaseGroupsQuery): Promise<mb.IBrowseReleaseGroupsResult> {
|
|
503
|
-
return this.browseEntity<mb.IBrowseReleaseGroupsResult>('release-group', query);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Browse series
|
|
508
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
509
|
-
*/
|
|
510
|
-
public browseSeries(query?: mb.IBrowseSeriesQuery): Promise<mb.IBrowseSeriesResult> {
|
|
511
|
-
return this.browseEntity<mb.IBrowseSeriesResult>('series', query);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Browse works
|
|
516
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
517
|
-
*/
|
|
518
|
-
public browseWorks(query?: mb.IBrowseWorksQuery): Promise<mb.IBrowseWorksResult> {
|
|
519
|
-
return this.browseEntity<mb.IBrowseWorksResult>('work', query);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Browse URLs
|
|
524
|
-
* @param query Query, like: {<entity>: <MBID:}
|
|
525
|
-
*/
|
|
526
|
-
public browseUrls(query?: mb.IBrowseUrlsQuery): Promise<mb.IUrl> {
|
|
527
|
-
return this.browseEntity<mb.IUrl>('url', query);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// ---------------------------------------------------------------------------
|
|
531
|
-
|
|
532
|
-
public async postRecording(xmlMetadata: XmlMetadata): Promise<void> {
|
|
533
|
-
return this.post('recording', xmlMetadata);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
public async post(entity: mb.EntityType, xmlMetadata: XmlMetadata): Promise<void> {
|
|
537
|
-
|
|
538
|
-
if (!this.config.appName || !this.config.appVersion) {
|
|
539
|
-
throw new Error(`XML-Post requires the appName & appVersion to be defined`);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const clientId = `${this.config.appName.replace(/-/g, '.')}-${this.config.appVersion}`;
|
|
543
|
-
|
|
544
|
-
const path = `ws/2/${entity}/`;
|
|
545
|
-
// Get digest challenge
|
|
546
|
-
|
|
547
|
-
let digest: string = null;
|
|
548
|
-
let n = 1;
|
|
549
|
-
const postData = xmlMetadata.toXml();
|
|
550
|
-
|
|
551
|
-
do {
|
|
552
|
-
await this.rateLimiter.limit();
|
|
553
|
-
const response: any = await got.post(path, {
|
|
554
|
-
searchParams: {client: clientId},
|
|
555
|
-
headers: {
|
|
556
|
-
authorization: digest,
|
|
557
|
-
'Content-Type': 'application/xml'
|
|
558
|
-
},
|
|
559
|
-
body: postData,
|
|
560
|
-
throwHttpErrors: false,
|
|
561
|
-
...this.options
|
|
562
|
-
});
|
|
563
|
-
if (response.statusCode === HttpStatus.UNAUTHORIZED) {
|
|
564
|
-
// Respond to digest challenge
|
|
565
|
-
const auth = new DigestAuth(this.config.botAccount);
|
|
566
|
-
const relPath = Url.parse(response.requestUrl).path; // Ensure path is relative
|
|
567
|
-
digest = auth.digest(response.request.method, relPath, response.headers['www-authenticate']);
|
|
568
|
-
++n;
|
|
569
|
-
} else {
|
|
570
|
-
break;
|
|
571
|
-
}
|
|
572
|
-
} while (n++ < 5);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
public async login(): Promise<boolean> {
|
|
576
|
-
|
|
577
|
-
assert.ok(this.config.botAccount.username, 'bot username should be set');
|
|
578
|
-
assert.ok(this.config.botAccount.password, 'bot password should be set');
|
|
579
|
-
|
|
580
|
-
if (this.session && this.session.loggedIn) {
|
|
581
|
-
for (const cookie of await this.getCookies(this.options.prefixUrl)) {
|
|
582
|
-
if (cookie.key === 'remember_login') {
|
|
583
|
-
return true;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
this.session = await this.getSession(this.config.baseUrl);
|
|
588
|
-
|
|
589
|
-
const redirectUri = '/success';
|
|
590
|
-
|
|
591
|
-
const formData = {
|
|
592
|
-
username: this.config.botAccount.username,
|
|
593
|
-
password: this.config.botAccount.password,
|
|
594
|
-
csrf_session_key: this.session.csrf.sessionKey,
|
|
595
|
-
csrf_token: this.session.csrf.token,
|
|
596
|
-
remember_me: 1
|
|
597
|
-
};
|
|
598
|
-
|
|
599
|
-
const response: any = await got.post('login', {
|
|
600
|
-
followRedirect: false,
|
|
601
|
-
searchParams: {
|
|
602
|
-
returnto: redirectUri
|
|
603
|
-
},
|
|
604
|
-
form: formData,
|
|
605
|
-
...this.options
|
|
606
|
-
});
|
|
607
|
-
const success = response.statusCode === HttpStatus.MOVED_TEMPORARILY && response.headers.location === redirectUri;
|
|
608
|
-
if (success) {
|
|
609
|
-
this.session.loggedIn = true;
|
|
610
|
-
}
|
|
611
|
-
return success;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* Logout
|
|
616
|
-
*/
|
|
617
|
-
public async logout(): Promise<boolean> {
|
|
618
|
-
const redirectUri = '/success';
|
|
619
|
-
|
|
620
|
-
const response: any = await got.get('logout', {
|
|
621
|
-
followRedirect: false,
|
|
622
|
-
searchParams: {
|
|
623
|
-
returnto: redirectUri
|
|
624
|
-
},
|
|
625
|
-
...this.options
|
|
626
|
-
});
|
|
627
|
-
const success = response.statusCode === HttpStatus.MOVED_TEMPORARILY && response.headers.location === redirectUri;
|
|
628
|
-
if (success) {
|
|
629
|
-
this.session.loggedIn = true;
|
|
630
|
-
}
|
|
631
|
-
return success;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Submit entity
|
|
636
|
-
* @param entity Entity type e.g. 'recording'
|
|
637
|
-
* @param mbid
|
|
638
|
-
* @param formData
|
|
639
|
-
*/
|
|
640
|
-
public async editEntity(entity: mb.EntityType, mbid: string, formData: Record<string, any>): Promise<void> {
|
|
641
|
-
|
|
642
|
-
await this.rateLimiter.limit();
|
|
643
|
-
|
|
644
|
-
this.session = await this.getSession(this.config.baseUrl);
|
|
645
|
-
|
|
646
|
-
formData.csrf_session_key = this.session.csrf.sessionKey;
|
|
647
|
-
formData.csrf_token = this.session.csrf.token;
|
|
648
|
-
formData.username = this.config.botAccount.username;
|
|
649
|
-
formData.password = this.config.botAccount.password;
|
|
650
|
-
formData.remember_me = 1;
|
|
651
|
-
|
|
652
|
-
const response: any = await got.post(`${entity}/${mbid}/edit`, {
|
|
653
|
-
form: formData,
|
|
654
|
-
followRedirect: false,
|
|
655
|
-
...this.options
|
|
656
|
-
});
|
|
657
|
-
if (response.statusCode === HttpStatus.OK)
|
|
658
|
-
throw new Error(`Failed to submit form data`);
|
|
659
|
-
if (response.statusCode === HttpStatus.MOVED_TEMPORARILY)
|
|
660
|
-
return;
|
|
661
|
-
throw new Error(`Unexpected status code: ${response.statusCode}`);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* Set URL to recording
|
|
666
|
-
* @param recording Recording to update
|
|
667
|
-
* @param url2add URL to add to the recording
|
|
668
|
-
* @param editNote Edit note
|
|
669
|
-
*/
|
|
670
|
-
public async addUrlToRecording(recording: mb.IRecording, url2add: { linkTypeId: mb.LinkType, text: string }, editNote: string = '') {
|
|
671
|
-
|
|
672
|
-
const formData = {};
|
|
673
|
-
|
|
674
|
-
formData['edit-recording.name'] = recording.title; // Required
|
|
675
|
-
formData['edit-recording.comment'] = recording.disambiguation;
|
|
676
|
-
formData['edit-recording.make_votable'] = true;
|
|
677
|
-
|
|
678
|
-
formData['edit-recording.url.0.link_type_id'] = url2add.linkTypeId;
|
|
679
|
-
formData['edit-recording.url.0.text'] = url2add.text;
|
|
680
|
-
|
|
681
|
-
for (const i in recording.isrcs) {
|
|
682
|
-
formData[`edit-recording.isrcs.${i}`] = recording.isrcs[i];
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
formData['edit-recording.edit_note'] = editNote;
|
|
686
|
-
|
|
687
|
-
return this.editEntity('recording', recording.id, formData);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Add ISRC to recording
|
|
692
|
-
* @param recording Recording to update
|
|
693
|
-
* @param isrc ISRC code to add
|
|
694
|
-
* @param editNote Edit note
|
|
695
|
-
*/
|
|
696
|
-
public async addIsrc(recording: mb.IRecording, isrc: string, editNote: string = '') {
|
|
697
|
-
|
|
698
|
-
const formData = {};
|
|
699
|
-
|
|
700
|
-
formData[`edit-recording.name`] = recording.title; // Required
|
|
701
|
-
|
|
702
|
-
if (!recording.isrcs) {
|
|
703
|
-
throw new Error('You must retrieve recording with existing ISRC values');
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
if (recording.isrcs.indexOf(isrc) === -1) {
|
|
707
|
-
recording.isrcs.push(isrc);
|
|
708
|
-
|
|
709
|
-
for (const i in recording.isrcs) {
|
|
710
|
-
formData[`edit-recording.isrcs.${i}`] = recording.isrcs[i];
|
|
711
|
-
}
|
|
712
|
-
return this.editEntity('recording', recording.id, formData);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// -----------------------------------------------------------------------------------------------------------------
|
|
717
|
-
// Query functions
|
|
718
|
-
// -----------------------------------------------------------------------------------------------------------------
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* Search an entity using a search query
|
|
722
|
-
* @param query e.g.: '" artist: Madonna, track: Like a virgin"' or object with search terms: {artist: Madonna}
|
|
723
|
-
* @param entity e.g. 'recording'
|
|
724
|
-
* @param query Arguments
|
|
725
|
-
*/
|
|
726
|
-
public search<T extends mb.ISearchResult, I extends string = never>(entity: mb.EntityType, query: mb.ISearchQuery<I>): Promise<T> {
|
|
727
|
-
const urlQuery: any = {...query};
|
|
728
|
-
if (typeof query.query === 'object') {
|
|
729
|
-
urlQuery.query = makeAndQueryString(query.query);
|
|
730
|
-
}
|
|
731
|
-
if (Array.isArray(query.inc)) {
|
|
732
|
-
urlQuery.inc = urlQuery.inc.join(' ');
|
|
733
|
-
}
|
|
734
|
-
return this.restGet<T>('/' + entity + '/', urlQuery);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// -----------------------------------------------------------------------------------------------------------------
|
|
738
|
-
// Helper functions
|
|
739
|
-
// -----------------------------------------------------------------------------------------------------------------
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* Add Spotify-ID to MusicBrainz recording.
|
|
743
|
-
* This function will automatically lookup the recording title, which is required to submit the recording URL
|
|
744
|
-
* @param recording MBID of the recording
|
|
745
|
-
* @param spotifyId Spotify ID
|
|
746
|
-
* @param editNote Comment to add.
|
|
747
|
-
*/
|
|
748
|
-
public addSpotifyIdToRecording(recording: mb.IRecording, spotifyId: string, editNote: string) {
|
|
749
|
-
|
|
750
|
-
assert.strictEqual(spotifyId.length, 22);
|
|
751
|
-
|
|
752
|
-
return this.addUrlToRecording(recording, {
|
|
753
|
-
linkTypeId: mb.LinkType.stream_for_free,
|
|
754
|
-
text: 'https://open.spotify.com/track/' + spotifyId
|
|
755
|
-
}, editNote);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
public searchArea(query: mb.ISearchQuery<AreaIncludes> & mb.ILinkedEntitiesArea): Promise<mb.IAreaList> {
|
|
759
|
-
return this.search<mb.IAreaList, AreaIncludes>('area', query);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
public searchArtist(query: mb.ISearchQuery<ArtistIncludes> & mb.ILinkedEntitiesArtist): Promise<mb.IArtistList> {
|
|
763
|
-
return this.search<mb.IArtistList, ArtistIncludes>('artist', query);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
public searchRelease(query: mb.ISearchQuery<ReleasesIncludes> & mb.ILinkedEntitiesRelease): Promise<mb.IReleaseList> {
|
|
767
|
-
return this.search<mb.IReleaseList, ReleasesIncludes>('release', query);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
public searchReleaseGroup(query: mb.ISearchQuery<ReleaseGroupIncludes> & mb.ILinkedEntitiesReleaseGroup): Promise<mb.IReleaseGroupList> {
|
|
771
|
-
return this.search<mb.IReleaseGroupList, ReleaseGroupIncludes>('release-group', query);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
public searchUrl(query: mb.ISearchQuery<UrlIncludes> & mb.ILinkedEntitiesUrl): Promise<mb.IUrlList> {
|
|
775
|
-
return this.search<mb.IUrlList, UrlIncludes>('url', query);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
private async getSession(url: string): Promise<ISessionInformation> {
|
|
779
|
-
|
|
780
|
-
const response: any = await got.get('login', {
|
|
781
|
-
followRedirect: false, // Disable redirects
|
|
782
|
-
responseType: 'text',
|
|
783
|
-
...this.options
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
return {
|
|
787
|
-
csrf: MusicBrainzApi.fetchCsrf(response.body)
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
export function makeAndQueryString(keyValuePairs: IFormData): string {
|
|
793
|
-
return Object.keys(keyValuePairs).map(key => `${key}:"${keyValuePairs[key]}"`).join(' AND ');
|
|
794
|
-
}
|