Files
channels-seerr/server/api/musicbrainz/index.ts
2025-12-15 09:36:22 +10:00

193 lines
4.3 KiB
TypeScript

import ExternalAPI from '@server/api/externalapi';
import cacheManager from '@server/lib/cache';
import axios from 'axios';
import DOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
import type { MbAlbumDetails, MbArtistDetails } from './interfaces';
const window = new JSDOM('').window;
const purify = DOMPurify(window);
class MusicBrainz extends ExternalAPI {
constructor() {
super(
'https://musicbrainz.org/ws/2',
{},
{
headers: {
'User-Agent':
'Jellyseerr/1.0.0 (https://github.com/Fallenbagel/jellyseerr)',
Accept: 'application/json',
},
nodeCache: cacheManager.getCache('musicbrainz').data,
rateLimit: {
maxRequests: 1,
maxRPS: 1,
},
}
);
}
public async searchAlbum({
query,
limit = 30,
offset = 0,
}: {
query: string;
limit?: number;
offset?: number;
}): Promise<MbAlbumDetails[]> {
try {
const data = await this.get<{
created: string;
count: number;
offset: number;
'release-groups': MbAlbumDetails[];
}>(
'/release-group',
{
params: {
query,
fmt: 'json',
limit: limit.toString(),
offset: offset.toString(),
},
},
43200
);
return data['release-groups'];
} catch (e) {
throw new Error(
`[MusicBrainz] Failed to search albums: ${
e instanceof Error ? e.message : 'Unknown error'
}`
);
}
}
public async searchArtist({
query,
limit = 50,
offset = 0,
}: {
query: string;
limit?: number;
offset?: number;
}): Promise<MbArtistDetails[]> {
try {
const data = await this.get<{
created: string;
count: number;
offset: number;
artists: MbArtistDetails[];
}>(
'/artist',
{
params: {
query,
fmt: 'json',
limit: limit.toString(),
offset: offset.toString(),
},
},
43200
);
return data.artists;
} catch (e) {
throw new Error(
`[MusicBrainz] Failed to search artists: ${
e instanceof Error ? e.message : 'Unknown error'
}`
);
}
}
public async getArtistWikipediaExtract({
artistMbid,
language = 'en',
}: {
artistMbid: string;
language?: string;
}): Promise<{ title: string; url: string; content: string } | null> {
if (
!artistMbid ||
typeof artistMbid !== 'string' ||
!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(
artistMbid
)
) {
throw new Error('Invalid MusicBrainz artist ID format');
}
try {
const safeUrl = `https://musicbrainz.org/artist/${artistMbid}/wikipedia-extract`;
const response = await axios.get(safeUrl, {
headers: {
Accept: 'application/json',
'Accept-Language': language,
'User-Agent':
'Jellyseerr/1.0.0 (https://github.com/Fallenbagel/jellyseerr)',
},
});
const data = response.data;
if (!data.wikipediaExtract || !data.wikipediaExtract.content) {
return null;
}
const cleanContent = purify.sanitize(data.wikipediaExtract.content, {
ALLOWED_TAGS: [],
ALLOWED_ATTR: [],
});
return {
title: data.wikipediaExtract.title,
url: data.wikipediaExtract.url,
content: cleanContent.trim(),
};
} catch (error) {
throw new Error(
`[MusicBrainz] Failed to fetch Wikipedia extract: ${
error instanceof Error ? error.message : 'Unknown error'
}`
);
}
}
public async getReleaseGroup({
releaseId,
}: {
releaseId: string;
}): Promise<string | null> {
try {
const data = await this.get<{
'release-group': {
id: string;
};
}>(
`/release/${releaseId}`,
{
params: {
inc: 'release-groups',
fmt: 'json',
},
},
43200
);
return data['release-group']?.id ?? null;
} catch (e) {
throw new Error(
`[MusicBrainz] Failed to fetch release group: ${
e instanceof Error ? e.message : 'Unknown error'
}`
);
}
}
}
export default MusicBrainz;