musicbrainz-api 0.5.2 → 0.7.2

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 (35) 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 +722 -0
  16. package/README.md +290 -287
  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 +156 -140
  21. package/lib/musicbrainz-api.js +390 -372
  22. package/lib/musicbrainz.types.d.ts +379 -252
  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 -99
  35. package/yarn-error.log +3608 -0
@@ -1,373 +1,391 @@
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 postRecording(xmlMetadata) {
177
- return this.post('recording', xmlMetadata);
178
- }
179
- async post(entity, xmlMetadata) {
180
- if (!this.config.appName || !this.config.appVersion) {
181
- throw new Error(`XML-Post requires the appName & appVersion to be defined`);
182
- }
183
- const clientId = `${this.config.appName.replace(/-/g, '.')}-${this.config.appVersion}`;
184
- const path = `/ws/2/${entity}/`;
185
- // Get digest challenge
186
- let digest = null;
187
- let n = 1;
188
- const postData = xmlMetadata.toXml();
189
- do {
190
- try {
191
- await this.rateLimiter.limit();
192
- await this.request.post(path, {
193
- qs: { client: clientId },
194
- headers: {
195
- authorization: digest,
196
- 'Content-Type': 'application/xml'
197
- },
198
- body: postData
199
- });
200
- }
201
- catch (err) {
202
- const response = err.response;
203
- assert.ok(response.complete);
204
- if (response.statusCode === HttpStatus.UNAUTHORIZED) {
205
- // Respond to digest challenge
206
- const auth = new digest_auth_1.DigestAuth(this.config.botAccount);
207
- const relPath = Url.parse(response.request.path).path; // Ensure path is relative
208
- digest = auth.digest(response.request.method, relPath, response.headers['www-authenticate']);
209
- continue;
210
- }
211
- else if (response.statusCode === 503) {
212
- continue;
213
- }
214
- break;
215
- }
216
- break;
217
- } while (n++ < 5);
218
- }
219
- async login() {
220
- const cookies = this.getCookies(this.config.baseUrl);
221
- for (const cookie of cookies) {
222
- if (cookie.key === 'musicbrainz_server_session')
223
- return true;
224
- }
225
- const redirectUri = '/success';
226
- assert.ok(this.config.botAccount.username, 'bot username should be set');
227
- assert.ok(this.config.botAccount.password, 'bot password should be set');
228
- let response;
229
- try {
230
- response = await this.request.post({
231
- uri: '/login',
232
- followRedirect: false,
233
- qs: {
234
- uri: redirectUri
235
- },
236
- form: {
237
- username: this.config.botAccount.username,
238
- password: this.config.botAccount.password
239
- }
240
- });
241
- }
242
- catch (err) {
243
- if (err.response) {
244
- assert.ok(err.response.complete);
245
- response = err.response;
246
- }
247
- else {
248
- throw err;
249
- }
250
- }
251
- assert.strictEqual(response.statusCode, HttpStatus.MOVED_TEMPORARILY, 'Expect redirect to /success');
252
- return response.headers.location === redirectUri;
253
- }
254
- /**
255
- * Submit entity
256
- * @param entity Entity type e.g. 'recording'
257
- * @param mbid
258
- * @param formData
259
- */
260
- async editEntity(entity, mbid, formData) {
261
- assert.ok(await this.login(), `should be logged in to ${this.config.botAccount.username} with username ${this.config.baseUrl}`);
262
- await this.rateLimiter.limit();
263
- let response;
264
- try {
265
- response = await this.request.post({
266
- uri: `/${entity}/${mbid}/edit`,
267
- form: formData,
268
- followRedirect: false
269
- });
270
- }
271
- catch (err) {
272
- assert.ok(err.response.complete);
273
- response = err.response;
274
- }
275
- assert.strictEqual(response.statusCode, HttpStatus.MOVED_TEMPORARILY);
276
- }
277
- /**
278
- * Set URL to recording
279
- * @param recording Recording to update
280
- * @param url2add URL to add to the recording
281
- * @param editNote Edit note
282
- */
283
- async addUrlToRecording(recording, url2add, editNote = '') {
284
- const formData = {};
285
- formData[`edit-recording.name`] = recording.title; // Required
286
- formData[`edit-recording.comment`] = recording.disambiguation;
287
- formData[`edit-recording.make_votable`] = true;
288
- formData[`edit-recording.url.0.link_type_id`] = url2add.linkTypeId;
289
- formData[`edit-recording.url.0.text`] = url2add.text;
290
- for (const i in recording.isrcs) {
291
- formData[`edit-recording.isrcs.${i}`] = recording.isrcs[i];
292
- }
293
- formData['edit-recording.edit_note'] = editNote;
294
- return this.editEntity('recording', recording.id, formData);
295
- }
296
- /**
297
- * Add ISRC to recording
298
- * @param recording Recording to update
299
- * @param isrc ISRC code to add
300
- * @param editNote Edit note
301
- */
302
- async addIsrc(recording, isrc, editNote = '') {
303
- const formData = {};
304
- formData[`edit-recording.name`] = recording.title; // Required
305
- if (!recording.isrcs) {
306
- throw new Error('You must retrieve recording with existing ISRC values');
307
- }
308
- if (recording.isrcs.indexOf(isrc) === -1) {
309
- recording.isrcs.push(isrc);
310
- for (const i in recording.isrcs) {
311
- formData[`edit-recording.isrcs.${i}`] = recording.isrcs[i];
312
- }
313
- return this.editEntity('recording', recording.id, formData);
314
- }
315
- }
316
- // -----------------------------------------------------------------------------------------------------------------
317
- // Query functions
318
- // -----------------------------------------------------------------------------------------------------------------
319
- /**
320
- * Search an entity using a search query
321
- * @param query e.g.: '" artist: Madonna, track: Like a virgin"' or object with search terms: {artist: Madonna}
322
- * @param entity e.g. 'recording'
323
- * @param offset
324
- * @param limit
325
- */
326
- search(entity, query, offset, limit) {
327
- if (typeof query === 'object') {
328
- query = makeAndQueryString(query);
329
- }
330
- return this.restGet('/' + entity + '/', { query, offset, limit });
331
- }
332
- // -----------------------------------------------------------------------------------------------------------------
333
- // Helper functions
334
- // -----------------------------------------------------------------------------------------------------------------
335
- /**
336
- * Add Spotify-ID to MusicBrainz recording.
337
- * This function will automatically lookup the recording title, which is required to submit the recording URL
338
- * @param recording MBID of the recording
339
- * @param spotifyId Spotify ID
340
- * @param editNote Comment to add.
341
- */
342
- addSpotifyIdToRecording(recording, spotifyId, editNote) {
343
- assert.strictEqual(spotifyId.length, 22);
344
- return this.addUrlToRecording(recording, {
345
- linkTypeId: mb.LinkType.stream_for_free,
346
- text: 'https://open.spotify.com/track/' + spotifyId
347
- }, editNote);
348
- }
349
- searchArtist(query, offset, limit) {
350
- return this.search('artist', query, offset, limit);
351
- }
352
- searchRelease(query, offset, limit) {
353
- return this.search('release', query, offset, limit);
354
- }
355
- searchReleaseGroup(query, offset, limit) {
356
- return this.search('release-group', query, offset, limit);
357
- }
358
- searchArea(query, offset, limit) {
359
- return this.search('area', query, offset, limit);
360
- }
361
- searchUrl(query, offset, limit) {
362
- return this.search('url', query, offset, limit);
363
- }
364
- getCookies(url) {
365
- return this.cookieJar.getCookies(url);
366
- }
367
- }
368
- exports.MusicBrainzApi = MusicBrainzApi;
369
- function makeAndQueryString(keyValuePairs) {
370
- return Object.keys(keyValuePairs).map(key => `${key}:"${keyValuePairs[key]}"`).join(' AND ');
371
- }
372
- 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 query Arguments
336
+ */
337
+ search(entity, query) {
338
+ const urlQuery = Object.assign({}, query);
339
+ if (typeof query.query === 'object') {
340
+ urlQuery.query = makeAndQueryString(query.query);
341
+ }
342
+ if (Array.isArray(query.inc)) {
343
+ urlQuery.inc = urlQuery.inc.join(' ');
344
+ }
345
+ return this.restGet('/' + entity + '/', urlQuery);
346
+ }
347
+ // -----------------------------------------------------------------------------------------------------------------
348
+ // Helper functions
349
+ // -----------------------------------------------------------------------------------------------------------------
350
+ /**
351
+ * Add Spotify-ID to MusicBrainz recording.
352
+ * This function will automatically lookup the recording title, which is required to submit the recording URL
353
+ * @param recording MBID of the recording
354
+ * @param spotifyId Spotify ID
355
+ * @param editNote Comment to add.
356
+ */
357
+ addSpotifyIdToRecording(recording, spotifyId, editNote) {
358
+ assert.strictEqual(spotifyId.length, 22);
359
+ return this.addUrlToRecording(recording, {
360
+ linkTypeId: mb.LinkType.stream_for_free,
361
+ text: 'https://open.spotify.com/track/' + spotifyId
362
+ }, editNote);
363
+ }
364
+ searchArea(query) {
365
+ return this.search('area', query);
366
+ }
367
+ searchArtist(query) {
368
+ return this.search('artist', query);
369
+ }
370
+ searchRelease(query) {
371
+ return this.search('release', query);
372
+ }
373
+ searchReleaseGroup(query) {
374
+ return this.search('release-group', query);
375
+ }
376
+ searchUrl(query) {
377
+ return this.search('url', query);
378
+ }
379
+ async getSession(url) {
380
+ const response = await got_1.default.get('login', Object.assign({ followRedirect: false, responseType: 'text' }, this.options));
381
+ return {
382
+ csrf: MusicBrainzApi.fetchCsrf(response.body)
383
+ };
384
+ }
385
+ }
386
+ exports.MusicBrainzApi = MusicBrainzApi;
387
+ function makeAndQueryString(keyValuePairs) {
388
+ return Object.keys(keyValuePairs).map(key => `${key}:"${keyValuePairs[key]}"`).join(' AND ');
389
+ }
390
+ exports.makeAndQueryString = makeAndQueryString;
373
391
  //# sourceMappingURL=musicbrainz-api.js.map