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.
@@ -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
- }