A MusicBrainz-API-client for reading and submitting metadata.
- Access Metadata: Retrieve detailed metadata from the MusicBrainz database.
- Submit metadata: Easily submit new metadata to MusicBrainz.
- Smart throttling: Implements intelligent throttling, allowing bursts of requests while adhering to MusicBrainz rate limits.
- TypeScript Definitions: Fully typed with built-in TypeScript definitions for a seamless development experience.
Module: version 8 migrated from CommonJS to pure ECMAScript Module (ESM). The distributed JavaScript codebase is compliant with the ECMAScript 2020 (11th Edition) standard.
Note
See also CommonJS backward compatibility
- Node.js: Requires Node.js version 20 or higher.
- Browser: Can be used in browser environments when bundled with a module bundler (not actively tested).
Note
We are looking into making this package usable in the browser as well.
Use Node.js 24 and enable Corepack (corepack enable) to use the Yarn version pinned in package.json.
Install dependencies with yarn install --immutable.
yarn testruns the mocked unit tests intest/unitwithtsx. These tests require no credentials and reject unmocked network requests.yarn build:devcompiles the library and tests with TypeScript.yarn test-coverageruns the mocked tests with coverage.yarn test-coverage:compiledruns the compiled mocked tests with coverage, as in CI. Runyarn build:devfirst.yarn test:liveruns the live integration tests intest/live;yarn test:live:compiledruns their compiled equivalents.
Live integration tests run only when both MBUSER and MBPWD are non-empty credentials for an account on test.musicbrainz.org.
When either is unavailable, including fork PR workflows, the entire live suite is skipped before making any requests.
CI always runs the mocked suite, enforcing per-file coverage of at least 95% for lines, statements and functions, and 85% for branches.
Coverage reports include only the mocked suite so PR coverage does not depend on access to secrets or live services.
If you find this project useful and would like to support its development, consider sponsoring or contributing:
-
Buy me a coffee:
MusicBrainz requires all API clients to identify their application.
Ensure you set the User-Agent header by providing appName, appVersion, and appContactInfo when configuring the client.
This library will automatically handle this for you.
If you plan to use this module for submitting metadata, please ensure you comply with the MusicBrainz Code of conduct/Bots.
import { MusicBrainzApi } from 'musicbrainz-api';
const mbApi = new MusicBrainzApi({
appName: 'my-app',
appVersion: '0.1.0',
appContactInfo: 'user@mail.org',
});Note
See also CommonJS backward compatibility
const config = {
// Optional: MusicBrainz bot account credentials
botAccount: {
username: 'myUserName_bot',
password: 'myPassword',
},
// Optional: API base URL (default: 'https://musicbrainz.org')
baseUrl: 'https://musicbrainz.org',
// Required: Application details
appName: 'my-app',
appVersion: '0.1.0',
appContactInfo: 'user@mail.org',
// Optional: OAuth Bearer access token for authenticated requests.
// Requires an HTTPS `baseUrl`. For web-service requests, `botAccount` is not needed.
// Submission scopes: `submit_isrc`, `submit_barcode`.
accessToken: 'eyJ...',
// Optional: Proxy settings (default: no proxy server)
proxy: {
host: 'localhost',
port: 8888,
},
// Optional: Disable rate limiting (default: false)
disableRateLimiting: false,
// Optional: Set max number of request with X seconds
// (default: 15 requests every 18 seconds)
rateLimit: [15, 18],
// Optional: Maximum GET attempts, including the initial request (default: 10)
// Set to 1 to disable retries
retryLimit: 10
};
const mbApi = new MusicBrainzApi(config);The MusicBrainz API allows you to look up various entities. Here’s how to use the lookup function:
You can use the lookup function, to look up an entity, when you have the MBID for that entity. MusicBrainz API documentation: MusicBrainz API - Lookups
const artist = await mbApi.lookup('artist', 'ab2528d9-719f-4261-8098-21849222a0f2', ['recordings']);Genre and identifier lookups are also supported:
const genre = await mbApi.lookup('genre', genreId);
const genres = await mbApi.getGenres({ limit: 25, offset: 0 });
const collectionGenres = await mbApi.browse('genre', { collection: collectionId });
const recordings = await mbApi.lookup('isrc', 'GBAAA9900303', ['artist-credits']);
const works = await mbApi.lookup('iswc', iswc);
const disc = await mbApi.lookup('discid', discId, ['recordings'], { cdstubs: 'no' });Disc lookups return a disc with releases, a CD stub, or a release list for a fuzzy TOC match.
ISRC lookups return { isrc, recordings }; ISWC lookups return a work list with work-count and work-offset.
Pass release type and status filters as the fourth lookup argument when including releases:
const artistReleases = await mbApi.lookup('artist', artistId, ['releases'], {
type: ['album', 'audio drama'],
status: ['official']
});Authenticated user data can be fetched with getUserTags({ entity, id }) and getUserRating({ entity, id }).
Use search() with a Lucene query for scored search results and browse() with a linked entity ID for paginated
browse results. Existing search() calls with browse parameters remain supported and now infer the browse response type.
Search responses do not expand inc; fetch additional information through lookup or browse requests.
Return types are inferred from the request. Lookup and browse results expose their guaranteed fields directly, so
artist['sort-name'], artist.ipis, and work.languages need no optional checks. Search hits and nested references
use the shared entity interfaces because they can contain less information. Include-dependent properties such as
media, relations, and releases remain optional. In a complete result, null represents an unknown value; optional
properties can be omitted.
Arguments:
- entity (
string):'area'|'artist'|'collection'|'instrument'|'label'|'place'|'release'|'release-group'|'recording'|'series'|'work'|'url'|'event'|'genre'|'isrc'|'iswc'|'discid' - MBID (
string): (MusicBrainz identifier) - include arguments (
string[]), see Include arguments
There is special method to lookup URL entity / entities by one, or an array of URLs (MusicBrainz API documentation: url (by text)):
const urls = await mbApi.lookupUrl(['https://open.spotify.com/track/2AMysGXOe0zzZJMtH3Nizb', 'https://open.spotify.com/track/78Teboqh9lPIxWlIW5RMQL']);or
const url = await mbApi.lookupUrl('https://open.spotify.com/track/2AMysGXOe0zzZJMtH3Nizb');Arguments:
- url (
string|string[]): URL or array of URLs - include arguments (
string[]), see Include arguments
Note that the return type is different, depending on if a single URL or an array of URLs is provided.
Browse requests are a direct lookup of all the entities directly linked to another entity ("directly linked" here meaning it does not include entities linked by a relationship).
For example, browse releases:
const artist_mbid = 'ab2528d9-719f-4261-8098-21849222a0f2';
const releases = await mbApi.browse('release', {
track_artist: artist_mbid,
limit: 0,
offset: 0,
}, ['url-rels', 'isrcs', 'recordings']);For the optional include arguments (string[]), see Include arguments.
const artists = await mbApi.browse('artist', query);
const artists = await mbApi.browse('artist', query, ['area', 'collection']);| Query argument | Query value |
|---|---|
query.area |
Area MBID |
query.collection |
Collection MBID |
query.recording |
Recording MBID |
query.release |
Release MBID |
query.release-group |
Release-group MBID |
query.work |
Work MBID |
const collections = await mbApi.browse('collection', query);
const collections = await mbApi.browse('collection', query, ['area', 'artist']);| Query argument | Query value |
|---|---|
query.area |
Area MBID |
query.artist |
Artist MBID |
query.editor |
Editor MBID |
query.event |
Event MBID |
query.label |
Label MBID |
query.place |
Place MBID |
query.recording |
Recording MBID |
query.release |
Release MBID |
query.release-group |
Release-group MBID |
query.work |
Work MBID |
const events = await mbApi.browse('event', query);
const events = await mbApi.browse('instrument', query, ['area', 'artist']);| Query argument | Query value |
|---|---|
query.area |
Area MBID |
query.artist |
Artist MBID |
query.collection |
Collection MBID |
query.place |
Place MBID |
const instruments = await mbApi.browse('instrument', query);
const instruments = await mbApi.browse('instrument', query, ['collection']);| Query argument | Query value |
|---|---|
query.collection |
Collection MBID |
const labels = await mbApi.browse('label', query);
const places = await mbApi.browse('place', query, ['area', 'collection']);| Query argument | Query value |
|---|---|
query.area |
Area MBID |
query.collection |
Collection MBID |
query.release |
Release MBID |
const places = await mbApi.browse('place', query);
const places = await mbApi.browse('place', query, ['area', 'collection']);| Query argument | Query value |
|---|---|
query.area |
Area MBID |
query.collection |
Collection MBID |
const recordings = await mbApi.browse('recording', query, ['artist']);| Query argument | Query value |
|---|---|
query.artist |
Area MBID |
query.collection |
Collection MBID |
query.release |
Release MBID |
query.work |
Work MBID |
const releases = await mbApi.browse('release', query);
const releases = await mbApi.browse('release', query, ['artist', 'track']);| Query argument | Query value |
|---|---|
query.area |
Area MBID |
query.artist |
Artist MBID |
query.editor |
Editor MBID |
query.event |
Event MBID |
query.label |
Label MBID |
query.place |
Place MBID |
query.recording |
Recording MBID |
query.release |
Release MBID |
query.release-group |
Release-group MBID |
query.work |
Work MBID |
const releaseGroups = await mbApi.browse('release-group', query);
const releaseGroups = await mbApi.browse('release-group', query, ['artist', 'release']);| Query argument | Query value |
|---|---|
query.artist |
Artist MBID |
query.collection |
Collection MBID |
query.release |
Release MBID |
const series = await mbApi.browse('series');
const series = await mbApi.browse('series', ['collection']);| Query argument | Query value |
|---|---|
query.area |
Area MBID |
query.artist |
Artist MBID |
query.editor |
Editor MBID |
query.event |
Event MBID |
query.label |
Label MBID |
query.place |
Place MBID |
query.recording |
Recording MBID |
query.release |
Release MBID |
query.release-group |
Release-group MBID |
query.work |
Work MBID |
const works = await mbApi.browse('work');
const series = await mbApi.browse('series', ['artist', 'collection']);| Query argument | Query value |
|---|---|
query.artist |
Artist MBID |
query.xollection |
Collection MBID |
const urls = await mbApi.browse('url');
const series = await mbApi.browse('series', ['artist', 'collection', 'artist-rels']);| Query argument | Query value |
|---|---|
query.artist |
Artist MBID |
query.xollection |
Collection MBID |
Implements MusicBrainz API: Search.
There are different search fields depending on the entity.
Searches can be performed using the generic search function: query(entity: mb.EntityType, query: string | IFormData, offset?: number, limit?: number): Promise<entity>
Arguments:
- Entity type, which can be one of:
annotation: search fieldsarea: search fieldsartist: search fieldscdstub: search fieldsevent: search fieldsinstrument: search fieldslabel: search fieldsplace: search fieldsrecording: search fieldsrelease: search fieldsrelease-group: search fieldsseries: search fieldstag: search fieldsurl: search fieldswork: search fields
query {query: string, offset: number, limit: number}query.query: supports the full Lucene Search syntax; you can find a detailed guide at Lucene Search Syntax. For example, you can set conditions while searching for a name with the AND operator.query.offset: optional, return search results starting at a given offset. Used for paging through more than one page of results.limit.query: optional, an integer value defining how many entries should be returned. Only values between 1 and 100 (both inclusive) are allowed. If not given, this defaults to 25.
For example, to search for release-group: "We Will Rock You" by Queen:
const query = 'query=artist:"Queen" AND release:"We Will Rock You"';
const result = await mbApi.search('release-group', {query}); mbApi.search('area', 'Île-de-France');Search a release with the barcode 602537479870:
mbApi.search('release', {query: {barcode: 602537479870}});Same as previous example, but automatically serialize parameters to search query
mbApi.search('release', 'barcode: 602537479870');Search artist:
const result = await mbApi.search('artist', {query: 'Stromae'});Search release-group:
const result = await mbApi.search('release-group', {query: 'Racine carrée'});Search a combination of a release-group and an artist.
const result = await mbApi.search('release-group', {artist: 'Racine carrée', releasegroup: 'Stromae'});Include Arguments which allow you to request more information to be included about the entity.
| entity | supported include arguments |
|---|---|
area |
|
artist |
recordings, releases, release-groups, works |
collection |
user-collections (includes private collections, requires authentication) |
event |
|
genre |
|
instrument |
|
label |
releases |
place |
|
recording |
artists, releases, release-groups, isrcs, url-rels |
release |
artists, collections, labels, recordings, release-groups |
release-group |
artists, releases |
series |
|
work |
|
url |
Some additional include parameters are supported to specify how much of the data about the linked entities should be included:
| include argument | Description |
|---|---|
discids |
include discids for all media in the releases |
media |
include media for all releases, this includes the # of tracks on each medium and its format. |
isrcs |
user-collections (includes private collections, requires authentication)include isrcs for all recordings |
artist-credits |
include artists credits for all releases and recordings |
various-artists |
include only those releases where the artist appears on one of the tracks, but not in the artist credit for the release itself (this is only valid on entity "artist" and include argument "releases request"). |
| include argument | Description |
|---|---|
aliases |
include artist, label, area or work aliases; treat these as a set, as they are not deliberately ordered |
annotation |
include annotation |
tags, ratings |
include tags and/or ratings for the entity |
user-tags, user-ratings |
same as above, but only return the tags and/or ratings submitted by the specified user |
genres, user-genres |
include genres (tags in the genres list): either all or the ones submitted by the user, respectively |
You can request relationships with the appropriate includes:
area-relsartist-relsevent-relsgenre-relsinstrument-relslabel-relsplace-relsrecording-relsrelease-relsrelease-group-relsseries-relsurl-relswork-rels
These will load relationships between the requested entity and the specific entity type. For example, if you request "work-rels" when looking up an artist, you'll get all the relationships between this artist and any works, and if you request "artist-rels" you'll get the relationships between this artist and any other artists. As such, keep in mind requesting "artist-rels" for an artist, "release-rels" for a release, etc. will not load all the relationships for the entity, just the ones to other entities of the same type.
In a release request, you might also be interested on relationships for the recordings linked to the release, or the release group linked to the release, or even for the works linked to those recordings that are linked to the release (for example, to find out who played guitar on a specific track, who wrote the lyrics for the song being performed, or whether the release group is part of a series). Similarly, for a recording request, you might want to get the relationships for any linked works. There are three additional includes for this:
recording-level-relsrelease-group-level-rels(for releases only)work-level-rels
Submitting data via XML POST may be done using personal MusicBrainz credentials.
Using the XML ISRC submission API.
const mbid_Formidable = '16afa384-174e-435e-bfa3-5591accda31c';
const isrc_Formidable = 'BET671300161';
const xmlMetadata = new XmlMetadata();
const xmlRecording = xmlMetadata.pushRecording(mbid_Formidable);
xmlRecording.isrcList.pushIsrc(isrc_Formidable);
await mbApi.post('recording', xmlMetadata);The website form methods login(), logout(), editEntity(), and addIsrc()
have been removed, together with the CSRF helpers and cookie store. This is a
breaking API change. The Node and default entrypoints now share the same stateless
web-service client.
The test server redirects website login and edit requests to MetaBrainz OAuth;
the old username/password form flow no longer establishes a session. Previously,
editEntity() could mistake a redirect to sign-in for a successful edit.
For ISRC submissions, use XmlMetadata with post('recording', metadata) as shown
above. XML web-service submissions support an OAuth accessToken or Digest
botAccount credentials and do not require login().
The previously removed addUrlToRecording() and addSpotifyIdToRecording()
methods have no replacement in the public web-service API. Edit URL relationships
through the MusicBrainz website.
This library also supports the Cover Art Archive API.
import { CoverArtArchiveApi } from 'musicbrainz-api';
const coverArtArchiveApiClient = new CoverArtArchiveApi();
async function fetchCoverArt(releaseMbid, coverType = '') {
const coverInfo = await coverArtArchiveApiClient.getReleaseCovers(releaseMbid);
for(const image of coverInfo.images) {
console.log(`Cover art front=${image.front} back=${image.back} url=${image.image}`);
}
}
fetchCoverArt('976e0677-a480-4a5e-a177-6a86c1900bbf').catch(error => {
console.error(`Failed to fetch cover art: ${error.message}`);
})import { CoverArtArchiveApi } from 'musicbrainz-api';
const coverArtArchiveApiClient = new CoverArtArchiveApi();
async function fetchCoverArt(releaseMbid, coverType = '') {
const coverInfo = await coverArtArchiveApiClient.getReleaseCover(releaseMbid, 'front');
console.log(`Cover art url=${coverInfo.url}`);
}
fetchCoverArt('976e0677-a480-4a5e-a177-6a86c1900bbf').catch(error => {
console.error(`Failed to fetch cover art: ${error.message}`);
})import { CoverArtArchiveApi } from 'musicbrainz-api';
const coverArtArchiveApiClient = new CoverArtArchiveApi();
async function fetchCoverArt(releaseMbid, coverType = '') {
const coverInfo = await coverArtArchiveApiClient.getReleaseGroupCovers(releaseMbid);
for(const image of coverInfo.images) {
console.log(`Cover art front=${image.front} back=${image.back} url=${image.image}`);
}
}
fetchCoverArt('976e0677-a480-4a5e-a177-6a86c1900bbf').catch(error => {
console.error(`Failed to fetch cover art: ${error.message}`);
})import { CoverArtArchiveApi } from 'musicbrainz-api';
const coverArtArchiveApiClient = new CoverArtArchiveApi();
async function fetchCoverArt(releaseMbid, coverType = '') {
const coverInfo = await coverArtArchiveApiClient.getReleaseGroupCover(releaseMbid, 'front');
console.log(`Cover art url=${coverInfo.url}`);
}
fetchCoverArt('976e0677-a480-4a5e-a177-6a86c1900bbf').catch(error => {
console.error(`Failed to fetch cover art: ${error.message}`);
})I recommend CommonJS projects to consider upgrading their project to ECMAScript Module (ESM). Please continue reading how to use musicbrainz-api in a CommonJS project.
Using Node.js ≥ 22, which is support loading ESM module via require, you can use:
const { MusicBrainzApi } = require('musicbrainz-api');Other CommonJS projects have to use dynamic import to load the musicbrainz-api pure ESM module:
async function run() {
// Dynamically loads the ESM module in a CommonJS project
const { MusicBrainzApi } = await import('musicbrainz-api');
};
run();This is known not to work in TypeScript CommonJS projects, as the TypeScript compiler, in my opinion,
incorrectly converts the dynamic import
to require(). To perform the dynamic import in TypeScript, you can use load-esm:
import {loadEsm} from 'load-esm';
async function run() {
// Dynamically loads the ESM module in a TypeScript CommonJS project
const { MusicBrainzApi } = await loadEsm<typeof import('musicbrainz-api')>('musicbrainz-api');
};
run();See the contributing guide for code style and development commands.
This project is licensed under the MIT License. Feel free to use, modify, and distribute as needed.
