musicbrainz-api
Advanced tools
| export type CovertType = 'Front' | 'Back' | 'Booklet' | 'Medium' | 'Obi' | 'Spine' | 'Track' | 'Tray' | 'Sticker' | 'Poster' | 'Liner' | 'Watermark' | 'Raw/Unedited' | 'Matrix/Runout' | 'Top' | 'Bottom' | 'Other'; | ||
| export interface IImage { | ||
| types: CovertType[]; | ||
| front: boolean; | ||
| back: boolean; | ||
| edit: number; | ||
| image: string; | ||
| comment: string; | ||
| approved: boolean; | ||
| id: string; | ||
| thumbnails: { | ||
| large: string; | ||
| small: string; | ||
| '250': string; | ||
| '500'?: string; | ||
| '1200'?: string; | ||
| }; | ||
| } | ||
| export interface ICoverInfo { | ||
| images: IImage[]; | ||
| release: string; | ||
| } | ||
| export declare class CoverArtArchiveApi { | ||
| private host; | ||
| private getJson; | ||
| /** | ||
| * | ||
| * @param releaseId MusicBrainz Release MBID | ||
| */ | ||
| getReleaseCovers(releaseId: string, coverType?: 'front' | 'back'): Promise<ICoverInfo>; | ||
| } |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.CoverArtArchiveApi = void 0; | ||
| /* eslint-disable-next-line */ | ||
| const got_1 = require("got"); | ||
| class CoverArtArchiveApi { | ||
| constructor() { | ||
| this.host = 'coverartarchive.org'; | ||
| } | ||
| async getJson(path) { | ||
| const response = await got_1.default.get('https://' + this.host + path, { | ||
| headers: { | ||
| Accept: `application/json` | ||
| }, | ||
| responseType: 'json' | ||
| }); | ||
| return response.body; | ||
| } | ||
| /** | ||
| * | ||
| * @param releaseId MusicBrainz Release MBID | ||
| */ | ||
| async getReleaseCovers(releaseId, coverType) { | ||
| const path = ['release', releaseId]; | ||
| if (coverType) { | ||
| path.push(coverType); | ||
| } | ||
| const info = await this.getJson('/' + path.join('/')); | ||
| // Hack to correct http addresses into https | ||
| if (info.release && info.release.startsWith('http:')) { | ||
| info.release = 'https' + info.release.substring(4); | ||
| } | ||
| return info; | ||
| } | ||
| } | ||
| exports.CoverArtArchiveApi = CoverArtArchiveApi; | ||
| //# sourceMappingURL=coverartarchive-api.js.map |
| export * from './coverartarchive-api'; | ||
| export * from './musicbrainz-api'; |
+19
| "use strict"; | ||
| var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
| if (k2 === undefined) k2 = k; | ||
| var desc = Object.getOwnPropertyDescriptor(m, k); | ||
| if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
| desc = { enumerable: true, get: function() { return m[k]; } }; | ||
| } | ||
| Object.defineProperty(o, k2, desc); | ||
| }) : (function(o, m, k, k2) { | ||
| if (k2 === undefined) k2 = k; | ||
| o[k2] = m[k]; | ||
| })); | ||
| var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
| for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| __exportStar(require("./coverartarchive-api"), exports); | ||
| __exportStar(require("./musicbrainz-api"), exports); | ||
| //# sourceMappingURL=index.js.map |
@@ -149,2 +149,7 @@ export { XmlMetadata } from './xml/xml-metadata'; | ||
| /** | ||
| * Lookup series | ||
| * @param seriesId Series MBID | ||
| */ | ||
| lookupSeries(seriesId: string): Promise<mb.ISeries>; | ||
| /** | ||
| * Lookup work | ||
@@ -151,0 +156,0 @@ * @param workId Work MBID |
+20
-34
@@ -38,3 +38,2 @@ "use strict"; | ||
| const util_1 = require("util"); | ||
| const retries = 3; | ||
| const debug = Debug('musicbrainz-api'); | ||
@@ -105,31 +104,11 @@ class MusicBrainzApi { | ||
| }; | ||
| this.rateLimiter = new rate_limiter_1.RateLimiter(60, 50); | ||
| this.rateLimiter = new rate_limiter_1.RateLimiter(15, 18); | ||
| } | ||
| async restGet(relUrl, query = {}, attempt = 1) { | ||
| query.fmt = 'json'; | ||
| let response; | ||
| await this.rateLimiter.limit(); | ||
| do { | ||
| response = await got_1.default.get('ws/2' + relUrl, Object.assign({ searchParams: query, responseType: 'json' }, this.options)); | ||
| if (response.statusCode !== 503) | ||
| break; | ||
| debug('Rate limiter kicked in, slowing down...'); | ||
| await rate_limiter_1.RateLimiter.sleep(500); | ||
| } while (true); | ||
| switch (response.statusCode) { | ||
| case http_status_codes_1.StatusCodes.OK: | ||
| return response.body; | ||
| case http_status_codes_1.StatusCodes.BAD_REQUEST: | ||
| case http_status_codes_1.StatusCodes.NOT_FOUND: | ||
| throw new Error(`Got response status ${response.statusCode}: ${(0, http_status_codes_1.getReasonPhrase)(response.status)}`); | ||
| case http_status_codes_1.StatusCodes.SERVICE_UNAVAILABLE: // 503 | ||
| default: | ||
| const msg = `Got response status ${response.statusCode} on attempt #${attempt} (${(0, http_status_codes_1.getReasonPhrase)(response.status)})`; | ||
| debug(msg); | ||
| if (attempt < retries) { | ||
| return this.restGet(relUrl, query, attempt + 1); | ||
| } | ||
| else | ||
| throw new Error(msg); | ||
| } | ||
| const response = await got_1.default.get('ws/2' + relUrl, Object.assign(Object.assign({}, this.options), { searchParams: query, responseType: 'json', retry: { | ||
| limit: 10 | ||
| } })); | ||
| return response.body; | ||
| } | ||
@@ -222,2 +201,9 @@ // ----------------------------------------------------------------------------------------------------------------- | ||
| /** | ||
| * Lookup series | ||
| * @param seriesId Series MBID | ||
| */ | ||
| lookupSeries(seriesId) { | ||
| return this.lookupEntity('series', seriesId); | ||
| } | ||
| /** | ||
| * Lookup work | ||
@@ -366,6 +352,6 @@ * @param workId Work MBID | ||
| await this.rateLimiter.limit(); | ||
| const response = await got_1.default.post(path, Object.assign({ searchParams: { client: clientId }, headers: { | ||
| const response = await got_1.default.post(path, Object.assign(Object.assign({}, this.options), { searchParams: { client: clientId }, headers: { | ||
| authorization: digest, | ||
| 'Content-Type': 'application/xml' | ||
| }, body: postData, throwHttpErrors: false }, this.options)); | ||
| }, body: postData, throwHttpErrors: false })); | ||
| if (response.statusCode === http_status_codes_1.StatusCodes.UNAUTHORIZED) { | ||
@@ -402,5 +388,5 @@ // Respond to digest challenge | ||
| }; | ||
| const response = await got_1.default.post('login', Object.assign({ followRedirect: false, searchParams: { | ||
| const response = await got_1.default.post('login', Object.assign(Object.assign({}, this.options), { followRedirect: false, searchParams: { | ||
| returnto: redirectUri | ||
| }, form: formData }, this.options)); | ||
| }, form: formData })); | ||
| const success = response.statusCode === http_status_codes_1.StatusCodes.MOVED_TEMPORARILY && response.headers.location === redirectUri; | ||
@@ -417,5 +403,5 @@ if (success) { | ||
| const redirectUri = '/success'; | ||
| const response = await got_1.default.get('logout', Object.assign({ followRedirect: false, searchParams: { | ||
| const response = await got_1.default.get('logout', Object.assign(Object.assign({}, this.options), { followRedirect: false, searchParams: { | ||
| returnto: redirectUri | ||
| } }, this.options)); | ||
| } })); | ||
| const success = response.statusCode === http_status_codes_1.StatusCodes.MOVED_TEMPORARILY && response.headers.location === redirectUri; | ||
@@ -441,3 +427,3 @@ if (success && this.session) { | ||
| formData.remember_me = 1; | ||
| const response = await got_1.default.post(`${entity}/${mbid}/edit`, Object.assign({ form: formData, followRedirect: false }, this.options)); | ||
| const response = await got_1.default.post(`${entity}/${mbid}/edit`, Object.assign(Object.assign({}, this.options), { form: formData, followRedirect: false })); | ||
| if (response.statusCode === http_status_codes_1.StatusCodes.OK) | ||
@@ -541,3 +527,3 @@ throw new Error(`Failed to submit form data`); | ||
| async getSession() { | ||
| const response = await got_1.default.get('login', Object.assign({ followRedirect: false, responseType: 'text' }, this.options)); | ||
| const response = await got_1.default.get('login', Object.assign(Object.assign({}, this.options), { followRedirect: false, responseType: 'text' })); | ||
| return { | ||
@@ -544,0 +530,0 @@ csrf: MusicBrainzApi.fetchCsrf(response.body) |
@@ -223,2 +223,8 @@ import DateTimeFormat = Intl.DateTimeFormat; | ||
| } | ||
| export interface ISeries extends IEntity { | ||
| name: string; | ||
| type: string; | ||
| disambiguation: string; | ||
| 'type-id': string; | ||
| } | ||
| export interface IUrl extends IEntity { | ||
@@ -225,0 +231,0 @@ id: string; |
@@ -6,4 +6,4 @@ export declare class RateLimiter { | ||
| private readonly period; | ||
| constructor(period: number, maxCalls: number); | ||
| constructor(maxCalls: number, period: number); | ||
| limit(): Promise<void>; | ||
| } |
@@ -10,5 +10,6 @@ "use strict"; | ||
| } | ||
| constructor(period, maxCalls) { | ||
| constructor(maxCalls, period) { | ||
| this.maxCalls = maxCalls; | ||
| this.queue = []; | ||
| debug(`Rate limiter initialized with max ${maxCalls} calls in ${period} seconds.`); | ||
| this.period = 1000 * period; | ||
@@ -22,2 +23,3 @@ } | ||
| } | ||
| // debug(`Current rate is ${this.queue.length} per ${this.period / 1000} sec`); | ||
| if (this.queue.length >= this.maxCalls) { | ||
@@ -24,0 +26,0 @@ const delay = this.queue[0] + this.period - now; |
+10
-4
| { | ||
| "name": "musicbrainz-api", | ||
| "version": "0.11.0", | ||
| "version": "0.12.0", | ||
| "description": "MusicBrainz API client for reading and submitting metadata", | ||
| "main": "lib/musicbrainz-api", | ||
| "types": "lib/musicbrainz-api", | ||
| "main": "lib/index", | ||
| "types": "lib/index", | ||
| "author": { | ||
@@ -23,3 +23,9 @@ "name": "Borewit", | ||
| "submit", | ||
| "metabrainz" | ||
| "metabrainz", | ||
| "Cover Art Archive", | ||
| "coverartarchive", | ||
| "coverartarchive.org", | ||
| "album art", | ||
| "covers", | ||
| "download covers" | ||
| ], | ||
@@ -26,0 +32,0 @@ "license": "MIT", |
+39
-0
@@ -118,2 +118,16 @@ [](https://github.com/Borewit/musicbrainz-api/actions/workflows/nodejs-ci.yml) | ||
| ### Lookup collection | ||
| Lookup an instrument | ||
| ```js | ||
| const collection = await mbApi.lookupCollection('de4fdfc4-53aa-458a-b463-8761cc7f5af8'); | ||
| ``` | ||
| Lookup an event | ||
| ```js | ||
| const event = await mbApi.lookupEvent('6d32c658-151e-45ec-88c4-fb8787524d61'); | ||
| ``` | ||
| ### Lookup instrument | ||
@@ -141,2 +155,6 @@ | ||
| ```js | ||
| const place = await mbApi.lookupSeries('1ae6c9bc-2931-4d75-bee4-3dc53dfd246a'); | ||
| ``` | ||
| The second argument can be used to pass [subqueries](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2#Subqueries), which will return more (nested) information: | ||
@@ -480,2 +498,23 @@ ```js | ||
| ## Cover Art Archive API | ||
| Implementation of the [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API). | ||
| ```js | ||
| import {CoverArtArchiveApi} from 'musicbrainz-api'; | ||
| coverArtArchiveApiClient.getReleaseCovers(releaseMbid).then(releaseCoverInfo => { | ||
| console.log('Release cover info', releaseCoverInfo); | ||
| }); | ||
| coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'front').then(releaseCoverInfo => { | ||
| console.log('Get best front cover', releaseCoverInfo); | ||
| }); | ||
| coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'back').then(releaseCoverInfo => { | ||
| console.log('Get best back cover', releaseCoverInfo); | ||
| }); | ||
| ``` | ||
| ## Compatibility | ||
@@ -482,0 +521,0 @@ |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
81637
4.87%22
22.22%1815
4.97%521
8.09%