a1f6701bac
- gRPC service with MusicBrainz provider - PostgreSQL schema with migrations - Service layer with database-first caching - Repository pattern for data access - YAML configuration support - Research documentation for 17 music metadata projects
903 lines
15 KiB
Markdown
903 lines
15 KiB
Markdown
# GraphBrainz API Reference
|
|
|
|
## Endpoint Configuration
|
|
|
|
| Parameter | Environment Variable | Default |
|
|
|-----------|---------------------|---------|
|
|
| Path | GRAPHBRAINZ_PATH | / |
|
|
| Port | PORT | 3000 |
|
|
| CORS Origin | GRAPHBRAINZ_CORS_ORIGIN | false |
|
|
| GraphiQL | GRAPHBRAINZ_GRAPHIQL | true (development) |
|
|
|
|
## Query Types
|
|
|
|
GraphBrainz exposes four primary query entry points:
|
|
|
|
### 1. Lookup Queries
|
|
|
|
Direct entity retrieval by MusicBrainz ID (MBID).
|
|
|
|
```graphql
|
|
type Query {
|
|
lookup: LookupQuery
|
|
}
|
|
|
|
type LookupQuery {
|
|
area(mbid: String!): Area
|
|
artist(mbid: String!): Artist
|
|
collection(mbid: String!): Collection
|
|
event(mbid: String!): Event
|
|
instrument(mbid: String!): Instrument
|
|
label(mbid: String!): Label
|
|
place(mbid: String!): Place
|
|
recording(mbid: String!): Recording
|
|
release(mbid: String!): Release
|
|
releaseGroup(mbid: String!): ReleaseGroup
|
|
series(mbid: String!): Series
|
|
url(mbid: String!): URL
|
|
work(mbid: String!): Work
|
|
}
|
|
```
|
|
|
|
**Example**:
|
|
```graphql
|
|
{
|
|
lookup {
|
|
artist(mbid: "5b11f4ce-a62d-471e-81fc-a69a8278c7da") {
|
|
name
|
|
type
|
|
country
|
|
lifeSpan {
|
|
begin
|
|
end
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Browse Queries
|
|
|
|
Retrieve entities linked to a parent entity with cursor-based pagination.
|
|
|
|
```graphql
|
|
type Query {
|
|
browse: BrowseQuery
|
|
}
|
|
|
|
type BrowseQuery {
|
|
areas(
|
|
collection: String
|
|
first: Int
|
|
after: String
|
|
): AreaConnection
|
|
|
|
artists(
|
|
area: String
|
|
collection: String
|
|
recording: String
|
|
release: String
|
|
releaseGroup: String
|
|
work: String
|
|
first: Int
|
|
after: String
|
|
): ArtistConnection
|
|
|
|
collections(
|
|
area: String
|
|
artist: String
|
|
editor: String
|
|
event: String
|
|
label: String
|
|
place: String
|
|
recording: String
|
|
release: String
|
|
releaseGroup: String
|
|
work: String
|
|
first: Int
|
|
after: String
|
|
): CollectionConnection
|
|
|
|
events(
|
|
area: String
|
|
artist: String
|
|
collection: String
|
|
place: String
|
|
first: Int
|
|
after: String
|
|
): EventConnection
|
|
|
|
labels(
|
|
area: String
|
|
collection: String
|
|
release: String
|
|
first: Int
|
|
after: String
|
|
): LabelConnection
|
|
|
|
places(
|
|
area: String
|
|
collection: String
|
|
first: Int
|
|
after: String
|
|
): PlaceConnection
|
|
|
|
recordings(
|
|
artist: String
|
|
collection: String
|
|
release: String
|
|
first: Int
|
|
after: String
|
|
): RecordingConnection
|
|
|
|
releases(
|
|
area: String
|
|
artist: String
|
|
collection: String
|
|
label: String
|
|
recording: String
|
|
releaseGroup: String
|
|
track: String
|
|
trackArtist: String
|
|
first: Int
|
|
after: String
|
|
): ReleaseConnection
|
|
|
|
releaseGroups(
|
|
artist: String
|
|
collection: String
|
|
release: String
|
|
first: Int
|
|
after: String
|
|
): ReleaseGroupConnection
|
|
}
|
|
```
|
|
|
|
**Example**:
|
|
```graphql
|
|
{
|
|
browse {
|
|
releases(
|
|
artist: "5b11f4ce-a62d-471e-81fc-a69a8278c7da"
|
|
first: 10
|
|
) {
|
|
edges {
|
|
node {
|
|
title
|
|
date
|
|
status
|
|
}
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
totalCount
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Search Queries
|
|
|
|
Lucene-based full-text search across entity types.
|
|
|
|
```graphql
|
|
type Query {
|
|
search: SearchQuery
|
|
}
|
|
|
|
type SearchQuery {
|
|
areas(query: String!, first: Int, after: String): AreaConnection
|
|
artists(query: String!, first: Int, after: String): ArtistConnection
|
|
events(query: String!, first: Int, after: String): EventConnection
|
|
instruments(query: String!, first: Int, after: String): InstrumentConnection
|
|
labels(query: String!, first: Int, after: String): LabelConnection
|
|
places(query: String!, first: Int, after: String): PlaceConnection
|
|
recordings(query: String!, first: Int, after: String): RecordingConnection
|
|
releases(query: String!, first: Int, after: String): ReleaseConnection
|
|
releaseGroups(query: String!, first: Int, after: String): ReleaseGroupConnection
|
|
works(query: String!, first: Int, after: String): WorkConnection
|
|
}
|
|
```
|
|
|
|
**Lucene Query Syntax**:
|
|
- `artist:"Radiohead"` - Exact phrase match
|
|
- `artist:Radiohead AND country:GB` - Boolean operators
|
|
- `artist:Radio*` - Wildcard search
|
|
- `begin:[1990 TO 2000]` - Range queries
|
|
- `tag:rock^2 tag:alternative` - Boosting
|
|
|
|
**Example**:
|
|
```graphql
|
|
{
|
|
search {
|
|
artists(query: "artist:Radiohead AND country:GB", first: 5) {
|
|
edges {
|
|
node {
|
|
name
|
|
country
|
|
type
|
|
score
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Node Query (Relay)
|
|
|
|
Global object identification via Relay-compliant node interface.
|
|
|
|
```graphql
|
|
type Query {
|
|
node(id: ID!): Node
|
|
}
|
|
|
|
interface Node {
|
|
id: ID!
|
|
}
|
|
```
|
|
|
|
**Example**:
|
|
```graphql
|
|
{
|
|
node(id: "QXJ0aXN0OjViMTFmNGNlLWE2MmQtNDcxZS04MWZjLWE2OWE4Mjc4YzdkYQ==") {
|
|
... on Artist {
|
|
name
|
|
country
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Entity Types
|
|
|
|
### Artist
|
|
|
|
```graphql
|
|
type Artist implements Node {
|
|
id: ID!
|
|
mbid: MBID!
|
|
name: String
|
|
sortName: String
|
|
disambiguation: String
|
|
type: String
|
|
typeID: MBID
|
|
country: String
|
|
area: Area
|
|
beginArea: Area
|
|
endArea: Area
|
|
lifeSpan: LifeSpan
|
|
gender: String
|
|
genderID: MBID
|
|
ipis: [IPI]
|
|
isnis: [ISNI]
|
|
aliases: [Alias]
|
|
recordings: RecordingConnection
|
|
releases: ReleaseConnection
|
|
releaseGroups: ReleaseGroupConnection
|
|
works: WorkConnection
|
|
relationships: RelationshipConnection
|
|
collections: CollectionConnection
|
|
tags: TagConnection
|
|
|
|
# Extension fields
|
|
fanArt: FanArtImages
|
|
mediaWikiImages: [MediaWikiImage]
|
|
theAudioDB: TheAudioDBArtist
|
|
}
|
|
```
|
|
|
|
### Release
|
|
|
|
```graphql
|
|
type Release implements Node {
|
|
id: ID!
|
|
mbid: MBID!
|
|
title: String
|
|
disambiguation: String
|
|
asin: String
|
|
status: String
|
|
statusID: MBID
|
|
packaging: String
|
|
packagingID: MBID
|
|
quality: String
|
|
date: Date
|
|
country: String
|
|
barcode: String
|
|
artists: [Artist]
|
|
artistCredit: [ArtistCredit]
|
|
labels: [ReleaseLabel]
|
|
media: [Medium]
|
|
releaseGroup: ReleaseGroup
|
|
relationships: RelationshipConnection
|
|
collections: CollectionConnection
|
|
tags: TagConnection
|
|
|
|
# Extension fields
|
|
coverArtArchive: CoverArtArchiveRelease
|
|
}
|
|
```
|
|
|
|
### Recording
|
|
|
|
```graphql
|
|
type Recording implements Node {
|
|
id: ID!
|
|
mbid: MBID!
|
|
title: String
|
|
disambiguation: String
|
|
length: Duration
|
|
video: Boolean
|
|
isrcs: [ISRC]
|
|
artists: [Artist]
|
|
artistCredit: [ArtistCredit]
|
|
releases: ReleaseConnection
|
|
relationships: RelationshipConnection
|
|
collections: CollectionConnection
|
|
tags: TagConnection
|
|
}
|
|
```
|
|
|
|
### ReleaseGroup
|
|
|
|
```graphql
|
|
type ReleaseGroup implements Node {
|
|
id: ID!
|
|
mbid: MBID!
|
|
title: String
|
|
disambiguation: String
|
|
type: String
|
|
typeID: MBID
|
|
primaryType: String
|
|
primaryTypeID: MBID
|
|
secondaryTypes: [String]
|
|
secondaryTypeIDs: [MBID]
|
|
firstReleaseDate: Date
|
|
artists: [Artist]
|
|
artistCredit: [ArtistCredit]
|
|
releases: ReleaseConnection
|
|
relationships: RelationshipConnection
|
|
collections: CollectionConnection
|
|
tags: TagConnection
|
|
}
|
|
```
|
|
|
|
### Area
|
|
|
|
```graphql
|
|
type Area implements Node {
|
|
id: ID!
|
|
mbid: MBID!
|
|
name: String
|
|
sortName: String
|
|
disambiguation: String
|
|
type: String
|
|
typeID: MBID
|
|
iso31661Codes: [String]
|
|
iso31662Codes: [String]
|
|
iso31663Codes: [String]
|
|
lifeSpan: LifeSpan
|
|
aliases: [Alias]
|
|
relationships: RelationshipConnection
|
|
collections: CollectionConnection
|
|
tags: TagConnection
|
|
}
|
|
```
|
|
|
|
### Label
|
|
|
|
```graphql
|
|
type Label implements Node {
|
|
id: ID!
|
|
mbid: MBID!
|
|
name: String
|
|
sortName: String
|
|
disambiguation: String
|
|
type: String
|
|
typeID: MBID
|
|
labelCode: Int
|
|
ipis: [IPI]
|
|
area: Area
|
|
lifeSpan: LifeSpan
|
|
aliases: [Alias]
|
|
releases: ReleaseConnection
|
|
relationships: RelationshipConnection
|
|
collections: CollectionConnection
|
|
tags: TagConnection
|
|
}
|
|
```
|
|
|
|
### Work
|
|
|
|
```graphql
|
|
type Work implements Node {
|
|
id: ID!
|
|
mbid: MBID!
|
|
title: String
|
|
disambiguation: String
|
|
type: String
|
|
typeID: MBID
|
|
language: String
|
|
languages: [String]
|
|
iswcs: [ISWC]
|
|
artists: [Artist]
|
|
relationships: RelationshipConnection
|
|
collections: CollectionConnection
|
|
tags: TagConnection
|
|
}
|
|
```
|
|
|
|
### Event
|
|
|
|
```graphql
|
|
type Event implements Node {
|
|
id: ID!
|
|
mbid: MBID!
|
|
name: String
|
|
disambiguation: String
|
|
type: String
|
|
typeID: MBID
|
|
time: String
|
|
cancelled: Boolean
|
|
setlist: String
|
|
lifeSpan: LifeSpan
|
|
aliases: [Alias]
|
|
relationships: RelationshipConnection
|
|
collections: CollectionConnection
|
|
tags: TagConnection
|
|
}
|
|
```
|
|
|
|
### Place
|
|
|
|
```graphql
|
|
type Place implements Node {
|
|
id: ID!
|
|
mbid: MBID!
|
|
name: String
|
|
disambiguation: String
|
|
type: String
|
|
typeID: MBID
|
|
address: String
|
|
area: Area
|
|
coordinates: Coordinates
|
|
lifeSpan: LifeSpan
|
|
aliases: [Alias]
|
|
relationships: RelationshipConnection
|
|
collections: CollectionConnection
|
|
tags: TagConnection
|
|
}
|
|
```
|
|
|
|
### Instrument
|
|
|
|
```graphql
|
|
type Instrument implements Node {
|
|
id: ID!
|
|
mbid: MBID!
|
|
name: String
|
|
disambiguation: String
|
|
type: String
|
|
typeID: MBID
|
|
description: String
|
|
aliases: [Alias]
|
|
relationships: RelationshipConnection
|
|
collections: CollectionConnection
|
|
tags: TagConnection
|
|
}
|
|
```
|
|
|
|
### Series
|
|
|
|
```graphql
|
|
type Series implements Node {
|
|
id: ID!
|
|
mbid: MBID!
|
|
name: String
|
|
disambiguation: String
|
|
type: String
|
|
typeID: MBID
|
|
aliases: [Alias]
|
|
relationships: RelationshipConnection
|
|
collections: CollectionConnection
|
|
tags: TagConnection
|
|
}
|
|
```
|
|
|
|
### Collection
|
|
|
|
```graphql
|
|
type Collection implements Node {
|
|
id: ID!
|
|
mbid: MBID!
|
|
name: String
|
|
editor: String
|
|
type: String
|
|
typeID: MBID
|
|
entityType: String
|
|
areas: AreaConnection
|
|
artists: ArtistConnection
|
|
events: EventConnection
|
|
instruments: InstrumentConnection
|
|
labels: LabelConnection
|
|
places: PlaceConnection
|
|
recordings: RecordingConnection
|
|
releases: ReleaseConnection
|
|
releaseGroups: ReleaseGroupConnection
|
|
series: SeriesConnection
|
|
works: WorkConnection
|
|
}
|
|
```
|
|
|
|
## Relay Connection Types
|
|
|
|
All list fields return Relay-compliant connection types:
|
|
|
|
```graphql
|
|
type ArtistConnection {
|
|
edges: [ArtistEdge]
|
|
nodes: [Artist]
|
|
pageInfo: PageInfo!
|
|
totalCount: Int
|
|
}
|
|
|
|
type ArtistEdge {
|
|
node: Artist
|
|
cursor: String!
|
|
score: Int # Only present in search results
|
|
}
|
|
|
|
type PageInfo {
|
|
hasNextPage: Boolean!
|
|
hasPreviousPage: Boolean!
|
|
startCursor: String
|
|
endCursor: String
|
|
}
|
|
```
|
|
|
|
### Pagination
|
|
|
|
- `first: Int` - Number of items to return
|
|
- `after: String` - Cursor for pagination
|
|
|
|
**Example**:
|
|
```graphql
|
|
{
|
|
browse {
|
|
releases(artist: "...", first: 10) {
|
|
edges {
|
|
node { title }
|
|
cursor
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Next page
|
|
{
|
|
browse {
|
|
releases(artist: "...", first: 10, after: "Y3Vyc29yOjEw") {
|
|
edges {
|
|
node { title }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Nodes Shortcut
|
|
|
|
Access nodes directly without edges:
|
|
|
|
```graphql
|
|
{
|
|
browse {
|
|
releases(artist: "...", first: 10) {
|
|
nodes {
|
|
title
|
|
date
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Extension Fields
|
|
|
|
### Cover Art Archive
|
|
|
|
Added to `Release` type:
|
|
|
|
```graphql
|
|
type Release {
|
|
coverArtArchive: CoverArtArchiveRelease
|
|
}
|
|
|
|
type CoverArtArchiveRelease {
|
|
front: Boolean
|
|
back: Boolean
|
|
artwork: Boolean
|
|
count: Int
|
|
release: String
|
|
images: [CoverArtArchiveImage]
|
|
}
|
|
|
|
type CoverArtArchiveImage {
|
|
fileID: String
|
|
image: String
|
|
thumbnails: CoverArtArchiveThumbnails
|
|
front: Boolean
|
|
back: Boolean
|
|
types: [String]
|
|
edit: Int
|
|
approved: Boolean
|
|
comment: String
|
|
}
|
|
|
|
type CoverArtArchiveThumbnails {
|
|
small: String
|
|
large: String
|
|
}
|
|
```
|
|
|
|
**Example**:
|
|
```graphql
|
|
{
|
|
lookup {
|
|
release(mbid: "...") {
|
|
title
|
|
coverArtArchive {
|
|
front
|
|
images {
|
|
image
|
|
thumbnails {
|
|
large
|
|
}
|
|
types
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### fanart.tv
|
|
|
|
Added to `Artist` type:
|
|
|
|
```graphql
|
|
type Artist {
|
|
fanArt: FanArtImages
|
|
}
|
|
|
|
type FanArtImages {
|
|
backgrounds: [FanArtImage]
|
|
banners: [FanArtImage]
|
|
logos: [FanArtLabelImage]
|
|
logosHD: [FanArtLabelImage]
|
|
thumbnails: [FanArtImage]
|
|
}
|
|
|
|
type FanArtImage {
|
|
imageID: String
|
|
url: String
|
|
likes: Int
|
|
}
|
|
|
|
type FanArtLabelImage {
|
|
imageID: String
|
|
url: String
|
|
likes: Int
|
|
color: String
|
|
}
|
|
```
|
|
|
|
**Configuration**: Requires `FANART_API_KEY` environment variable.
|
|
|
|
**Example**:
|
|
```graphql
|
|
{
|
|
lookup {
|
|
artist(mbid: "...") {
|
|
name
|
|
fanArt {
|
|
backgrounds {
|
|
url
|
|
likes
|
|
}
|
|
logosHD {
|
|
url
|
|
color
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### MediaWiki
|
|
|
|
Added to `Artist` type:
|
|
|
|
```graphql
|
|
type Artist {
|
|
mediaWikiImages: [MediaWikiImage]
|
|
}
|
|
|
|
type MediaWikiImage {
|
|
url: String
|
|
descriptionURL: String
|
|
title: String
|
|
user: String
|
|
size: Int
|
|
width: Int
|
|
height: Int
|
|
canonicalTitle: String
|
|
objectName: String
|
|
descriptionShortURL: String
|
|
metadata: [MediaWikiImageMetadata]
|
|
}
|
|
|
|
type MediaWikiImageMetadata {
|
|
name: String
|
|
value: String
|
|
}
|
|
```
|
|
|
|
**Example**:
|
|
```graphql
|
|
{
|
|
lookup {
|
|
artist(mbid: "...") {
|
|
name
|
|
mediaWikiImages {
|
|
url
|
|
width
|
|
height
|
|
metadata {
|
|
name
|
|
value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### TheAudioDB
|
|
|
|
Added to `Artist` type:
|
|
|
|
```graphql
|
|
type Artist {
|
|
theAudioDB: TheAudioDBArtist
|
|
}
|
|
|
|
type TheAudioDBArtist {
|
|
artistID: String
|
|
biography: String
|
|
biographyEN: String
|
|
memberCount: Int
|
|
banner: String
|
|
logo: String
|
|
thumbnail: String
|
|
fanArt: [TheAudioDBImage]
|
|
}
|
|
|
|
type TheAudioDBImage {
|
|
url: String
|
|
}
|
|
```
|
|
|
|
**Configuration**: Requires `THEAUDIODB_API_KEY` environment variable.
|
|
|
|
**Example**:
|
|
```graphql
|
|
{
|
|
lookup {
|
|
artist(mbid: "...") {
|
|
name
|
|
theAudioDB {
|
|
biographyEN
|
|
logo
|
|
fanArt {
|
|
url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Scalar Types
|
|
|
|
```graphql
|
|
scalar MBID # MusicBrainz ID (UUID format)
|
|
scalar Date # ISO 8601 date (YYYY-MM-DD)
|
|
scalar Duration # Milliseconds (integer)
|
|
scalar IPI # Interested Parties Information code
|
|
scalar ISNI # International Standard Name Identifier
|
|
scalar ISRC # International Standard Recording Code
|
|
scalar ISWC # International Standard Musical Work Code
|
|
```
|
|
|
|
## Authentication
|
|
|
|
Core GraphBrainz API requires no authentication. Extensions may require API keys:
|
|
|
|
| Extension | Environment Variable | Required |
|
|
|-----------|---------------------|----------|
|
|
| fanart.tv | FANART_API_KEY | Yes |
|
|
| TheAudioDB | THEAUDIODB_API_KEY | Yes |
|
|
| Cover Art Archive | - | No |
|
|
| MediaWiki | - | No |
|
|
|
|
## CORS Configuration
|
|
|
|
Enable CORS via environment variable:
|
|
|
|
```bash
|
|
GRAPHBRAINZ_CORS_ORIGIN="https://example.com"
|
|
# or
|
|
GRAPHBRAINZ_CORS_ORIGIN="*"
|
|
```
|
|
|
|
Default: `false` (CORS disabled)
|
|
|
|
## GraphiQL Interface
|
|
|
|
Interactive GraphQL IDE enabled by default in development mode.
|
|
|
|
**Configuration**:
|
|
```bash
|
|
GRAPHBRAINZ_GRAPHIQL=true # Enable
|
|
GRAPHBRAINZ_GRAPHIQL=false # Disable
|
|
```
|
|
|
|
Access at configured path (default: http://localhost:3000/)
|
|
|
|
## Rate Limits
|
|
|
|
GraphBrainz enforces MusicBrainz API rate limits:
|
|
|
|
- **MusicBrainz**: 5 requests per 5.5 seconds
|
|
- **Extensions**: 10 requests per second (default)
|
|
|
|
Rate limit errors return HTTP 429 with retry-after header.
|
|
|
|
## Error Handling
|
|
|
|
GraphQL errors follow standard format:
|
|
|
|
```json
|
|
{
|
|
"errors": [
|
|
{
|
|
"message": "Artist not found",
|
|
"locations": [{ "line": 2, "column": 3 }],
|
|
"path": ["lookup", "artist"],
|
|
"extensions": {
|
|
"code": "NOT_FOUND",
|
|
"mbid": "invalid-mbid"
|
|
}
|
|
}
|
|
],
|
|
"data": null
|
|
}
|
|
```
|
|
|
|
Error codes:
|
|
|
|
- `NOT_FOUND` - Entity not found
|
|
- `INVALID_MBID` - Invalid MusicBrainz ID format
|
|
- `RATE_LIMIT` - Rate limit exceeded
|
|
- `NETWORK_ERROR` - Upstream API error
|
|
- `VALIDATION_ERROR` - Invalid query parameters
|