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
885 lines
17 KiB
Markdown
885 lines
17 KiB
Markdown
# GraphBrainz Integrations
|
|
|
|
## Integration Architecture
|
|
|
|
GraphBrainz integrates with 5 external APIs through a unified extension system:
|
|
|
|
| Integration | Type | Authentication | Rate Limit |
|
|
|-------------|------|----------------|------------|
|
|
| MusicBrainz | Core | None | 5 req/5.5s |
|
|
| Cover Art Archive | Built-in | None | 10 req/s |
|
|
| fanart.tv | Built-in | API key | 10 req/s |
|
|
| MediaWiki | Built-in | None | 10 req/s |
|
|
| TheAudioDB | Built-in | API key | 10 req/s |
|
|
|
|
External extensions (separate npm packages):
|
|
|
|
| Extension | Package | Authentication |
|
|
|-----------|---------|----------------|
|
|
| Last.fm | graphbrainz-extension-lastfm | API key |
|
|
| Discogs | graphbrainz-extension-discogs | API key |
|
|
| Spotify | graphbrainz-extension-spotify | OAuth |
|
|
|
|
## MusicBrainz REST API
|
|
|
|
### Overview
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Base URL | http://musicbrainz.org/ws/2/ |
|
|
| Protocol | REST (JSON) |
|
|
| Authentication | None |
|
|
| Rate Limit | 5 requests per 5.5 seconds |
|
|
| Documentation | https://musicbrainz.org/doc/MusicBrainz_API |
|
|
|
|
### Operations
|
|
|
|
#### Lookup
|
|
|
|
Retrieve single entity by MBID.
|
|
|
|
**Endpoint Pattern**:
|
|
```
|
|
GET /ws/2/{entity}/{mbid}?inc={relationships}&fmt=json
|
|
```
|
|
|
|
**Supported Entities**:
|
|
- area, artist, collection, event, instrument, label, place, recording, release, release-group, series, url, work
|
|
|
|
**Example**:
|
|
```
|
|
GET /ws/2/artist/5b11f4ce-a62d-471e-81fc-a69a8278c7da?inc=releases+recordings&fmt=json
|
|
```
|
|
|
|
#### Browse
|
|
|
|
Retrieve entities linked to parent entity.
|
|
|
|
**Endpoint Pattern**:
|
|
```
|
|
GET /ws/2/{entity}?{parent-entity}={mbid}&limit={limit}&offset={offset}&inc={relationships}&fmt=json
|
|
```
|
|
|
|
**Example**:
|
|
```
|
|
GET /ws/2/release?artist=5b11f4ce-a62d-471e-81fc-a69a8278c7da&limit=25&offset=0&fmt=json
|
|
```
|
|
|
|
#### Search
|
|
|
|
Lucene-based full-text search.
|
|
|
|
**Endpoint Pattern**:
|
|
```
|
|
GET /ws/2/{entity}?query={lucene-query}&limit={limit}&offset={offset}&fmt=json
|
|
```
|
|
|
|
**Example**:
|
|
```
|
|
GET /ws/2/artist?query=artist:Radiohead%20AND%20country:GB&limit=25&fmt=json
|
|
```
|
|
|
|
### Rate Limiting
|
|
|
|
**Policy**: 5 requests per 5.5 seconds (0.909 req/s average)
|
|
|
|
**Implementation**:
|
|
```javascript
|
|
const musicbrainzLimiter = new RateLimiter({
|
|
limit: 5,
|
|
interval: 5500,
|
|
concurrency: 1
|
|
});
|
|
```
|
|
|
|
**Compliance Strategy**:
|
|
- Token bucket algorithm
|
|
- Sequential requests (no parallelization)
|
|
- Priority queue for request ordering
|
|
|
|
### Local Mirror Support
|
|
|
|
GraphBrainz supports local MusicBrainz mirrors to eliminate rate limits:
|
|
|
|
```bash
|
|
MUSICBRAINZ_BASE_URL=http://localhost:5000/ws/2/
|
|
```
|
|
|
|
**Benefits**:
|
|
- No rate limiting
|
|
- Reduced latency
|
|
- Offline operation
|
|
- Full dataset access
|
|
|
|
**Setup**: See https://musicbrainz.org/doc/MusicBrainz_Server/Setup
|
|
|
|
## Cover Art Archive
|
|
|
|
### Overview
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Base URL | http://coverartarchive.org/ |
|
|
| Protocol | REST (JSON) |
|
|
| Authentication | None |
|
|
| Rate Limit | 10 requests per second |
|
|
| Documentation | https://musicbrainz.org/doc/Cover_Art_Archive/API |
|
|
|
|
### Purpose
|
|
|
|
Provides album artwork and thumbnails for MusicBrainz releases.
|
|
|
|
### Schema Extension
|
|
|
|
Adds `coverArtArchive` field to `Release` type:
|
|
|
|
```graphql
|
|
extend 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 # 250px
|
|
large: String # 500px
|
|
}
|
|
```
|
|
|
|
### API Endpoints
|
|
|
|
#### Release Cover Art
|
|
|
|
**Endpoint**:
|
|
```
|
|
GET /release/{mbid}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"images": [
|
|
{
|
|
"id": "12345",
|
|
"image": "http://coverartarchive.org/release/{mbid}/12345.jpg",
|
|
"thumbnails": {
|
|
"small": "http://coverartarchive.org/release/{mbid}/12345-250.jpg",
|
|
"large": "http://coverartarchive.org/release/{mbid}/12345-500.jpg"
|
|
},
|
|
"front": true,
|
|
"back": false,
|
|
"types": ["Front"],
|
|
"approved": true
|
|
}
|
|
],
|
|
"release": "http://musicbrainz.org/release/{mbid}"
|
|
}
|
|
```
|
|
|
|
#### Front Cover (Direct)
|
|
|
|
**Endpoint**:
|
|
```
|
|
GET /release/{mbid}/front
|
|
GET /release/{mbid}/front-250 # Small thumbnail
|
|
GET /release/{mbid}/front-500 # Large thumbnail
|
|
```
|
|
|
|
Returns image binary (JPEG/PNG).
|
|
|
|
### Configuration
|
|
|
|
| Environment Variable | Default | Purpose |
|
|
|---------------------|---------|---------|
|
|
| COVERART_CACHE_SIZE | 8192 | LRU cache size |
|
|
| COVERART_CACHE_TTL | 86400000 | Cache TTL (1 day) |
|
|
|
|
### Example Query
|
|
|
|
```graphql
|
|
{
|
|
lookup {
|
|
release(mbid: "f0c8b1e5-c3b6-46c0-9641-25fd3c00e56a") {
|
|
title
|
|
coverArtArchive {
|
|
front
|
|
back
|
|
count
|
|
images {
|
|
image
|
|
thumbnails {
|
|
large
|
|
}
|
|
types
|
|
front
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Implementation
|
|
|
|
**File**: `src/extensions/cover-art-archive/index.js`
|
|
|
|
**Client**: Custom HTTP client extending base `Client` class
|
|
|
|
**Resolver**:
|
|
```javascript
|
|
Release: {
|
|
coverArtArchive(release, args, context) {
|
|
return context.coverArtArchive.loader.load(release.id);
|
|
}
|
|
}
|
|
```
|
|
|
|
## fanart.tv
|
|
|
|
### Overview
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Base URL | http://webservice.fanart.tv/v3/ |
|
|
| Protocol | REST (JSON) |
|
|
| Authentication | API key (required) |
|
|
| Rate Limit | 10 requests per second |
|
|
| Documentation | https://fanart.tv/api-docs/ |
|
|
|
|
### Purpose
|
|
|
|
Provides high-quality artist images: backgrounds, banners, logos, thumbnails.
|
|
|
|
### Schema Extension
|
|
|
|
Adds `fanArt` field to `Artist` type:
|
|
|
|
```graphql
|
|
extend 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
|
|
}
|
|
```
|
|
|
|
### API Endpoints
|
|
|
|
#### Artist Images
|
|
|
|
**Endpoint**:
|
|
```
|
|
GET /music/{mbid}?api_key={key}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"name": "Radiohead",
|
|
"mbid_id": "5b11f4ce-a62d-471e-81fc-a69a8278c7da",
|
|
"artistbackground": [
|
|
{
|
|
"id": "12345",
|
|
"url": "https://assets.fanart.tv/fanart/music/5b11f4ce.../artistbackground/...",
|
|
"likes": "42"
|
|
}
|
|
],
|
|
"hdmusiclogo": [
|
|
{
|
|
"id": "67890",
|
|
"url": "https://assets.fanart.tv/fanart/music/5b11f4ce.../hdmusiclogo/...",
|
|
"likes": "128",
|
|
"colour": "FFFFFF"
|
|
}
|
|
],
|
|
"artistthumb": [...],
|
|
"musicbanner": [...]
|
|
}
|
|
```
|
|
|
|
### Configuration
|
|
|
|
| Environment Variable | Required | Default | Purpose |
|
|
|---------------------|----------|---------|---------|
|
|
| FANART_API_KEY | Yes | - | API authentication |
|
|
| FANART_CACHE_SIZE | No | 8192 | LRU cache size |
|
|
| FANART_CACHE_TTL | No | 86400000 | Cache TTL (1 day) |
|
|
|
|
### Example Query
|
|
|
|
```graphql
|
|
{
|
|
lookup {
|
|
artist(mbid: "5b11f4ce-a62d-471e-81fc-a69a8278c7da") {
|
|
name
|
|
fanArt {
|
|
backgrounds {
|
|
url
|
|
likes
|
|
}
|
|
logosHD {
|
|
url
|
|
color
|
|
likes
|
|
}
|
|
banners {
|
|
url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Implementation
|
|
|
|
**File**: `src/extensions/fanart/index.js`
|
|
|
|
**Client**: `FanArtClient` extending base `Client`
|
|
|
|
**Resolver**:
|
|
```javascript
|
|
Artist: {
|
|
fanArt(artist, args, context) {
|
|
return context.fanart.loader.load(artist.id);
|
|
}
|
|
}
|
|
```
|
|
|
|
## MediaWiki
|
|
|
|
### Overview
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Base URL | https://musicbrainz.org/w/api.php |
|
|
| Protocol | MediaWiki API |
|
|
| Authentication | None |
|
|
| Rate Limit | 10 requests per second |
|
|
| Documentation | https://www.mediawiki.org/wiki/API |
|
|
|
|
### Purpose
|
|
|
|
Retrieves images from MusicBrainz Wiki for artists, including EXIF metadata and license information.
|
|
|
|
### Schema Extension
|
|
|
|
Adds `mediaWikiImages` field to `Artist` type:
|
|
|
|
```graphql
|
|
extend 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
|
|
}
|
|
```
|
|
|
|
### API Endpoints
|
|
|
|
#### Image Search
|
|
|
|
**Endpoint**:
|
|
```
|
|
GET /w/api.php?action=query&titles={artist-name}&prop=images&format=json
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"query": {
|
|
"pages": {
|
|
"12345": {
|
|
"title": "Radiohead",
|
|
"images": [
|
|
{
|
|
"title": "File:Radiohead.jpg"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Image Info
|
|
|
|
**Endpoint**:
|
|
```
|
|
GET /w/api.php?action=query&titles=File:{filename}&prop=imageinfo&iiprop=url|size|metadata|user&format=json
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"query": {
|
|
"pages": {
|
|
"67890": {
|
|
"imageinfo": [
|
|
{
|
|
"url": "https://musicbrainz.org/w/images/...",
|
|
"descriptionurl": "https://musicbrainz.org/w/File:...",
|
|
"width": 1200,
|
|
"height": 800,
|
|
"size": 245678,
|
|
"user": "WikiUser",
|
|
"metadata": [
|
|
{ "name": "DateTime", "value": "2020:01:15 10:30:00" },
|
|
{ "name": "Artist", "value": "Photographer Name" }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Configuration
|
|
|
|
| Environment Variable | Default | Purpose |
|
|
|---------------------|---------|---------|
|
|
| MEDIAWIKI_CACHE_SIZE | 8192 | LRU cache size |
|
|
| MEDIAWIKI_CACHE_TTL | 86400000 | Cache TTL (1 day) |
|
|
|
|
### Example Query
|
|
|
|
```graphql
|
|
{
|
|
lookup {
|
|
artist(mbid: "5b11f4ce-a62d-471e-81fc-a69a8278c7da") {
|
|
name
|
|
mediaWikiImages {
|
|
url
|
|
width
|
|
height
|
|
user
|
|
metadata {
|
|
name
|
|
value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Implementation
|
|
|
|
**File**: `src/extensions/mediawiki/index.js`
|
|
|
|
**Client**: `MediaWikiClient` extending base `Client`
|
|
|
|
**Resolver**:
|
|
```javascript
|
|
Artist: {
|
|
mediaWikiImages(artist, args, context) {
|
|
return context.mediawiki.loader.load(artist.name);
|
|
}
|
|
}
|
|
```
|
|
|
|
## TheAudioDB
|
|
|
|
### Overview
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Base URL | http://www.theaudiodb.com/api/v1/json/ |
|
|
| Protocol | REST (JSON) |
|
|
| Authentication | API key (required) |
|
|
| Rate Limit | 10 requests per second |
|
|
| Documentation | https://www.theaudiodb.com/api_guide.php |
|
|
|
|
### Purpose
|
|
|
|
Provides artist biographies, logos, and additional metadata.
|
|
|
|
### Schema Extension
|
|
|
|
Adds `theAudioDB` field to `Artist` type:
|
|
|
|
```graphql
|
|
extend 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
|
|
}
|
|
```
|
|
|
|
### API Endpoints
|
|
|
|
#### Artist by MBID
|
|
|
|
**Endpoint**:
|
|
```
|
|
GET /{api-key}/artist-mb.php?i={mbid}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"artists": [
|
|
{
|
|
"idArtist": "111239",
|
|
"strArtist": "Radiohead",
|
|
"strArtistMBID": "5b11f4ce-a62d-471e-81fc-a69a8278c7da",
|
|
"strBiographyEN": "Radiohead are an English rock band...",
|
|
"intMembers": "5",
|
|
"strArtistBanner": "https://www.theaudiodb.com/images/media/artist/banner/...",
|
|
"strArtistLogo": "https://www.theaudiodb.com/images/media/artist/logo/...",
|
|
"strArtistThumb": "https://www.theaudiodb.com/images/media/artist/thumb/...",
|
|
"strArtistFanart": "https://www.theaudiodb.com/images/media/artist/fanart/...",
|
|
"strArtistFanart2": "https://www.theaudiodb.com/images/media/artist/fanart2/...",
|
|
"strArtistFanart3": "https://www.theaudiodb.com/images/media/artist/fanart3/..."
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Configuration
|
|
|
|
| Environment Variable | Required | Default | Purpose |
|
|
|---------------------|----------|---------|---------|
|
|
| THEAUDIODB_API_KEY | Yes | - | API authentication |
|
|
| THEAUDIODB_CACHE_SIZE | No | 8192 | LRU cache size |
|
|
| THEAUDIODB_CACHE_TTL | No | 86400000 | Cache TTL (1 day) |
|
|
|
|
### Example Query
|
|
|
|
```graphql
|
|
{
|
|
lookup {
|
|
artist(mbid: "5b11f4ce-a62d-471e-81fc-a69a8278c7da") {
|
|
name
|
|
theAudioDB {
|
|
biographyEN
|
|
memberCount
|
|
logo
|
|
banner
|
|
fanArt {
|
|
url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Implementation
|
|
|
|
**File**: `src/extensions/theaudiodb/index.js`
|
|
|
|
**Client**: `TheAudioDBClient` extending base `Client`
|
|
|
|
**Resolver**:
|
|
```javascript
|
|
Artist: {
|
|
theAudioDB(artist, args, context) {
|
|
return context.theaudiodb.loader.load(artist.id);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Extension Pattern
|
|
|
|
All extensions follow a consistent pattern for integration.
|
|
|
|
### Extension Interface
|
|
|
|
```javascript
|
|
{
|
|
name: String, // Extension identifier
|
|
description: String, // Human-readable description
|
|
extendContext: Function, // Add HTTP client, DataLoader, cache to context
|
|
extendSchema: Function // Add GraphQL types and resolvers
|
|
}
|
|
```
|
|
|
|
### Context Extension
|
|
|
|
```javascript
|
|
extendContext(context, options) {
|
|
const client = new ExtensionClient({
|
|
baseURL: options.baseURL,
|
|
apiKey: options.apiKey,
|
|
timeout: options.timeout
|
|
});
|
|
|
|
const cache = new LRU({
|
|
max: options.cacheSize || 8192,
|
|
ttl: options.cacheTTL || 86400000
|
|
});
|
|
|
|
const loader = new DataLoader(
|
|
keys => batchFetch(client, keys),
|
|
{ cache: false } // Use LRU cache instead
|
|
);
|
|
|
|
return {
|
|
...context,
|
|
[extensionName]: {
|
|
client,
|
|
loader,
|
|
cache
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
### Schema Extension
|
|
|
|
```javascript
|
|
extendSchema(schema, options) {
|
|
const typeDefs = `
|
|
extend type Artist {
|
|
extensionField: ExtensionType
|
|
}
|
|
|
|
type ExtensionType {
|
|
field1: String
|
|
field2: Int
|
|
}
|
|
`;
|
|
|
|
const resolvers = {
|
|
Artist: {
|
|
extensionField(artist, args, context) {
|
|
return context.extensionName.loader.load(artist.id);
|
|
}
|
|
}
|
|
};
|
|
|
|
return extendSchema(schema, { typeDefs, resolvers });
|
|
}
|
|
```
|
|
|
|
### Client Base Class
|
|
|
|
All extension clients extend a base `Client` class:
|
|
|
|
**File**: `src/client.js`
|
|
|
|
```javascript
|
|
class Client {
|
|
constructor(options) {
|
|
this.client = got.extend({
|
|
prefixUrl: options.baseURL,
|
|
headers: options.headers,
|
|
timeout: options.timeout || 30000,
|
|
retry: { limit: 3 },
|
|
hooks: {
|
|
beforeRequest: [this.beforeRequest.bind(this)],
|
|
afterResponse: [this.afterResponse.bind(this)]
|
|
}
|
|
});
|
|
|
|
this.cache = options.cache;
|
|
this.limiter = options.limiter;
|
|
}
|
|
|
|
async get(path, options) {
|
|
const cacheKey = this.getCacheKey(path, options);
|
|
const cached = this.cache.get(cacheKey);
|
|
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
await this.limiter.acquire();
|
|
|
|
const response = await this.client.get(path, options);
|
|
const data = response.body;
|
|
|
|
this.cache.set(cacheKey, data);
|
|
|
|
return data;
|
|
}
|
|
|
|
getCacheKey(path, options) {
|
|
return `${path}:${JSON.stringify(options)}`;
|
|
}
|
|
|
|
beforeRequest(options) {
|
|
debug(`${this.constructor.name}`)(`${options.method} ${options.url}`);
|
|
}
|
|
|
|
afterResponse(response) {
|
|
return response;
|
|
}
|
|
}
|
|
```
|
|
|
|
## External Extensions
|
|
|
|
### Last.fm
|
|
|
|
**Package**: `graphbrainz-extension-lastfm`
|
|
|
|
**Installation**:
|
|
```bash
|
|
npm install graphbrainz-extension-lastfm
|
|
```
|
|
|
|
**Configuration**:
|
|
```bash
|
|
LASTFM_API_KEY=your-api-key
|
|
```
|
|
|
|
**Schema Additions**:
|
|
- `Artist.lastFM` - Scrobble statistics, similar artists
|
|
- `Recording.lastFM` - Play counts, listener counts
|
|
|
|
### Discogs
|
|
|
|
**Package**: `graphbrainz-extension-discogs`
|
|
|
|
**Installation**:
|
|
```bash
|
|
npm install graphbrainz-extension-discogs
|
|
```
|
|
|
|
**Configuration**:
|
|
```bash
|
|
DISCOGS_API_KEY=your-api-key
|
|
```
|
|
|
|
**Schema Additions**:
|
|
- `Release.discogs` - Marketplace data, pricing, community ratings
|
|
|
|
### Spotify
|
|
|
|
**Package**: `graphbrainz-extension-spotify`
|
|
|
|
**Installation**:
|
|
```bash
|
|
npm install graphbrainz-extension-spotify
|
|
```
|
|
|
|
**Configuration**:
|
|
```bash
|
|
SPOTIFY_CLIENT_ID=your-client-id
|
|
SPOTIFY_CLIENT_SECRET=your-client-secret
|
|
```
|
|
|
|
**Schema Additions**:
|
|
- `Artist.spotify` - Popularity, followers, genres
|
|
- `Recording.spotify` - Audio features, preview URLs
|
|
|
|
## Integration Best Practices
|
|
|
|
### Error Handling
|
|
|
|
Each extension implements custom error classes:
|
|
|
|
```javascript
|
|
class FanArtError extends Error {
|
|
constructor(message, statusCode) {
|
|
super(message);
|
|
this.name = 'FanArtError';
|
|
this.statusCode = statusCode;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Graceful Degradation
|
|
|
|
Extension failures don't break core queries:
|
|
|
|
```graphql
|
|
{
|
|
lookup {
|
|
artist(mbid: "...") {
|
|
name # Always works (core)
|
|
fanArt { # Returns null if fanart.tv fails
|
|
backgrounds
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Rate Limit Coordination
|
|
|
|
Each extension has independent rate limiter to prevent cross-contamination:
|
|
|
|
```javascript
|
|
const fanartLimiter = new RateLimiter({ limit: 10, interval: 1000 });
|
|
const theaudiodbLimiter = new RateLimiter({ limit: 10, interval: 1000 });
|
|
```
|
|
|
|
### Cache Isolation
|
|
|
|
Separate caches prevent eviction conflicts:
|
|
|
|
```javascript
|
|
const fanartCache = new LRU({ max: 8192 });
|
|
const theaudiodbCache = new LRU({ max: 8192 });
|
|
```
|