--- name: musicbrainz-scraper description: Read MusicBrainz artists, releases, recordings, labels and works as structured records via the Apify Actor arman-bd/musicbrainz-scraper. Returns one row per entity with its MBID, name and sort name, sub-type, disambiguation, country and area, begin and end dates, recording length, credited artists, community tags, curated genres, aliases, ISRCs, ISWCs, release labels with catalogue numbers, a release count, community rating and a flattened relationship graph. Use when a task needs music reference metadata, ISRC or ISWC identity matching, discography counts, credit and rights research, or a lookup tool that stops an agent guessing at music facts. Not for audio, cover art, streaming links, chart or sales data, lyrics, or more than one entity type in a single run. --- # MusicBrainz Scraper Apify Actor `arman-bd/musicbrainz-scraper`. Give it search queries or MBIDs and one entity type, get one dataset record per entity. It runs without credentials. Every record carries every field, so a field that does not apply to the entity type comes back `null` or `[]` rather than disappearing. ## When to use it - Seeding a music catalogue with open reference metadata: MBIDs, names, types, dates, countries. - Matching a recording across platforms by ISRC, or a composition by ISWC. - Discography work: counting releases per artist or label and tracking the number over time. - Credit and rights research, walking `relations` for band members, writers, producers and official URLs. - Giving an agent a cheap, deterministic music lookup so it stops inventing release dates and personnel. ## When not to use it - Audio, cover art, streaming or purchase links, lyrics, chart positions or sales figures. None of that is here. - Mixed entity types in one run. One `entityType` per run, because the search index and the include list differ per type. Run once per type and merge. - Track listings. A release record carries its labels and credits, not its medium and track breakdown. - High-volume enrichment on a tight deadline. Requests are paced at roughly one per 1.2 seconds by design, so a 500-entity run is measured in tens of minutes. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/musicbrainz-scraper').call({ entityType: 'artist', searchQueries: ['radiohead', 'portishead'], mbids: [], includeRelations: true, maxResults: 20, contactEmail: 'you@example.com', }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~musicbrainz-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"entityType":"recording","mbids":["b1a9c0e9-d987-4042-ae91-78d6a3267d69"],"maxResults":1}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/musicbrainz-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input You need at least one of `searchQueries` or `mbids`. Both together is fine. | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `entityType` | string | no | `"artist"` | One of `artist`, `release`, `recording`, `label`, `work`. Applies to every query and MBID in the run. An unrecognised value throws immediately. | | `searchQueries` | string[] | no | `[]` | Free text or Lucene syntax such as `artist:radiohead AND country:GB`. Each query is paged 100 hits at a time until the cap is reached. | | `mbids` | string[] | no | `[]` | MusicBrainz IDs. A full musicbrainz.org URL is accepted and the UUID is extracted. Anything with no UUID in it is silently dropped. | | `includeRelations` | boolean | no | `false` | Adds the relationship graph to each record. Costs no extra requests, but records get much larger: an established band can carry ninety-odd relations. | | `maxResults` | integer | no | `25` | Total entities saved across all queries and MBIDs combined, 1 to 1000. Not per query. | | `contactEmail` | string | no | `""` | An email or URL identifying you as the caller. Optional, and worth setting for heavy scheduled runs. | **Pacing decides the shape of every run.** Calls are spaced 1.2 seconds apart by a single global limiter, and an entity costs one lookup plus, for artists, labels and recordings, one small extra call that produces the exact `releaseCount`. So budget roughly 2.5 seconds per artist, label or recording and 1.2 seconds per release or work, on top of one call per 100 search hits. A 500-artist run is about 20 minutes, which is beyond the default run timeout: raise it before scheduling one. `includeRelations` is free in request terms and is the one option you can turn on without paying for it in time. MBIDs are resolved before searches, so when the cap is tight your explicit IDs always win. ## Output One record per entity. Every key is present on every record, whatever the entity type. | Field | Type | Notes | |---|---|---| | `entityType` | string | The type you asked for. Same on every row in a run. | | `mbid` | string | MusicBrainz ID, lower-cased. Join and dedup key. | | `name` | string \| null | Display name for artists and labels, title for releases, recordings and works. | | `sortName` | string \| null | Sort form. Populated for artists and labels. | | `type` | string \| null | Sub-type: `Group`, `Person`, `Album`, `Song` and so on. Not the same thing as `entityType`. | | `disambiguation` | string \| null | The community's disambiguation comment, when there is one. | | `country` | string \| null | ISO country code, falling back to the area's ISO code. | | `area` | string \| null | Human-readable area name. | | `beginDate` | string \| null | Means different things per type: life-span start for artists and labels, release date for releases, first-release date for recordings. | | `endDate` | string \| null | Life-span end. Only artists and labels have one. | | `lengthMs` | number \| null | Recording duration in milliseconds. `null` on every other type. | | `artistCredit` | string[] | Credited artist names in order. Populated for releases and recordings; empty for artists, labels and works. | | `tags` | string[] | Community tags. | | `genres` | string[] | Curated genres. Overlaps `tags` but is not the same list. | | `aliases` | string[] | Every known alias, including transliterations and misspellings. | | `isrcs` | string[] | Recording ISRCs. Populated for recordings only, and coverage is uneven because editors contribute them. | | `iswcs` | string[] | Work ISWCs, where the source holds them. Empty on other types. | | `labels` | object[] | Release labels. Populated for releases only. Each: `name`, `mbid`, `catalogNumber`. | | `releaseCount` | number \| null | Exact count of linked releases, from a separate call. Always `null` for `release` and `work`, which have no equivalent filter. | | `rating` | number \| null | Community rating out of 5. Requested for artists, labels and works only, so `null` on releases and recordings. | | `ratingVotes` | number \| null | Vote count behind that rating, same availability. | | `relations` | object[] | Flattened relationship graph, `[]` unless `includeRelations` is on. Each: `type`, `direction`, `targetType`, `targetMbid`, `targetName`, `begin`, `end`. For a URL relation, `targetName` holds the URL. | | `score` | number \| null | Search relevance 0 to 100. `null` for entities that came from `mbids`. | | `matchedQuery` | string \| null | The query that produced this hit. `null` for entities that came from `mbids`. | | `url` | string | Canonical musicbrainz.org page for the entity. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real record, list fields trimmed: ```json { "entityType": "recording", "mbid": "b1a9c0e9-d987-4042-ae91-78d6a3267d69", "name": "Bohemian Rhapsody", "sortName": null, "type": null, "disambiguation": null, "country": null, "area": null, "beginDate": "1975-11-21", "endDate": null, "lengthMs": 355106, "artistCredit": ["Queen"], "tags": ["album rock", "alternative rock", "arena rock", "art rock"], "genres": ["alternative rock", "arena rock", "art rock", "baroque pop"], "aliases": ["Bohemian Rapsody", "Bohemian Rhapsody (remastered 2011)", "…"], "isrcs": ["GBCEE0100112", "GBCEE0500364", "GBCEE7500015", "GBUM71029604"], "iswcs": [], "labels": [], "releaseCount": 352, "rating": null, "ratingVotes": null, "relations": [], "score": null, "matchedQuery": null, "url": "https://musicbrainz.org/recording/b1a9c0e9-d987-4042-ae91-78d6a3267d69", "scrapedAt": "2026-08-06T11:38:26.193Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial run admits that it was partial. ```json { "entityType": "artist", "queriesRequested": 2, "mbidsRequested": 0, "entitiesQueued": 20, "entitiesSaved": 19, "failures": [ { "target": "a74b1b7f-71a5-4011-9441-d0b5e4122711", "kind": "lookup", "error": "not found (404)" } ], "filters": { "entityType": "artist", "includeRelations": true, "maxResults": 20 }, "finishedAt": "2026-08-06T11:38:40.512Z" } ``` `entitiesQueued` is what survived deduplication and the cap; `entitiesSaved` is what actually resolved. A gap between them is fully explained by `failures`, whose entries carry `kind: "search"` when a whole query failed and `kind: "lookup"` when one entity did. A failed search silently costs you all the entities it would have contributed, so treat any `kind: "search"` entry as a much bigger hole than a lookup failure. `entitiesQueued` equal to `maxResults` means you were capped, not exhausted. ## Behaviour to plan around - **`type` and `entityType` are different fields.** `entityType` is what you asked for; `type` is the sub-type MusicBrainz assigned, and it is frequently `null`. Do not collapse them. - **`beginDate` is overloaded.** It is a life-span start for artists and labels, a release date for releases, and a first-release date for recordings. Branch on `entityType` before comparing dates across a mixed dataset. - **Which fields are populated depends on the entity type**, because the include list is type-specific. Recordings get `isrcs` and `artistCredit` but no `rating`. Releases get `labels` and `artistCredit` but no `rating`. Artists, labels and works get `rating` but no `artistCredit`. An empty array here means "not applicable to this type", not "the source has no data". - **`releaseCount` is always `null` for releases and works.** There is no filter that produces one for those types. Do not read that `null` as zero. - **MBIDs come first, searches fill the remainder.** The cap is global, so a run with 30 MBIDs and `maxResults: 25` never touches your search queries at all. - **Duplicates are removed before any lookup.** Overlapping queries, or an MBID that a query also returned, cost you one fetch, not two. - **A malformed MBID or query fails that target only.** 400 and 404 are treated as final and are not retried, because retrying a bad identifier never helps. 429, 503 and other 5xx get three attempts with linear backoff. The run throws only when nothing at all was saved. - **Entries in `mbids` with no UUID in them vanish silently.** Compare `RUN_SUMMARY.mbidsRequested` against the length of your input list. - **`score` is only meaningful for search hits**, where 100 means the index considered it an exact match. It is `null` for direct lookups, which says nothing about quality. - **Empty `isrcs` is normal.** ISRCs are editor-contributed, so a canonical studio recording usually has several and a live capture often none. The Actor reports what is held and never synthesises an identifier. ## Recipes **Resolve artists by name and keep the MBIDs.** Search first, then use the MBIDs in later runs so results stop depending on index relevance. ```json { "entityType": "artist", "searchQueries": ["radiohead", "aphex twin"], "maxResults": 20 } ``` Keep the rows with `score` at or near 100 and store `mbid` against your own identifiers. **Full profile for known entities, with the credit graph.** ```json { "entityType": "artist", "mbids": [ "a74b1b7f-71a5-4011-9441-d0b5e4122711", "https://musicbrainz.org/artist/f23074f4-2c06-477a-bf1d-12fa66e087ee" ], "includeRelations": true, "contactEmail": "you@example.com" } ``` Filter `relations` on `targetType` to separate band members from official URLs. **Harvest ISRCs for identity matching.** Lucene syntax narrows the search index far better than a bare phrase. ```json { "entityType": "recording", "searchQueries": ["artist:queen AND recording:\"bohemian rhapsody\""], "maxResults": 50, "contactEmail": "you@example.com" } ``` Index the rows by every entry in `isrcs`, then join your platform data on that. **Rule of thumb for run length.** Multiply your `maxResults` by 2.5 seconds for artists, labels and recordings, or 1.2 seconds for releases and works, and add 1.2 seconds per 100 search hits. If the answer is over ten minutes, raise the run timeout or split the work across scheduled runs.