musicbrainz-api 0.5.0 → 0.7.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.
Files changed (42) hide show
  1. package/.idea/$CACHE_FILE$ +6 -0
  2. package/.idea/$PRODUCT_WORKSPACE_FILE$ +19 -0
  3. package/.idea/checkstyle-idea.xml +16 -0
  4. package/.idea/codeStyles/Project.xml +38 -0
  5. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  6. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  7. package/.idea/misc.xml +6 -0
  8. package/.idea/modules.xml +8 -0
  9. package/.idea/shelf/Uncommitted_changes_before_Update_at_6-1-2022_11_38_[Default_Changelist]/shelved.patch +58 -0
  10. package/.idea/shelf/Uncommitted_changes_before_Update_at_6-1-2022_11_38__Default_Changelist_.xml +4 -0
  11. package/.idea/shelf/Uncommitted_changes_before_rebase_[Default_Changelist]/shelved.patch +738 -0
  12. package/.idea/shelf/Uncommitted_changes_before_rebase_[Default_Changelist]1/shelved.patch +0 -0
  13. package/.idea/shelf/Uncommitted_changes_before_rebase__Default_Changelist_.xml +4 -0
  14. package/.idea/vcs.xml +6 -0
  15. package/.idea/workspace.xml +700 -0
  16. package/README.md +284 -285
  17. package/etc/config.js +32 -0
  18. package/lib/digest-auth.d.ts +21 -21
  19. package/lib/digest-auth.js +87 -86
  20. package/lib/musicbrainz-api.d.ts +157 -139
  21. package/lib/musicbrainz-api.js +387 -369
  22. package/lib/musicbrainz.types.d.ts +253 -257
  23. package/lib/musicbrainz.types.js +16 -15
  24. package/lib/rate-limiter.d.ts +8 -8
  25. package/lib/rate-limiter.js +31 -30
  26. package/lib/xml/xml-isrc-list.d.ts +17 -17
  27. package/lib/xml/xml-isrc-list.js +22 -21
  28. package/lib/xml/xml-isrc.d.ts +10 -10
  29. package/lib/xml/xml-isrc.js +17 -16
  30. package/lib/xml/xml-metadata.d.ts +6 -6
  31. package/lib/xml/xml-metadata.js +29 -28
  32. package/lib/xml/xml-recording.d.ts +24 -24
  33. package/lib/xml/xml-recording.js +20 -19
  34. package/package.json +98 -81
  35. package/yarn-error.log +3608 -0
  36. package/.travis.yml +0 -24
  37. package/lib/xml/xml-isrc-list.js.map +0 -1
  38. package/lib/xml/xml-isrc.js.map +0 -1
  39. package/lib/xml/xml-metadata.js.map +0 -1
  40. package/lib/xml/xml-recording.js.map +0 -1
  41. package/tsconfig.json +0 -9
  42. package/tslint.json +0 -26
@@ -1,370 +1,388 @@
1
- "use strict";
2
- function __export(m) {
3
- for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
4
- }
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const assert = require("assert");
7
- const HttpStatus = require("http-status-codes");
8
- const Url = require("url");
9
- const Debug = require("debug");
10
- var xml_metadata_1 = require("./xml/xml-metadata");
11
- exports.XmlMetadata = xml_metadata_1.XmlMetadata;
12
- var xml_isrc_1 = require("./xml/xml-isrc");
13
- exports.XmlIsrc = xml_isrc_1.XmlIsrc;
14
- var xml_isrc_list_1 = require("./xml/xml-isrc-list");
15
- exports.XmlIsrcList = xml_isrc_list_1.XmlIsrcList;
16
- var xml_recording_1 = require("./xml/xml-recording");
17
- exports.XmlRecording = xml_recording_1.XmlRecording;
18
- const digest_auth_1 = require("./digest-auth");
19
- const rate_limiter_1 = require("./rate-limiter");
20
- const mb = require("./musicbrainz.types");
21
- const requestPromise = require("request-promise-native");
22
- const request = require("request");
23
- __export(require("./musicbrainz.types"));
24
- const retries = 3;
25
- const debug = Debug('musicbrainz-api');
26
- class MusicBrainzApi {
27
- constructor(_config) {
28
- this.config = {
29
- baseUrl: 'https://musicbrainz.org'
30
- };
31
- Object.assign(this.config, _config);
32
- this.cookieJar = request.jar();
33
- this.request = requestPromise.defaults({
34
- baseUrl: this.config.baseUrl,
35
- timeout: 20 * 1000,
36
- headers: {
37
- /**
38
- * https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#Provide_meaningful_User-Agent_strings
39
- */
40
- 'User-Agent': `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
41
- },
42
- proxy: this.config.proxy,
43
- strictSSL: false,
44
- jar: this.cookieJar,
45
- resolveWithFullResponse: true
46
- });
47
- this.rateLimiter = new rate_limiter_1.RateLimiter(14, 14);
48
- }
49
- static escapeText(text) {
50
- let str = '';
51
- for (const chr of text) {
52
- // Escaping Special Characters: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
53
- // ToDo: && ||
54
- switch (chr) {
55
- case '+':
56
- case '-':
57
- case '!':
58
- case '(':
59
- case ')':
60
- case '{':
61
- case '}':
62
- case '[':
63
- case ']':
64
- case '^':
65
- case '"':
66
- case '~':
67
- case '*':
68
- case '?':
69
- case ':':
70
- case '\\':
71
- case '/':
72
- str += '\\';
73
- }
74
- str += chr;
75
- }
76
- return str;
77
- }
78
- async restGet(relUrl, query = {}, attempt = 1) {
79
- query.fmt = 'json';
80
- let response;
81
- await this.rateLimiter.limit();
82
- do {
83
- response = await this.request.get('/ws/2' + relUrl, {
84
- qs: query,
85
- json: true
86
- }, null);
87
- if (response.statusCode !== 503)
88
- break;
89
- debug('Rate limiter kicked in, slowing down...');
90
- await rate_limiter_1.RateLimiter.sleep(500);
91
- } while (true);
92
- switch (response.statusCode) {
93
- case HttpStatus.OK:
94
- return response.body;
95
- case HttpStatus.BAD_REQUEST:
96
- case HttpStatus.NOT_FOUND:
97
- throw new Error(`Got response status ${response.statusCode}: ${HttpStatus.getStatusText(response.statusCode)}`);
98
- case HttpStatus.SERVICE_UNAVAILABLE: // 503
99
- default:
100
- const msg = `Got response status ${response.statusCode} on attempt #${attempt} (${HttpStatus.getStatusText(response.statusCode)})`;
101
- debug(msg);
102
- if (attempt < retries) {
103
- return this.restGet(relUrl, query, attempt + 1);
104
- }
105
- else
106
- throw new Error(msg);
107
- }
108
- }
109
- // -----------------------------------------------------------------------------------------------------------------
110
- // Lookup functions
111
- // -----------------------------------------------------------------------------------------------------------------
112
- /**
113
- * Generic lookup function
114
- * @param entity
115
- * @param mbid
116
- * @param inc
117
- */
118
- getEntity(entity, mbid, inc = []) {
119
- return this.restGet(`/${entity}/${mbid}`, { inc: inc.join(' ') });
120
- }
121
- /**
122
- * Lookup area
123
- * @param areaId Area MBID
124
- * @param inc Sub-queries
125
- */
126
- getArea(areaId, inc = []) {
127
- return this.getEntity('area', areaId, inc);
128
- }
129
- /**
130
- * Lookup artist
131
- * @param artistId Artist MBID
132
- * @param inc Sub-queries
133
- */
134
- getArtist(artistId, inc = []) {
135
- return this.getEntity('artist', artistId, inc);
136
- }
137
- /**
138
- * Lookup release
139
- * @param releaseId Release MBID
140
- * @param inc Include: artist-credits, labels, recordings, release-groups, media, discids, isrcs (with recordings)
141
- * ToDo: ['recordings', 'artists', 'artist-credits', 'isrcs', 'url-rels', 'release-groups']
142
- */
143
- getRelease(releaseId, inc = []) {
144
- return this.getEntity('release', releaseId, inc);
145
- }
146
- /**
147
- * Lookup release-group
148
- * @param releaseGroupId Release-group MBID
149
- * @param inc Include: ToDo
150
- */
151
- getReleaseGroup(releaseGroupId, inc = []) {
152
- return this.getEntity('release-group', releaseGroupId, inc);
153
- }
154
- /**
155
- * Lookup work
156
- * @param workId Work MBID
157
- */
158
- getWork(workId) {
159
- return this.getEntity('work', workId);
160
- }
161
- /**
162
- * Lookup label
163
- * @param labelId Label MBID
164
- */
165
- getLabel(labelId) {
166
- return this.getEntity('label', labelId);
167
- }
168
- /**
169
- * Lookup recording
170
- * @param recordingId Label MBID
171
- * @param inc Include: artist-credits, isrcs
172
- */
173
- getRecording(recordingId, inc = []) {
174
- return this.getEntity('recording', recordingId, inc);
175
- }
176
- async post(entity, xmlMetadata) {
177
- if (!this.config.appName || !this.config.appVersion) {
178
- throw new Error(`XML-Post requires the appName & appVersion to be defined`);
179
- }
180
- const clientId = `${this.config.appName.replace(/-/g, '.')}-${this.config.appVersion}`;
181
- const path = `/ws/2/${entity}/`;
182
- // Get digest challenge
183
- let digest = null;
184
- let n = 1;
185
- const postData = xmlMetadata.toXml();
186
- do {
187
- try {
188
- await this.rateLimiter.limit();
189
- await this.request.post(path, {
190
- qs: { client: clientId },
191
- headers: {
192
- authorization: digest,
193
- 'Content-Type': 'application/xml'
194
- },
195
- body: postData
196
- });
197
- }
198
- catch (err) {
199
- const response = err.response;
200
- assert.ok(response.complete);
201
- if (response.statusCode === HttpStatus.UNAUTHORIZED) {
202
- // Respond to digest challenge
203
- const auth = new digest_auth_1.DigestAuth(this.config.botAccount);
204
- const relPath = Url.parse(response.request.path).path; // Ensure path is relative
205
- digest = auth.digest(response.request.method, relPath, response.headers['www-authenticate']);
206
- continue;
207
- }
208
- else if (response.statusCode === 503) {
209
- continue;
210
- }
211
- break;
212
- }
213
- break;
214
- } while (n++ < 5);
215
- }
216
- async login() {
217
- const cookies = this.getCookies(this.config.baseUrl);
218
- for (const cookie of cookies) {
219
- if (cookie.key === 'musicbrainz_server_session')
220
- return true;
221
- }
222
- const redirectUri = '/success';
223
- assert.ok(this.config.botAccount.username, 'bot username should be set');
224
- assert.ok(this.config.botAccount.password, 'bot password should be set');
225
- let response;
226
- try {
227
- response = await this.request.post({
228
- uri: '/login',
229
- followRedirect: false,
230
- qs: {
231
- uri: redirectUri
232
- },
233
- form: {
234
- username: this.config.botAccount.username,
235
- password: this.config.botAccount.password
236
- }
237
- });
238
- }
239
- catch (err) {
240
- if (err.response) {
241
- assert.ok(err.response.complete);
242
- response = err.response;
243
- }
244
- else {
245
- throw err;
246
- }
247
- }
248
- assert.strictEqual(response.statusCode, HttpStatus.MOVED_TEMPORARILY, 'Expect redirect to /success');
249
- return response.headers.location === redirectUri;
250
- }
251
- /**
252
- * Submit entity
253
- * @param entity Entity type e.g. 'recording'
254
- * @param mbid
255
- * @param formData
256
- */
257
- async editEntity(entity, mbid, formData) {
258
- assert.ok(await this.login(), `should be logged in to ${this.config.botAccount.username} with username ${this.config.baseUrl}`);
259
- await this.rateLimiter.limit();
260
- let response;
261
- try {
262
- response = await this.request.post({
263
- uri: `/${entity}/${mbid}/edit`,
264
- form: formData,
265
- followRedirect: false
266
- });
267
- }
268
- catch (err) {
269
- assert.ok(err.response.complete);
270
- response = err.response;
271
- }
272
- assert.strictEqual(response.statusCode, HttpStatus.MOVED_TEMPORARILY);
273
- }
274
- /**
275
- * Set URL to recording
276
- * @param recording Recording to update
277
- * @param url2add URL to add to the recording
278
- * @param editNote Edit note
279
- */
280
- async addUrlToRecording(recording, url2add, editNote = '') {
281
- const formData = {};
282
- formData[`edit-recording.name`] = recording.title; // Required
283
- formData[`edit-recording.comment`] = recording.disambiguation;
284
- formData[`edit-recording.make_votable`] = true;
285
- formData[`edit-recording.url.0.link_type_id`] = url2add.linkTypeId;
286
- formData[`edit-recording.url.0.text`] = url2add.text;
287
- for (const i in recording.isrcs) {
288
- formData[`edit-recording.isrcs.${i}`] = recording.isrcs[i];
289
- }
290
- formData['edit-recording.edit_note'] = editNote;
291
- return this.editEntity('recording', recording.id, formData);
292
- }
293
- /**
294
- * Add ISRC to recording
295
- * @param recording Recording to update
296
- * @param isrc ISRC code to add
297
- * @param editNote Edit note
298
- */
299
- async addIsrc(recording, isrc, editNote = '') {
300
- const formData = {};
301
- formData[`edit-recording.name`] = recording.title; // Required
302
- if (!recording.isrcs) {
303
- throw new Error('You must retrieve recording with existing ISRC values');
304
- }
305
- if (recording.isrcs.indexOf(isrc) === -1) {
306
- recording.isrcs.push(isrc);
307
- for (const i in recording.isrcs) {
308
- formData[`edit-recording.isrcs.${i}`] = recording.isrcs[i];
309
- }
310
- return this.editEntity('recording', recording.id, formData);
311
- }
312
- }
313
- // -----------------------------------------------------------------------------------------------------------------
314
- // Query functions
315
- // -----------------------------------------------------------------------------------------------------------------
316
- /**
317
- * Search an entity using a search query
318
- * @param query e.g.: '" artist: Madonna, track: Like a virgin"' or object with search terms: {artist: Madonna}
319
- * @param entity e.g. 'recording'
320
- * @param offset
321
- * @param limit
322
- */
323
- search(entity, query, offset, limit) {
324
- if (typeof query === 'object') {
325
- query = makeAndQueryString(query);
326
- }
327
- return this.restGet('/' + entity + '/', { query, offset, limit });
328
- }
329
- // -----------------------------------------------------------------------------------------------------------------
330
- // Helper functions
331
- // -----------------------------------------------------------------------------------------------------------------
332
- /**
333
- * Add Spotify-ID to MusicBrainz recording.
334
- * This function will automatically lookup the recording title, which is required to submit the recording URL
335
- * @param recording MBID of the recording
336
- * @param spotifyId Spotify ID
337
- * @param editNote Comment to add.
338
- */
339
- addSpotifyIdToRecording(recording, spotifyId, editNote) {
340
- assert.strictEqual(spotifyId.length, 22);
341
- return this.addUrlToRecording(recording, {
342
- linkTypeId: mb.LinkType.stream_for_free,
343
- text: 'https://open.spotify.com/track/' + spotifyId
344
- }, editNote);
345
- }
346
- searchArtist(query, offset, limit) {
347
- return this.search('artist', query, offset, limit);
348
- }
349
- searchRelease(query, offset, limit) {
350
- return this.search('release', query, offset, limit);
351
- }
352
- searchReleaseGroup(query, offset, limit) {
353
- return this.search('release-group', query, offset, limit);
354
- }
355
- searchArea(query, offset, limit) {
356
- return this.search('area', query, offset, limit);
357
- }
358
- searchUrl(query, offset, limit) {
359
- return this.search('url', query, offset, limit);
360
- }
361
- getCookies(url) {
362
- return this.cookieJar.getCookies(url);
363
- }
364
- }
365
- exports.MusicBrainzApi = MusicBrainzApi;
366
- function makeAndQueryString(keyValuePairs) {
367
- return Object.keys(keyValuePairs).map(key => `${key}:"${keyValuePairs[key]}"`).join(' AND ');
368
- }
369
- exports.makeAndQueryString = makeAndQueryString;
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
+ }) : (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ o[k2] = m[k];
8
+ }));
9
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
10
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.makeAndQueryString = exports.MusicBrainzApi = exports.XmlRecording = exports.XmlIsrcList = exports.XmlIsrc = exports.XmlMetadata = void 0;
14
+ const assert = require("assert");
15
+ const http_status_codes_1 = require("http-status-codes");
16
+ const Url = require("url");
17
+ const Debug = require("debug");
18
+ var xml_metadata_1 = require("./xml/xml-metadata");
19
+ Object.defineProperty(exports, "XmlMetadata", { enumerable: true, get: function () { return xml_metadata_1.XmlMetadata; } });
20
+ var xml_isrc_1 = require("./xml/xml-isrc");
21
+ Object.defineProperty(exports, "XmlIsrc", { enumerable: true, get: function () { return xml_isrc_1.XmlIsrc; } });
22
+ var xml_isrc_list_1 = require("./xml/xml-isrc-list");
23
+ Object.defineProperty(exports, "XmlIsrcList", { enumerable: true, get: function () { return xml_isrc_list_1.XmlIsrcList; } });
24
+ var xml_recording_1 = require("./xml/xml-recording");
25
+ Object.defineProperty(exports, "XmlRecording", { enumerable: true, get: function () { return xml_recording_1.XmlRecording; } });
26
+ const digest_auth_1 = require("./digest-auth");
27
+ const rate_limiter_1 = require("./rate-limiter");
28
+ const mb = require("./musicbrainz.types");
29
+ const got_1 = require("got");
30
+ const tough = require("tough-cookie");
31
+ __exportStar(require("./musicbrainz.types"), exports);
32
+ const util_1 = require("util");
33
+ const retries = 3;
34
+ const debug = Debug('musicbrainz-api');
35
+ class MusicBrainzApi {
36
+ constructor(_config) {
37
+ this.config = {
38
+ baseUrl: 'https://musicbrainz.org'
39
+ };
40
+ Object.assign(this.config, _config);
41
+ const cookieJar = new tough.CookieJar();
42
+ this.getCookies = (0, util_1.promisify)(cookieJar.getCookies.bind(cookieJar));
43
+ this.options = {
44
+ prefixUrl: this.config.baseUrl,
45
+ timeout: 20 * 1000,
46
+ headers: {
47
+ 'User-Agent': `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
48
+ },
49
+ cookieJar
50
+ };
51
+ this.rateLimiter = new rate_limiter_1.RateLimiter(60, 50);
52
+ }
53
+ static escapeText(text) {
54
+ let str = '';
55
+ for (const chr of text) {
56
+ // Escaping Special Characters: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
57
+ // ToDo: && ||
58
+ switch (chr) {
59
+ case '+':
60
+ case '-':
61
+ case '!':
62
+ case '(':
63
+ case ')':
64
+ case '{':
65
+ case '}':
66
+ case '[':
67
+ case ']':
68
+ case '^':
69
+ case '"':
70
+ case '~':
71
+ case '*':
72
+ case '?':
73
+ case ':':
74
+ case '\\':
75
+ case '/':
76
+ str += '\\';
77
+ }
78
+ str += chr;
79
+ }
80
+ return str;
81
+ }
82
+ static fetchCsrf(html) {
83
+ return {
84
+ sessionKey: MusicBrainzApi.fetchValue(html, 'csrf_session_key'),
85
+ token: MusicBrainzApi.fetchValue(html, 'csrf_token')
86
+ };
87
+ }
88
+ static fetchValue(html, key) {
89
+ let pos = html.indexOf(`name="${key}"`);
90
+ if (pos >= 0) {
91
+ pos = html.indexOf('value="', pos + key.length + 7);
92
+ if (pos >= 0) {
93
+ pos += 7;
94
+ const endValuePos = html.indexOf('"', pos);
95
+ const value = html.substring(pos, endValuePos);
96
+ return value;
97
+ }
98
+ }
99
+ }
100
+ async restGet(relUrl, query = {}, attempt = 1) {
101
+ query.fmt = 'json';
102
+ let response;
103
+ await this.rateLimiter.limit();
104
+ do {
105
+ response = await got_1.default.get('ws/2' + relUrl, Object.assign({ searchParams: query, responseType: 'json' }, this.options));
106
+ if (response.statusCode !== 503)
107
+ break;
108
+ debug('Rate limiter kicked in, slowing down...');
109
+ await rate_limiter_1.RateLimiter.sleep(500);
110
+ } while (true);
111
+ switch (response.statusCode) {
112
+ case http_status_codes_1.StatusCodes.OK:
113
+ return response.body;
114
+ case http_status_codes_1.StatusCodes.BAD_REQUEST:
115
+ case http_status_codes_1.StatusCodes.NOT_FOUND:
116
+ throw new Error(`Got response status ${response.statusCode}: ${(0, http_status_codes_1.getReasonPhrase)(response.status)}`);
117
+ case http_status_codes_1.StatusCodes.SERVICE_UNAVAILABLE: // 503
118
+ default:
119
+ const msg = `Got response status ${response.statusCode} on attempt #${attempt} (${(0, http_status_codes_1.getReasonPhrase)(response.status)})`;
120
+ debug(msg);
121
+ if (attempt < retries) {
122
+ return this.restGet(relUrl, query, attempt + 1);
123
+ }
124
+ else
125
+ throw new Error(msg);
126
+ }
127
+ }
128
+ // -----------------------------------------------------------------------------------------------------------------
129
+ // Lookup functions
130
+ // -----------------------------------------------------------------------------------------------------------------
131
+ /**
132
+ * Generic lookup function
133
+ * @param entity
134
+ * @param mbid
135
+ * @param inc
136
+ */
137
+ getEntity(entity, mbid, inc = []) {
138
+ return this.restGet(`/${entity}/${mbid}`, { inc: inc.join(' ') });
139
+ }
140
+ /**
141
+ * Lookup area
142
+ * @param areaId Area MBID
143
+ * @param inc Sub-queries
144
+ */
145
+ getArea(areaId, inc = []) {
146
+ return this.getEntity('area', areaId, inc);
147
+ }
148
+ /**
149
+ * Lookup artist
150
+ * @param artistId Artist MBID
151
+ * @param inc Sub-queries
152
+ */
153
+ getArtist(artistId, inc = []) {
154
+ return this.getEntity('artist', artistId, inc);
155
+ }
156
+ /**
157
+ * Lookup release
158
+ * @param releaseId Release MBID
159
+ * @param inc Include: artist-credits, labels, recordings, release-groups, media, discids, isrcs (with recordings)
160
+ * ToDo: ['recordings', 'artists', 'artist-credits', 'isrcs', 'url-rels', 'release-groups']
161
+ */
162
+ getRelease(releaseId, inc = []) {
163
+ return this.getEntity('release', releaseId, inc);
164
+ }
165
+ /**
166
+ * Lookup release-group
167
+ * @param releaseGroupId Release-group MBID
168
+ * @param inc Include: ToDo
169
+ */
170
+ getReleaseGroup(releaseGroupId, inc = []) {
171
+ return this.getEntity('release-group', releaseGroupId, inc);
172
+ }
173
+ /**
174
+ * Lookup work
175
+ * @param workId Work MBID
176
+ */
177
+ getWork(workId) {
178
+ return this.getEntity('work', workId);
179
+ }
180
+ /**
181
+ * Lookup label
182
+ * @param labelId Label MBID
183
+ */
184
+ getLabel(labelId) {
185
+ return this.getEntity('label', labelId);
186
+ }
187
+ /**
188
+ * Lookup recording
189
+ * @param recordingId Label MBID
190
+ * @param inc Include: artist-credits, isrcs
191
+ */
192
+ getRecording(recordingId, inc = []) {
193
+ return this.getEntity('recording', recordingId, inc);
194
+ }
195
+ async postRecording(xmlMetadata) {
196
+ return this.post('recording', xmlMetadata);
197
+ }
198
+ async post(entity, xmlMetadata) {
199
+ if (!this.config.appName || !this.config.appVersion) {
200
+ throw new Error(`XML-Post requires the appName & appVersion to be defined`);
201
+ }
202
+ const clientId = `${this.config.appName.replace(/-/g, '.')}-${this.config.appVersion}`;
203
+ const path = `ws/2/${entity}/`;
204
+ // Get digest challenge
205
+ let digest = null;
206
+ let n = 1;
207
+ const postData = xmlMetadata.toXml();
208
+ do {
209
+ await this.rateLimiter.limit();
210
+ const response = await got_1.default.post(path, Object.assign({ searchParams: { client: clientId }, headers: {
211
+ authorization: digest,
212
+ 'Content-Type': 'application/xml'
213
+ }, body: postData, throwHttpErrors: false }, this.options));
214
+ if (response.statusCode === http_status_codes_1.StatusCodes.UNAUTHORIZED) {
215
+ // Respond to digest challenge
216
+ const auth = new digest_auth_1.DigestAuth(this.config.botAccount);
217
+ const relPath = Url.parse(response.requestUrl).path; // Ensure path is relative
218
+ digest = auth.digest(response.request.method, relPath, response.headers['www-authenticate']);
219
+ ++n;
220
+ }
221
+ else {
222
+ break;
223
+ }
224
+ } while (n++ < 5);
225
+ }
226
+ async login() {
227
+ assert.ok(this.config.botAccount.username, 'bot username should be set');
228
+ assert.ok(this.config.botAccount.password, 'bot password should be set');
229
+ if (this.session && this.session.loggedIn) {
230
+ for (const cookie of await this.getCookies(this.options.prefixUrl)) {
231
+ if (cookie.key === 'remember_login') {
232
+ return true;
233
+ }
234
+ }
235
+ }
236
+ this.session = await this.getSession(this.config.baseUrl);
237
+ const redirectUri = '/success';
238
+ const formData = {
239
+ username: this.config.botAccount.username,
240
+ password: this.config.botAccount.password,
241
+ csrf_session_key: this.session.csrf.sessionKey,
242
+ csrf_token: this.session.csrf.token,
243
+ remember_me: 1
244
+ };
245
+ const response = await got_1.default.post('login', Object.assign({ followRedirect: false, searchParams: {
246
+ returnto: redirectUri
247
+ }, form: formData }, this.options));
248
+ const success = response.statusCode === http_status_codes_1.StatusCodes.MOVED_TEMPORARILY && response.headers.location === redirectUri;
249
+ if (success) {
250
+ this.session.loggedIn = true;
251
+ }
252
+ return success;
253
+ }
254
+ /**
255
+ * Logout
256
+ */
257
+ async logout() {
258
+ const redirectUri = '/success';
259
+ const response = await got_1.default.get('logout', Object.assign({ followRedirect: false, searchParams: {
260
+ returnto: redirectUri
261
+ } }, this.options));
262
+ const success = response.statusCode === http_status_codes_1.StatusCodes.MOVED_TEMPORARILY && response.headers.location === redirectUri;
263
+ if (success) {
264
+ this.session.loggedIn = true;
265
+ }
266
+ return success;
267
+ }
268
+ /**
269
+ * Submit entity
270
+ * @param entity Entity type e.g. 'recording'
271
+ * @param mbid
272
+ * @param formData
273
+ */
274
+ async editEntity(entity, mbid, formData) {
275
+ await this.rateLimiter.limit();
276
+ this.session = await this.getSession(this.config.baseUrl);
277
+ formData.csrf_session_key = this.session.csrf.sessionKey;
278
+ formData.csrf_token = this.session.csrf.token;
279
+ formData.username = this.config.botAccount.username;
280
+ formData.password = this.config.botAccount.password;
281
+ formData.remember_me = 1;
282
+ const response = await got_1.default.post(`${entity}/${mbid}/edit`, Object.assign({ form: formData, followRedirect: false }, this.options));
283
+ if (response.statusCode === http_status_codes_1.StatusCodes.OK)
284
+ throw new Error(`Failed to submit form data`);
285
+ if (response.statusCode === http_status_codes_1.StatusCodes.MOVED_TEMPORARILY)
286
+ return;
287
+ throw new Error(`Unexpected status code: ${response.statusCode}`);
288
+ }
289
+ /**
290
+ * Set URL to recording
291
+ * @param recording Recording to update
292
+ * @param url2add URL to add to the recording
293
+ * @param editNote Edit note
294
+ */
295
+ async addUrlToRecording(recording, url2add, editNote = '') {
296
+ const formData = {};
297
+ formData['edit-recording.name'] = recording.title; // Required
298
+ formData['edit-recording.comment'] = recording.disambiguation;
299
+ formData['edit-recording.make_votable'] = true;
300
+ formData['edit-recording.url.0.link_type_id'] = url2add.linkTypeId;
301
+ formData['edit-recording.url.0.text'] = url2add.text;
302
+ for (const i in recording.isrcs) {
303
+ formData[`edit-recording.isrcs.${i}`] = recording.isrcs[i];
304
+ }
305
+ formData['edit-recording.edit_note'] = editNote;
306
+ return this.editEntity('recording', recording.id, formData);
307
+ }
308
+ /**
309
+ * Add ISRC to recording
310
+ * @param recording Recording to update
311
+ * @param isrc ISRC code to add
312
+ * @param editNote Edit note
313
+ */
314
+ async addIsrc(recording, isrc, editNote = '') {
315
+ const formData = {};
316
+ formData[`edit-recording.name`] = recording.title; // Required
317
+ if (!recording.isrcs) {
318
+ throw new Error('You must retrieve recording with existing ISRC values');
319
+ }
320
+ if (recording.isrcs.indexOf(isrc) === -1) {
321
+ recording.isrcs.push(isrc);
322
+ for (const i in recording.isrcs) {
323
+ formData[`edit-recording.isrcs.${i}`] = recording.isrcs[i];
324
+ }
325
+ return this.editEntity('recording', recording.id, formData);
326
+ }
327
+ }
328
+ // -----------------------------------------------------------------------------------------------------------------
329
+ // Query functions
330
+ // -----------------------------------------------------------------------------------------------------------------
331
+ /**
332
+ * Search an entity using a search query
333
+ * @param query e.g.: '" artist: Madonna, track: Like a virgin"' or object with search terms: {artist: Madonna}
334
+ * @param entity e.g. 'recording'
335
+ * @param offset
336
+ * @param limit
337
+ */
338
+ search(entity, query, offset, limit) {
339
+ if (typeof query === 'object') {
340
+ query = makeAndQueryString(query);
341
+ }
342
+ return this.restGet('/' + entity + '/', { query, offset, limit });
343
+ }
344
+ // -----------------------------------------------------------------------------------------------------------------
345
+ // Helper functions
346
+ // -----------------------------------------------------------------------------------------------------------------
347
+ /**
348
+ * Add Spotify-ID to MusicBrainz recording.
349
+ * This function will automatically lookup the recording title, which is required to submit the recording URL
350
+ * @param recording MBID of the recording
351
+ * @param spotifyId Spotify ID
352
+ * @param editNote Comment to add.
353
+ */
354
+ addSpotifyIdToRecording(recording, spotifyId, editNote) {
355
+ assert.strictEqual(spotifyId.length, 22);
356
+ return this.addUrlToRecording(recording, {
357
+ linkTypeId: mb.LinkType.stream_for_free,
358
+ text: 'https://open.spotify.com/track/' + spotifyId
359
+ }, editNote);
360
+ }
361
+ searchArtist(query, offset, limit) {
362
+ return this.search('artist', query, offset, limit);
363
+ }
364
+ searchRelease(query, offset, limit) {
365
+ return this.search('release', query, offset, limit);
366
+ }
367
+ searchReleaseGroup(query, offset, limit) {
368
+ return this.search('release-group', query, offset, limit);
369
+ }
370
+ searchArea(query, offset, limit) {
371
+ return this.search('area', query, offset, limit);
372
+ }
373
+ searchUrl(query, offset, limit) {
374
+ return this.search('url', query, offset, limit);
375
+ }
376
+ async getSession(url) {
377
+ const response = await got_1.default.get('login', Object.assign({ followRedirect: false, responseType: 'text' }, this.options));
378
+ return {
379
+ csrf: MusicBrainzApi.fetchCsrf(response.body)
380
+ };
381
+ }
382
+ }
383
+ exports.MusicBrainzApi = MusicBrainzApi;
384
+ function makeAndQueryString(keyValuePairs) {
385
+ return Object.keys(keyValuePairs).map(key => `${key}:"${keyValuePairs[key]}"`).join(' AND ');
386
+ }
387
+ exports.makeAndQueryString = makeAndQueryString;
370
388
  //# sourceMappingURL=musicbrainz-api.js.map