musicbrainz-api 0.23.0 → 0.23.1

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.
@@ -0,0 +1,74 @@
1
+ export type CovertType = 'Front' | 'Back' | 'Booklet' | 'Medium' | 'Obi' | 'Spine' | 'Track' | 'Tray' | 'Sticker' | 'Poster' | 'Liner' | 'Watermark' | 'Raw/Unedited' | 'Matrix/Runout' | 'Top' | 'Bottom' | 'Other';
2
+ export interface IImage {
3
+ types: CovertType[];
4
+ front: boolean;
5
+ back: boolean;
6
+ edit: number;
7
+ image: string;
8
+ comment: string;
9
+ approved: boolean;
10
+ id: string;
11
+ thumbnails: {
12
+ large: string;
13
+ small: string;
14
+ '250': string;
15
+ '500'?: string;
16
+ '1200'?: string;
17
+ };
18
+ }
19
+ export type CoverType = 'front' | 'back';
20
+ export interface ICoversInfo {
21
+ images: IImage[];
22
+ release: string;
23
+ }
24
+ export interface ICoverInfo {
25
+ url: string | null;
26
+ }
27
+ export declare class CoverArtArchiveApi {
28
+ private httpClient;
29
+ private getJson;
30
+ private getCoverRedirect;
31
+ /**
32
+ * Fetch release
33
+ * @releaseId Release MBID
34
+ * @param releaseId MusicBrainz Release MBID
35
+ */
36
+ getReleaseCovers(releaseId: string): Promise<ICoversInfo>;
37
+ /**
38
+ * Fetch release-group
39
+ * @releaseGroupId Release-group MBID
40
+ * @param releaseGroupId MusicBrainz Release Group MBID
41
+ */
42
+ getReleaseGroupCovers(releaseGroupId: string): Promise<ICoversInfo>;
43
+ /**
44
+ * Fetch release cover
45
+ * @releaseId Release MBID
46
+ * @param releaseId MusicBrainz Release MBID
47
+ * @param coverType Front or back cover
48
+ */
49
+ getReleaseCover(releaseId: string, coverType: CoverType): Promise<ICoverInfo>;
50
+ /**
51
+ * Fetch release-group cover
52
+ * @releaseId Release-group MBID
53
+ * @param releaseGroupId MusicBrainz Release-group MBID
54
+ * @param coverType Front or back cover
55
+ */
56
+ getReleaseGroupCover(releaseGroupId: string, coverType: CoverType): Promise<ICoverInfo>;
57
+ private static makePath;
58
+ /**
59
+ * Fetch covers
60
+ * @releaseId MBID
61
+ * @param releaseId MusicBrainz Release Group MBID
62
+ * @param releaseType Fetch covers for specific release or release-group
63
+ * @param coverType Cover type
64
+ */
65
+ private getCovers;
66
+ /**
67
+ * Fetch covers
68
+ * @releaseId MBID
69
+ * @param releaseId MusicBrainz Release Group MBID
70
+ * @param releaseType Fetch covers for specific release or release-group
71
+ * @param coverType Cover type
72
+ */
73
+ private getCover;
74
+ }
@@ -0,0 +1,110 @@
1
+ /* eslint-disable-next-line */
2
+ import { HttpClient } from "./http-client.js";
3
+ export class CoverArtArchiveApi {
4
+ constructor() {
5
+ this.httpClient = new HttpClient({ baseUrl: 'https://coverartarchive.org', userAgent: 'Node.js musicbrains-api', timeout: 20000, followRedirects: false });
6
+ }
7
+ async getJson(path) {
8
+ const response = await this.httpClient.get(path, {
9
+ headers: {
10
+ Accept: "application/json"
11
+ }
12
+ });
13
+ const contentType = response.headers.get("Content-Type");
14
+ if (response.status === 404 && contentType?.toLowerCase() !== "application/json") {
15
+ return {
16
+ "error": "Not Found",
17
+ "help": "For usage, please see: https://musicbrainz.org/development/mmd"
18
+ };
19
+ }
20
+ return response.json();
21
+ }
22
+ async getCoverRedirect(path) {
23
+ const response = await this.httpClient.get(path, {
24
+ followRedirects: false
25
+ });
26
+ switch (response.status) {
27
+ case 307:
28
+ return response.headers.get('LOCATION');
29
+ case 400:
30
+ throw new Error('Invalid UUID');
31
+ case 404:
32
+ // No release with this MBID
33
+ return null;
34
+ case 405:
35
+ throw new Error('Invalid HTTP method');
36
+ case 503:
37
+ return null;
38
+ default:
39
+ throw new Error(`Unexpected HTTP-status response: ${response.status}`);
40
+ }
41
+ }
42
+ /**
43
+ * Fetch release
44
+ * @releaseId Release MBID
45
+ * @param releaseId MusicBrainz Release MBID
46
+ */
47
+ getReleaseCovers(releaseId) {
48
+ return this.getCovers(releaseId, 'release');
49
+ }
50
+ /**
51
+ * Fetch release-group
52
+ * @releaseGroupId Release-group MBID
53
+ * @param releaseGroupId MusicBrainz Release Group MBID
54
+ */
55
+ getReleaseGroupCovers(releaseGroupId) {
56
+ return this.getCovers(releaseGroupId, 'release-group');
57
+ }
58
+ /**
59
+ * Fetch release cover
60
+ * @releaseId Release MBID
61
+ * @param releaseId MusicBrainz Release MBID
62
+ * @param coverType Front or back cover
63
+ */
64
+ getReleaseCover(releaseId, coverType) {
65
+ return this.getCover(releaseId, 'release', coverType);
66
+ }
67
+ /**
68
+ * Fetch release-group cover
69
+ * @releaseId Release-group MBID
70
+ * @param releaseGroupId MusicBrainz Release-group MBID
71
+ * @param coverType Front or back cover
72
+ */
73
+ getReleaseGroupCover(releaseGroupId, coverType) {
74
+ return this.getCover(releaseGroupId, 'release-group', coverType);
75
+ }
76
+ static makePath(releaseId, releaseType = 'release', coverType) {
77
+ const path = [releaseType, releaseId];
78
+ if (coverType) {
79
+ path.push(coverType);
80
+ }
81
+ return `/${path.join('/')}`;
82
+ }
83
+ /**
84
+ * Fetch covers
85
+ * @releaseId MBID
86
+ * @param releaseId MusicBrainz Release Group MBID
87
+ * @param releaseType Fetch covers for specific release or release-group
88
+ * @param coverType Cover type
89
+ */
90
+ async getCovers(releaseId, releaseType = 'release') {
91
+ const info = await this.getJson(CoverArtArchiveApi.makePath(releaseId, releaseType));
92
+ // Hack to correct http addresses into https
93
+ if (info.release?.startsWith('http:')) {
94
+ info.release = `https${info.release.substring(4)}`;
95
+ }
96
+ return info;
97
+ }
98
+ /**
99
+ * Fetch covers
100
+ * @releaseId MBID
101
+ * @param releaseId MusicBrainz Release Group MBID
102
+ * @param releaseType Fetch covers for specific release or release-group
103
+ * @param coverType Cover type
104
+ */
105
+ async getCover(releaseId, releaseType = 'release', coverType) {
106
+ const url = await this.getCoverRedirect(CoverArtArchiveApi.makePath(releaseId, releaseType, coverType));
107
+ return { url: url };
108
+ }
109
+ }
110
+ //# sourceMappingURL=coverartarchive-api.js.map
@@ -0,0 +1,21 @@
1
+ export interface ICredentials {
2
+ username: string;
3
+ password: string;
4
+ }
5
+ export declare class DigestAuth {
6
+ private credentials;
7
+ /**
8
+ * RFC 2617: handle both MD5 and MD5-sess algorithms.
9
+ *
10
+ * If the algorithm directive's value is "MD5" or unspecified, then HA1 is
11
+ * HA1=MD5(username:realm:password)
12
+ * If the algorithm directive's value is "MD5-sess", then HA1 is
13
+ * HA1=MD5(MD5(username:realm:password):nonce:cnonce)
14
+ */
15
+ static ha1Compute(algorithm: string, user: string, realm: string, pass: string, nonce: string, cnonce: string): string;
16
+ hasAuth: boolean;
17
+ sentAuth: boolean;
18
+ bearerToken: string | null;
19
+ constructor(credentials: ICredentials);
20
+ digest(method: string, path: string, authHeader: string): string;
21
+ }
@@ -0,0 +1,77 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import sparkMd5 from 'spark-md5';
3
+ const md5 = sparkMd5.hash;
4
+ export class DigestAuth {
5
+ /**
6
+ * RFC 2617: handle both MD5 and MD5-sess algorithms.
7
+ *
8
+ * If the algorithm directive's value is "MD5" or unspecified, then HA1 is
9
+ * HA1=MD5(username:realm:password)
10
+ * If the algorithm directive's value is "MD5-sess", then HA1 is
11
+ * HA1=MD5(MD5(username:realm:password):nonce:cnonce)
12
+ */
13
+ static ha1Compute(algorithm, user, realm, pass, nonce, cnonce) {
14
+ const ha1 = md5(`${user}:${realm}:${pass}`); // lgtm [js/insufficient-password-hash]
15
+ return algorithm && algorithm.toLowerCase() === 'md5-sess' ? md5(`${ha1}:${nonce}:${cnonce}`) : ha1;
16
+ }
17
+ constructor(credentials) {
18
+ this.credentials = credentials;
19
+ this.hasAuth = false;
20
+ this.sentAuth = false;
21
+ this.bearerToken = null;
22
+ }
23
+ digest(method, path, authHeader) {
24
+ // TODO: More complete implementation of RFC 2617.
25
+ // - support qop="auth-int" only
26
+ // - handle Authentication-Info (not necessarily?)
27
+ // - check challenge.stale (not necessarily?)
28
+ // - increase nc (not necessarily?)
29
+ // For reference:
30
+ // http://tools.ietf.org/html/rfc2617#section-3
31
+ // https://github.com/bagder/curl/blob/master/lib/http_digest.c
32
+ const challenge = {};
33
+ const re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi;
34
+ while (true) {
35
+ const match = re.exec(authHeader);
36
+ if (!match) {
37
+ break;
38
+ }
39
+ challenge[match[1]] = match[2] || match[3];
40
+ }
41
+ const qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth';
42
+ const nc = qop && '00000001';
43
+ const cnonce = qop && uuidv4().replace(/-/g, '');
44
+ const ha1 = DigestAuth.ha1Compute(challenge.algorithm, this.credentials.username, challenge.realm, this.credentials.password, challenge.nonce, cnonce);
45
+ const ha2 = md5(`${method}:${path}`); // lgtm [js/insufficient-password-hash]
46
+ const digestResponse = qop
47
+ ? md5(`${ha1}:${challenge.nonce}:${nc}:${cnonce}:${qop}:${ha2}`) // lgtm [js/insufficient-password-hash]
48
+ : md5(`${ha1}:${challenge.nonce}:${ha2}`); // lgtm [js/insufficient-password-hash]
49
+ const authValues = {
50
+ username: this.credentials.username,
51
+ realm: challenge.realm,
52
+ nonce: challenge.nonce,
53
+ uri: path,
54
+ qop,
55
+ response: digestResponse,
56
+ nc,
57
+ cnonce,
58
+ algorithm: challenge.algorithm,
59
+ opaque: challenge.opaque
60
+ };
61
+ const parts = [];
62
+ Object.entries(authValues).forEach(([key, value]) => {
63
+ if (value) {
64
+ if (key === 'qop' || key === 'nc' || key === 'algorithm') {
65
+ parts.push(`${key}=${value}`);
66
+ }
67
+ else {
68
+ parts.push(`${key}="${value}"`);
69
+ }
70
+ }
71
+ });
72
+ const digest = `Digest ${parts.join(', ')}`;
73
+ this.sentAuth = true;
74
+ return digest;
75
+ }
76
+ }
77
+ //# sourceMappingURL=digest-auth.js.map
@@ -0,0 +1,2 @@
1
+ export * from './coverartarchive-api.js';
2
+ export * from './musicbrainz-api.js';
@@ -0,0 +1,3 @@
1
+ export * from './coverartarchive-api.js';
2
+ export * from './musicbrainz-api.js';
3
+ //# sourceMappingURL=entry-default.js.map
@@ -0,0 +1,2 @@
1
+ export * from './coverartarchive-api.js';
2
+ export * from './musicbrainz-api-node.js';
@@ -0,0 +1,3 @@
1
+ export * from './coverartarchive-api.js';
2
+ export * from './musicbrainz-api-node.js';
3
+ //# sourceMappingURL=entry-node.js.map
@@ -0,0 +1,11 @@
1
+ import { type Cookie } from "tough-cookie";
2
+ import { HttpClient, type IHttpClientOptions } from "./http-client.js";
3
+ export type HttpFormData = {
4
+ [key: string]: string;
5
+ };
6
+ export declare class HttpClientNode extends HttpClient {
7
+ private cookieJar;
8
+ constructor(options: IHttpClientOptions);
9
+ protected registerCookies(response: Response): Promise<Cookie | undefined>;
10
+ getCookies(): Promise<string | null>;
11
+ }
@@ -0,0 +1,19 @@
1
+ import { CookieJar } from "tough-cookie";
2
+ import { HttpClient } from "./http-client.js";
3
+ export class HttpClientNode extends HttpClient {
4
+ constructor(options) {
5
+ super(options);
6
+ this.cookieJar = new CookieJar();
7
+ }
8
+ registerCookies(response) {
9
+ const cookie = response.headers.get('set-cookie');
10
+ if (cookie) {
11
+ return this.cookieJar.setCookie(cookie, response.url);
12
+ }
13
+ return Promise.resolve(undefined);
14
+ }
15
+ getCookies() {
16
+ return this.cookieJar.getCookieString(this.options.baseUrl); // Get cookies for the request
17
+ }
18
+ }
19
+ //# sourceMappingURL=http-client-node.js.map
@@ -0,0 +1,34 @@
1
+ import type { Cookie } from "tough-cookie";
2
+ export type HttpFormData = {
3
+ [key: string]: string;
4
+ };
5
+ /**
6
+ * Allows multiple entries for the same key
7
+ */
8
+ export type MultiQueryFormData = {
9
+ [key: string]: string | string[];
10
+ };
11
+ export interface IHttpClientOptions {
12
+ baseUrl: string;
13
+ timeout: number;
14
+ userAgent: string;
15
+ followRedirects?: boolean;
16
+ }
17
+ export interface IFetchOptions {
18
+ query?: MultiQueryFormData;
19
+ retryLimit?: number;
20
+ body?: string;
21
+ headers?: HeadersInit;
22
+ followRedirects?: boolean;
23
+ }
24
+ export declare class HttpClient {
25
+ protected options: IHttpClientOptions;
26
+ constructor(options: IHttpClientOptions);
27
+ get(path: string, options?: IFetchOptions): Promise<Response>;
28
+ post(path: string, options?: IFetchOptions): Promise<Response>;
29
+ postForm(path: string, formData: HttpFormData, options?: IFetchOptions): Promise<Response>;
30
+ postJson(path: string, json: Object, options?: IFetchOptions): Promise<Response>;
31
+ private _fetch;
32
+ protected registerCookies(response: Response): Promise<Cookie | undefined>;
33
+ getCookies(): Promise<string | null>;
34
+ }
@@ -0,0 +1,57 @@
1
+ export class HttpClient {
2
+ constructor(options) {
3
+ this.options = options;
4
+ }
5
+ get(path, options) {
6
+ return this._fetch('get', path, options);
7
+ }
8
+ post(path, options) {
9
+ return this._fetch('post', path, options);
10
+ }
11
+ postForm(path, formData, options) {
12
+ const encodedFormData = new URLSearchParams(formData).toString();
13
+ return this._fetch('post', path, { ...options, body: encodedFormData, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
14
+ }
15
+ // biome-ignore lint/complexity/noBannedTypes:
16
+ postJson(path, json, options) {
17
+ const encodedJson = JSON.stringify(json);
18
+ return this._fetch('post', path, { ...options, body: encodedJson, headers: { 'Content-Type': 'application/json.' } });
19
+ }
20
+ async _fetch(method, path, options) {
21
+ if (!options)
22
+ options = {};
23
+ let url = path.startsWith('/') ? `${this.options.baseUrl}${path}` : `${this.options.baseUrl}/${path}`;
24
+ if (options.query) {
25
+ const urlSearchParams = new URLSearchParams();
26
+ for (const key of Object.keys(options.query)) {
27
+ const value = options.query[key];
28
+ (Array.isArray(value) ? value : [value]).forEach(value => {
29
+ urlSearchParams.append(key, value);
30
+ });
31
+ }
32
+ url += `?${urlSearchParams.toString()}`;
33
+ }
34
+ const cookies = await this.getCookies();
35
+ const headers = new Headers(options.headers);
36
+ headers.set('User-Agent', this.options.userAgent);
37
+ if (cookies !== null) {
38
+ headers.set('Cookie', cookies);
39
+ }
40
+ const response = await fetch(url, {
41
+ method,
42
+ ...options,
43
+ headers,
44
+ body: options.body,
45
+ redirect: options.followRedirects === false ? 'manual' : 'follow'
46
+ });
47
+ await this.registerCookies(response);
48
+ return response;
49
+ }
50
+ registerCookies(response) {
51
+ return Promise.resolve(undefined);
52
+ }
53
+ async getCookies() {
54
+ return Promise.resolve(null);
55
+ }
56
+ }
57
+ //# sourceMappingURL=http-client.js.map
@@ -0,0 +1,16 @@
1
+ export { XmlMetadata } from './xml/xml-metadata.js';
2
+ export { XmlIsrc } from './xml/xml-isrc.js';
3
+ export { XmlIsrcList } from './xml/xml-isrc-list.js';
4
+ export { XmlRecording } from './xml/xml-recording.js';
5
+ import { HttpClientNode } from "./http-client-node.js";
6
+ import { MusicBrainzApi as MusicBrainzApiDefault } from "./musicbrainz-api.js";
7
+ export * from './musicbrainz.types.js';
8
+ export * from './http-client.js';
9
+ export declare class MusicBrainzApi extends MusicBrainzApiDefault {
10
+ protected initHttpClient(): HttpClientNode;
11
+ login(): Promise<boolean>;
12
+ /**
13
+ * Logout
14
+ */
15
+ logout(): Promise<boolean>;
16
+ }
@@ -0,0 +1,71 @@
1
+ import { StatusCodes as HttpStatus } from 'http-status-codes';
2
+ import Debug from 'debug';
3
+ export { XmlMetadata } from './xml/xml-metadata.js';
4
+ export { XmlIsrc } from './xml/xml-isrc.js';
5
+ export { XmlIsrcList } from './xml/xml-isrc-list.js';
6
+ export { XmlRecording } from './xml/xml-recording.js';
7
+ import { HttpClientNode } from "./http-client-node.js";
8
+ import { MusicBrainzApi as MusicBrainzApiDefault } from "./musicbrainz-api.js";
9
+ export * from './musicbrainz.types.js';
10
+ export * from './http-client.js';
11
+ /*
12
+ * https://musicbrainz.org/doc/Development/XML_Web_Service/Version_2#Subqueries
13
+ */
14
+ const debug = Debug('musicbrainz-api-node');
15
+ export class MusicBrainzApi extends MusicBrainzApiDefault {
16
+ initHttpClient() {
17
+ return new HttpClientNode({
18
+ baseUrl: this.config.baseUrl,
19
+ timeout: 20 * 1000,
20
+ userAgent: `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
21
+ });
22
+ }
23
+ async login() {
24
+ if (!this.config.botAccount?.username)
25
+ throw new Error('bot username should be set');
26
+ if (!this.config.botAccount?.password)
27
+ throw new Error('bot password should be set');
28
+ if (this.session?.loggedIn) {
29
+ const cookies = await this.httpClient.getCookies();
30
+ return cookies.indexOf('musicbrainz_server_session') !== -1;
31
+ }
32
+ this.session = await this.getSession();
33
+ const redirectUri = '/success';
34
+ const formData = {
35
+ username: this.config.botAccount.username,
36
+ password: this.config.botAccount.password,
37
+ csrf_session_key: this.session.csrf.sessionKey,
38
+ csrf_token: this.session.csrf.token,
39
+ remember_me: '1'
40
+ };
41
+ const response = await this.httpClient.postForm('login', formData, {
42
+ query: {
43
+ returnto: redirectUri
44
+ },
45
+ followRedirects: false
46
+ });
47
+ const success = response.status === HttpStatus.MOVED_TEMPORARILY && response.headers.get('location') === redirectUri;
48
+ if (success) {
49
+ this.session.loggedIn = true;
50
+ }
51
+ return success;
52
+ }
53
+ /**
54
+ * Logout
55
+ */
56
+ async logout() {
57
+ const redirectUri = '/success';
58
+ const response = await this.httpClient.post('logout', {
59
+ followRedirects: false,
60
+ query: {
61
+ returnto: redirectUri
62
+ }
63
+ });
64
+ const success = response.status === HttpStatus.MOVED_TEMPORARILY && response.headers.get('location') === redirectUri;
65
+ if (success && this.session) {
66
+ this.session.loggedIn = true;
67
+ }
68
+ return success;
69
+ }
70
+ }
71
+ //# sourceMappingURL=musicbrainz-api-node.js.map