musicbrainz-api 0.10.3 → 0.11.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 CHANGED
@@ -25,7 +25,7 @@ We are looking into making this package usable in the browser as well.
25
25
 
26
26
  ## Before using this library
27
27
 
28
- MusicBrainz asks that you [identifying your application](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2#User%20Data) by filling in the ['User-Agent' Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent).
28
+ MusicBrainz asks that you to [identify your application](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2#User%20Data) by filling in the ['User-Agent' Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent).
29
29
  By passing `appName`, `appVersion`, `appMail` musicbrainz-api takes care of that.
30
30
 
31
31
  ## Submitting metadata
@@ -12,10 +12,10 @@ export declare class DigestAuth {
12
12
  * If the algorithm directive's value is "MD5-sess", then HA1 is
13
13
  * HA1=MD5(MD5(username:realm:password):nonce:cnonce)
14
14
  */
15
- static ha1Compute(algorithm: any, user: any, realm: any, pass: any, nonce: any, cnonce: any): string;
15
+ static ha1Compute(algorithm: string, user: string, realm: string, pass: string, nonce: string, cnonce: string): string;
16
16
  hasAuth: boolean;
17
17
  sentAuth: boolean;
18
- bearerToken: string;
18
+ bearerToken: string | null;
19
19
  constructor(credentials: ICredentials);
20
20
  digest(method: string, path: string, authHeader: string): string;
21
21
  }
@@ -64,16 +64,16 @@ class DigestAuth {
64
64
  opaque: challenge.opaque
65
65
  };
66
66
  const parts = [];
67
- for (const k in authValues) {
68
- if (authValues[k]) {
69
- if (k === 'qop' || k === 'nc' || k === 'algorithm') {
70
- parts.push(k + '=' + authValues[k]);
67
+ Object.entries(authValues).forEach(([key, value]) => {
68
+ if (value) {
69
+ if (key === 'qop' || key === 'nc' || key === 'algorithm') {
70
+ parts.push(key + '=' + value);
71
71
  }
72
72
  else {
73
- parts.push(k + '="' + authValues[k] + '"');
73
+ parts.push(key + '="' + value + '"');
74
74
  }
75
75
  }
76
- }
76
+ });
77
77
  authHeader = 'Digest ' + parts.join(', ');
78
78
  this.sentAuth = true;
79
79
  return authHeader;
@@ -42,13 +42,13 @@ export type ReleaseGroupIncludes = MiscIncludes | SubQueryIncludes | RelationsIn
42
42
  export type SeriesIncludes = MiscIncludes | RelationsIncludes;
43
43
  export type WorkIncludes = MiscIncludes | RelationsIncludes;
44
44
  export type UrlIncludes = RelationsIncludes;
45
- export interface IFormData {
45
+ export type IFormData = {
46
46
  [key: string]: string | number;
47
- }
47
+ };
48
48
  export interface IMusicBrainzConfig {
49
- botAccount?: {
50
- username: string;
51
- password: string;
49
+ botAccount: {
50
+ username?: string;
51
+ password?: string;
52
52
  };
53
53
  baseUrl?: string;
54
54
  appName?: string;
@@ -62,11 +62,12 @@ export interface IMusicBrainzConfig {
62
62
  */
63
63
  appContactInfo?: string;
64
64
  }
65
+ export interface ICsrfSession {
66
+ sessionKey: string;
67
+ token: string;
68
+ }
65
69
  export interface ISessionInformation {
66
- csrf: {
67
- sessionKey: string;
68
- token: string;
69
- };
70
+ csrf: ICsrfSession;
70
71
  loggedIn?: boolean;
71
72
  }
72
73
  export declare class MusicBrainzApi {
@@ -74,11 +75,8 @@ export declare class MusicBrainzApi {
74
75
  readonly config: IMusicBrainzConfig;
75
76
  private rateLimiter;
76
77
  private options;
77
- private session;
78
- static fetchCsrf(html: string): {
79
- sessionKey: string;
80
- token: string;
81
- };
78
+ private session?;
79
+ static fetchCsrf(html: string): ICsrfSession;
82
80
  private static fetchValue;
83
81
  private getCookies;
84
82
  constructor(_config?: IMusicBrainzConfig);
@@ -30,8 +30,9 @@ Object.defineProperty(exports, "XmlRecording", { enumerable: true, get: function
30
30
  const digest_auth_1 = require("./digest-auth");
31
31
  const rate_limiter_1 = require("./rate-limiter");
32
32
  const mb = require("./musicbrainz.types");
33
+ /* eslint-disable-next-line */
33
34
  const got_1 = require("got");
34
- const tough = require("tough-cookie");
35
+ const tough_cookie_1 = require("tough-cookie");
35
36
  __exportStar(require("./musicbrainz.types"), exports);
36
37
  const util_1 = require("util");
37
38
  const retries = 3;
@@ -86,10 +87,11 @@ class MusicBrainzApi {
86
87
  }
87
88
  constructor(_config) {
88
89
  this.config = {
89
- baseUrl: 'https://musicbrainz.org'
90
+ baseUrl: 'https://musicbrainz.org',
91
+ botAccount: {}
90
92
  };
91
93
  Object.assign(this.config, _config);
92
- const cookieJar = new tough.CookieJar();
94
+ const cookieJar = new tough_cookie_1.CookieJar();
93
95
  this.getCookies = (0, util_1.promisify)(cookieJar.getCookies.bind(cookieJar));
94
96
  this.options = {
95
97
  prefixUrl: this.config.baseUrl,
@@ -97,7 +99,7 @@ class MusicBrainzApi {
97
99
  headers: {
98
100
  'User-Agent': `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
99
101
  },
100
- cookieJar
102
+ cookieJar: cookieJar
101
103
  };
102
104
  this.rateLimiter = new rate_limiter_1.RateLimiter(60, 50);
103
105
  }
@@ -353,7 +355,7 @@ class MusicBrainzApi {
353
355
  const clientId = `${this.config.appName.replace(/-/g, '.')}-${this.config.appVersion}`;
354
356
  const path = `ws/2/${entity}/`;
355
357
  // Get digest challenge
356
- let digest = null;
358
+ let digest;
357
359
  let n = 1;
358
360
  const postData = xmlMetadata.toXml();
359
361
  do {
@@ -384,7 +386,7 @@ class MusicBrainzApi {
384
386
  }
385
387
  }
386
388
  }
387
- this.session = await this.getSession(this.config.baseUrl);
389
+ this.session = await this.getSession();
388
390
  const redirectUri = '/success';
389
391
  const formData = {
390
392
  username: this.config.botAccount.username,
@@ -411,7 +413,7 @@ class MusicBrainzApi {
411
413
  returnto: redirectUri
412
414
  } }, this.options));
413
415
  const success = response.statusCode === http_status_codes_1.StatusCodes.MOVED_TEMPORARILY && response.headers.location === redirectUri;
414
- if (success) {
416
+ if (success && this.session) {
415
417
  this.session.loggedIn = true;
416
418
  }
417
419
  return success;
@@ -424,7 +426,7 @@ class MusicBrainzApi {
424
426
  */
425
427
  async editEntity(entity, mbid, formData) {
426
428
  await this.rateLimiter.limit();
427
- this.session = await this.getSession(this.config.baseUrl);
429
+ this.session = await this.getSession();
428
430
  formData.csrf_session_key = this.session.csrf.sessionKey;
429
431
  formData.csrf_token = this.session.csrf.token;
430
432
  formData.username = this.config.botAccount.username;
@@ -444,15 +446,16 @@ class MusicBrainzApi {
444
446
  * @param editNote Edit note
445
447
  */
446
448
  async addUrlToRecording(recording, url2add, editNote = '') {
449
+ var _a;
447
450
  const formData = {};
448
451
  formData['edit-recording.name'] = recording.title; // Required
449
452
  formData['edit-recording.comment'] = recording.disambiguation;
450
453
  formData['edit-recording.make_votable'] = true;
451
454
  formData['edit-recording.url.0.link_type_id'] = url2add.linkTypeId;
452
455
  formData['edit-recording.url.0.text'] = url2add.text;
453
- for (const i in recording.isrcs) {
454
- formData[`edit-recording.isrcs.${i}`] = recording.isrcs[i];
455
- }
456
+ (_a = recording.isrcs) === null || _a === void 0 ? void 0 : _a.forEach((isrcs, i) => {
457
+ formData[`edit-recording.isrcs.${i}`] = isrcs;
458
+ });
456
459
  formData['edit-recording.edit_note'] = editNote;
457
460
  return this.editEntity('recording', recording.id, formData);
458
461
  }
@@ -527,7 +530,7 @@ class MusicBrainzApi {
527
530
  searchUrl(query) {
528
531
  return this.search('url', query);
529
532
  }
530
- async getSession(url) {
533
+ async getSession() {
531
534
  const response = await got_1.default.get('login', Object.assign({ followRedirect: false, responseType: 'text' }, this.options));
532
535
  return {
533
536
  csrf: MusicBrainzApi.fetchCsrf(response.body)
@@ -33,7 +33,7 @@ export interface IArtist extends IEntity {
33
33
  disambiguation: string;
34
34
  'sort-name': string;
35
35
  'type-id'?: string;
36
- 'gender-id'?: any;
36
+ 'gender-id'?: string;
37
37
  'life-span'?: IPeriod;
38
38
  country?: string;
39
39
  ipis?: any[];
@@ -183,6 +183,7 @@ export interface IAreaList extends ISearchResult {
183
183
  }
184
184
  export interface IReleaseList extends ISearchResult {
185
185
  releases: IReleaseMatch[];
186
+ 'release-count': number;
186
187
  }
187
188
  export interface IReleaseGroupList extends ISearchResult {
188
189
  'release-groups': IReleaseGroupMatch[];
@@ -1,6 +1,6 @@
1
1
  export declare class RateLimiter {
2
2
  private maxCalls;
3
- static sleep(ms: any): Promise<void>;
3
+ static sleep(ms: number): Promise<void>;
4
4
  queue: number[];
5
5
  private readonly period;
6
6
  constructor(period: number, maxCalls: number);
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RateLimiter = void 0;
4
- const Debug = require("debug");
5
- const debug = Debug('musicbrainz-api:rate-limiter');
4
+ const debug_1 = require("debug");
5
+ const debug = (0, debug_1.default)('musicbrainz-api:rate-limiter');
6
6
  class RateLimiter {
7
7
  static sleep(ms) {
8
8
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -13,5 +13,5 @@ export declare class XmlIsrcList {
13
13
  id: string;
14
14
  };
15
15
  }[];
16
- };
16
+ } | null;
17
17
  }
@@ -8,7 +8,7 @@ export declare class XmlRecording {
8
8
  attrs: {
9
9
  id: string;
10
10
  };
11
- children: {
11
+ children: ({
12
12
  name: string;
13
13
  attrs: {
14
14
  count: number;
@@ -19,6 +19,6 @@ export declare class XmlRecording {
19
19
  id: string;
20
20
  };
21
21
  }[];
22
- }[];
22
+ } | null)[];
23
23
  };
24
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musicbrainz-api",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "description": "MusicBrainz API client for reading and submitting metadata",
5
5
  "main": "lib/musicbrainz-api",
6
6
  "types": "lib/musicbrainz-api",
@@ -45,13 +45,14 @@
45
45
  "json-stringify-safe": "^5.0.1",
46
46
  "jsontoxml": "^1.0.1",
47
47
  "source-map-support": "^0.5.16",
48
- "tough-cookie": "^4.0.0",
48
+ "tough-cookie": "^4.1.3",
49
49
  "uuid": "^9.0.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/chai": "^4.3.0",
53
+ "@types/jsontoxml": "^1.0.5",
53
54
  "@types/mocha": "^9.0.0",
54
- "@types/node": "^18.6.1",
55
+ "@types/node": "^20.8.10",
55
56
  "@typescript-eslint/eslint-plugin": "^5.13.0",
56
57
  "@typescript-eslint/parser": "^5.13.0",
57
58
  "chai": "^4.2.0",
@@ -60,7 +61,7 @@
60
61
  "eslint-config-prettier": "^8.4.0",
61
62
  "eslint-import-resolver-typescript": "^3.3.0",
62
63
  "eslint-plugin-import": "^2.25.4",
63
- "eslint-plugin-jsdoc": "^40.1.0",
64
+ "eslint-plugin-jsdoc": "^46.8.2",
64
65
  "eslint-plugin-node": "^11.1.0",
65
66
  "eslint-plugin-unicorn": "^46.0.0",
66
67
  "mocha": "^9.0.1",
@@ -71,6 +72,10 @@
71
72
  "tslint": "^6.1.1",
72
73
  "typescript": "^5.0.2"
73
74
  },
75
+ "files": [
76
+ "lib/**/*.js",
77
+ "lib/**/*.d.ts"
78
+ ],
74
79
  "scripts": {
75
80
  "clean": "del-cli lib/**/*.js lib/**/*.js.map lib/**/*.d.ts test/**/*.js test/**/*.js.map",
76
81
  "compile-lib": "tsc -p lib",
@@ -1,16 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="CheckStyle-IDEA" serialisationVersion="2">
4
- <checkstyleVersion>10.5.0</checkstyleVersion>
5
- <scanScope>JavaOnly</scanScope>
6
- <copyLibs>true</copyLibs>
7
- <option name="thirdPartyClasspath" />
8
- <option name="activeLocationIds" />
9
- <option name="locations">
10
- <list>
11
- <ConfigurationLocation id="bundled-sun-checks" type="BUNDLED" scope="All" description="Sun Checks">(bundled)</ConfigurationLocation>
12
- <ConfigurationLocation id="bundled-google-checks" type="BUNDLED" scope="All" description="Google Checks">(bundled)</ConfigurationLocation>
13
- </list>
14
- </option>
15
- </component>
16
- </project>
@@ -1,7 +0,0 @@
1
- <component name="InspectionProjectProfileManager">
2
- <profile version="1.0">
3
- <option name="myName" value="Project Default" />
4
- <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
5
- <inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
6
- </profile>
7
- </component>
package/.idea/misc.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectRootManager">
4
- <output url="file://$PROJECT_DIR$/out" />
5
- </component>
6
- </project>
package/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/musicbrainz-api.iml" filepath="$PROJECT_DIR$/.idea/musicbrainz-api.iml" />
6
- </modules>
7
- </component>
8
- </project>
package/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="" vcs="Git" />
5
- </component>
6
- </project>
@@ -1,101 +0,0 @@
1
- import { v4 as uuidv4 } from 'uuid';
2
- import * as crypto from 'crypto';
3
-
4
- interface IChallenge {
5
- algorithm?: string;
6
- realm?: string;
7
- nonce?: string;
8
- opaque?: string;
9
- qop?: string;
10
- }
11
-
12
- export interface ICredentials {
13
- username: string;
14
- password: string;
15
- }
16
-
17
- function md5(str) {
18
- return crypto.createHash('md5').update(str).digest('hex'); // lgtm [js/insufficient-password-hash]
19
- }
20
-
21
- export class DigestAuth {
22
-
23
- /**
24
- * RFC 2617: handle both MD5 and MD5-sess algorithms.
25
- *
26
- * If the algorithm directive's value is "MD5" or unspecified, then HA1 is
27
- * HA1=MD5(username:realm:password)
28
- * If the algorithm directive's value is "MD5-sess", then HA1 is
29
- * HA1=MD5(MD5(username:realm:password):nonce:cnonce)
30
- */
31
- public static ha1Compute(algorithm, user, realm, pass, nonce, cnonce) {
32
- const ha1 = md5(user + ':' + realm + ':' + pass); // lgtm [js/insufficient-password-hash]
33
- return algorithm && algorithm.toLowerCase() === 'md5-sess' ? md5(ha1 + ':' + nonce + ':' + cnonce) : ha1;
34
- }
35
-
36
- public hasAuth: boolean;
37
- public sentAuth: boolean;
38
- public bearerToken: string;
39
-
40
- public constructor(private credentials: ICredentials) {
41
- this.hasAuth = false;
42
- this.sentAuth = false;
43
- this.bearerToken = null;
44
- }
45
-
46
- public digest(method: string, path: string, authHeader: string): string {
47
- // TODO: More complete implementation of RFC 2617.
48
- // - support qop="auth-int" only
49
- // - handle Authentication-Info (not necessarily?)
50
- // - check challenge.stale (not necessarily?)
51
- // - increase nc (not necessarily?)
52
- // For reference:
53
- // http://tools.ietf.org/html/rfc2617#section-3
54
- // https://github.com/bagder/curl/blob/master/lib/http_digest.c
55
-
56
- const challenge: IChallenge = {};
57
- const re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi;
58
- while (true) {
59
- const match = re.exec(authHeader);
60
- if (!match) {
61
- break;
62
- }
63
- challenge[match[1]] = match[2] || match[3];
64
- }
65
-
66
- const qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth';
67
- const nc = qop && '00000001';
68
- const cnonce = qop && uuidv4().replace(/-/g, '');
69
- const ha1 = DigestAuth.ha1Compute(challenge.algorithm, this.credentials.username, challenge.realm, this.credentials.password, challenge.nonce, cnonce);
70
- const ha2 = md5(method + ':' + path); // lgtm [js/insufficient-password-hash]
71
- const digestResponse = qop
72
- ? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) // lgtm [js/insufficient-password-hash]
73
- : md5(ha1 + ':' + challenge.nonce + ':' + ha2); // lgtm [js/insufficient-password-hash]
74
- const authValues = {
75
- username: this.credentials.username,
76
- realm: challenge.realm,
77
- nonce: challenge.nonce,
78
- uri: path,
79
- qop,
80
- response: digestResponse,
81
- nc,
82
- cnonce,
83
- algorithm: challenge.algorithm,
84
- opaque: challenge.opaque
85
- };
86
-
87
- const parts: string[] = [];
88
- for (const k in authValues) {
89
- if (authValues[k]) {
90
- if (k === 'qop' || k === 'nc' || k === 'algorithm') {
91
- parts.push(k + '=' + authValues[k]);
92
- } else {
93
- parts.push(k + '="' + authValues[k] + '"');
94
- }
95
- }
96
- }
97
- authHeader = 'Digest ' + parts.join(', ');
98
- this.sentAuth = true;
99
- return authHeader;
100
- }
101
- }