Added MusicBrainz API support

pull/3800/merge^2
Anatole Sot 4 months ago
parent 1a1c44e89d
commit dcf990702c

@ -64,6 +64,7 @@
"node-cache": "5.1.2",
"node-gyp": "^8.0.0",
"node-schedule": "2.1.1",
"nodebrainz": "^2.1.1",
"nodemailer": "6.9.1",
"openpgp": "5.7.0",
"plex-api": "5.3.2",

File diff suppressed because one or more lines are too long

@ -0,0 +1,569 @@
import { BaseNodeBrainz } from 'nodebrainz';
import type { mbArtist, mbRecording, mbReleaseGroup, mbRelease, mbWork} from './interfaces';
import {mbArtistType, mbReleaseGroupType, mbWorkType} from './interfaces';
interface SearchOptions {
query: string;
page?: number;
limit?: number;
keywords?: string;
artistname?: string;
albumname?: string;
recordingname?: string;
tag?: string;
}
interface ArtistSearchOptions {
query: string;
tag?: string; // (part of) a tag attached to the artist
limit?: number;
offset?: number;
}
interface RecordingSearchOptions {
query: string;
tag?: string; // (part of) a tag attached to the recording
artistname?: string; // (part of) the name of any of the recording artists
release?: string; // the name of a release that the recording appears on
offset?: number;
limit?: number;
}
interface ReleaseSearchOptions {
query: string;
artistname?: string; // (part of) the name of any of the release artists
tag?: string; // (part of) a tag attached to the release
limit?: number;
offset?: number;
}
interface ReleaseGroupSearchOptions {
query: string;
artistname?: string; // (part of) the name of any of the release group artists
tag?: string; // (part of) a tag attached to the release group
limit?: number;
offset?: number;
}
interface WorkSearchOptions {
query: string;
artist?: string; // (part of) the name of an artist related to the work (e.g. a composer or lyricist)
tag?: string; // (part of) a tag attached to the work
limit?: number;
offset?: number;
}
interface Tag {
name: string;
count: number;
}
interface Area {
"sort-name": string
"type-id": string
"iso-3166-1-codes": string[]
type: string
disambiguation: string
name: string
id: string
}
interface Media {
position: number
"track-count": number
format: string
"format-id": string
title: string
}
interface ReleaseEvent {
area: Area
date: string
}
interface RawArtist {
"sort-name": string
disambiguation: string
id: string
name: string
"type-id": string
type: string
}
interface RawRecording {
length: number
video: boolean
title: string
id: string
disambiguation: string
tags: Tag[]
}
interface RawReleaseGroup {
tags: Tag[],
"primary-type": string
"secondary-types": string[]
disambiguation: string
"first-release-date": string
"secondary-type-ids": string[]
releases: any[]
"primary-type-id": string
id: string
title: string
}
interface RawRelease {
barcode: string
tags: Tag[]
disambiguation: string
packaging: string
"packaging-id": string
"release-events": ReleaseEvent[]
title: string
status: string
"text-representation": {
language: string
script: string
}
"status-id": string
"release-group": any
country: string
quality: string
date: string
id: string
media: Media[]
}
interface RawWork {
disambiguation: string
attributes: any[]
id: string
"type-id": string
languages: string[]
type: string
tags: Tag[]
iswcs: string[]
title: string
language: string
}
function searchOptionstoArtistSearchOptions(options: SearchOptions): ArtistSearchOptions {
const data : ArtistSearchOptions = {
query: options.query
}
if (options.tag) {
data.tag = options.tag;
}
if (options.limit) {
data.limit = options.limit;
}
else {
data.limit = 25;
}
if (options.page) {
data.offset = (options.page-1)*data.limit;
}
return data;
}
function searchOptionstoRecordingSearchOptions(options: SearchOptions): RecordingSearchOptions {
const data : RecordingSearchOptions = {
query: options.query
}
if (options.tag) {
data.tag = options.tag;
}
if (options.artistname) {
data.artistname = options.artistname;
}
if (options.albumname) {
data.release = options.albumname;
}
if (options.limit) {
data.limit = options.limit;
}
else {
data.limit = 25;
}
if (options.page) {
data.offset = (options.page-1)*data.limit;
}
return data;
}
function searchOptionstoReleaseSearchOptions(options: SearchOptions): ReleaseSearchOptions {
const data : ReleaseSearchOptions = {
query: options.query
}
if (options.artistname) {
data.artistname = options.artistname;
}
if (options.tag) {
data.tag = options.tag;
}
if (options.limit) {
data.limit = options.limit;
}
else {
data.limit = 25;
}
if (options.page) {
data.offset = (options.page-1)*data.limit;
}
return data;
}
function searchOptionstoReleaseGroupSearchOptions(options: SearchOptions): ReleaseGroupSearchOptions {
const data : ReleaseGroupSearchOptions = {
query: options.query
}
if (options.artistname) {
data.artistname = options.artistname;
}
if (options.tag) {
data.tag = options.tag;
}
if (options.limit) {
data.limit = options.limit;
}
else {
data.limit = 25;
}
if (options.page) {
data.offset = (options.page-1)*data.limit;
}
return data;
}
function searchOptionstoWorkSearchOptions(options: SearchOptions): WorkSearchOptions {
const data : WorkSearchOptions = {
query: options.query
}
if (options.artistname) {
data.artist = options.artistname;
}
if (options.tag) {
data.tag = options.tag;
}
if (options.limit) {
data.limit = options.limit;
}
else {
data.limit = 25;
}
if (options.page) {
data.offset = (options.page-1)*data.limit;
}
return data;
}
class MusicBrainz extends BaseNodeBrainz {
constructor() {
super({userAgent:'Overseer-with-lidar-support/0.0.1 ( https://github.com/ano0002/overseerr )'});
}
public searchMulti = async (search: SearchOptions) => {
try {
const artistSearch = searchOptionstoArtistSearchOptions(search);
const recordingSearch = searchOptionstoRecordingSearchOptions(search);
const releaseGroupSearch = searchOptionstoReleaseGroupSearchOptions(search);
const releaseSearch = searchOptionstoReleaseSearchOptions(search);
const workSearch = searchOptionstoWorkSearchOptions(search);
const artistResults = await this.searchArtists(artistSearch);
const recordingResults = await this.searchRecordings(recordingSearch);
const releaseGroupResults = await this.searchReleaseGroups(releaseGroupSearch);
const releaseResults = await this.searchReleases(releaseSearch);
const workResults = await this.searchWorks(workSearch);
const combinedResults = {
status: 'ok',
artistResults,
recordingResults,
releaseGroupResults,
releaseResults,
workResults
};
return combinedResults;
} catch (e) {
return {
status: 'error',
artistResults: [],
recordingResults: [],
releaseGroupResults: [],
releaseResults: [],
workResults: []
};
}
};
public searchArtists = async (search: ArtistSearchOptions) => {
try {
const results = await this.search('artist', search);
return results;
} catch (e) {
return [];
}
};
public searchRecordings = async (search: RecordingSearchOptions) => {
try {
const results = await this.search('recording', search);
return results;
} catch (e) {
return [];
}
};
public searchReleaseGroups = async (search: ReleaseGroupSearchOptions) => {
try {
const results = await this.search('release-group', search);
return results;
} catch (e) {
return [];
}
};
public searchReleases = async (search: ReleaseSearchOptions) => {
try {
const results = await this.search('release', search);
return results;
} catch (e) {
return [];
}
};
public searchWorks = async (search: WorkSearchOptions) => {
try {
const results = await this.search('work', search);
return results;
} catch (e) {
return [];
}
};
public getArtist = async (artistId : string): Promise<mbArtist> => {
try {
const rawData = this.artist(artistId, {inc: 'tags+recordings+releases+release-groups+works'});
const artist : mbArtist = {
id: rawData.id,
name: rawData.name,
sortName: rawData["sort-name"],
type: (rawData.type as mbArtistType) || mbArtistType.OTHER,
recordings: rawData.recordings.map((recording: RawRecording): mbRecording => {
return {
id: recording.id,
artist: [{
id: rawData.id,
name: rawData.name,
sortName: rawData["sort-name"],
type: (rawData.type as mbArtistType) || mbArtistType.OTHER,
tags: rawData.tags.map((tag: Tag) => tag.name)
}],
title: recording.title,
length: recording.length,
tags: recording.tags.map((tag: Tag) => tag.name),
}
}),
releases: rawData.releases.map((release: RawRelease): mbRelease => {
return {
id: release.id,
artist: [{
id: rawData.id,
name: rawData.name,
sortName: rawData["sort-name"],
type: (rawData.type as mbArtistType) || mbArtistType.OTHER,
tags: rawData.tags.map((tag: Tag) => tag.name)
}],
title: release.title,
date: new Date(release.date),
tags: release.tags.map((tag: Tag) => tag.name),
}
}),
releaseGroups: rawData["release-groups"].map((releaseGroup: RawReleaseGroup): mbReleaseGroup => {
return {
id: releaseGroup.id,
artist: [{
id: rawData.id,
name: rawData.name,
sortName: rawData["sort-name"],
type: (rawData.type as mbArtistType) || mbArtistType.OTHER,
tags: rawData.tags.map((tag: Tag) => tag.name)
}],
title: releaseGroup.title,
type: (releaseGroup["primary-type"] as mbReleaseGroupType) || mbReleaseGroupType.OTHER,
firstReleased: new Date(releaseGroup["first-release-date"]),
tags: releaseGroup.tags.map((tag: Tag) => tag.name),
}
}),
works: rawData.works.map((work: RawWork): mbWork => {
return {
id: work.id,
title: work.title,
type: (work.type as mbWorkType) || mbWorkType.OTHER,
artist: [{
id: rawData.id,
name: rawData.name,
sortName: rawData["sort-name"],
type: (rawData.type as mbArtistType) || mbArtistType.OTHER,
tags: rawData.tags.map((tag: Tag) => tag.name)
}],
tags: work.tags.map((tag: Tag) => tag.name),
}
}),
tags: rawData.tags.map((tag: Tag) => tag.name),
};
return artist;
} catch (e) {
throw new Error(`[MusicBrainz] Failed to fetch artist details: ${e.message}`);
}
};
public getRecording = async (recordingId : string): Promise<mbRecording> => {
try {
const rawData = this.recording(recordingId, {inc: 'tags+artists+releases'});
const recording : mbRecording = {
id: rawData.id,
title: rawData.title,
artist: rawData["artist-credit"].map((artist: {artist: RawArtist}) => {
return {
id: artist.artist.id,
name: artist.artist.name,
sortName: artist.artist["sort-name"],
type: (artist.artist.type as mbArtistType) || mbArtistType.OTHER
}
}),
length: rawData.length,
firstReleased: new Date(rawData["first-release-date"]),
tags: rawData.tags.map((tag: Tag) => tag.name),
};
return recording;
} catch (e) {
throw new Error(`[MusicBrainz] Failed to fetch recording details: ${e.message}`);
}
};
public async getReleaseGroup(releaseGroupId : string): Promise<mbReleaseGroup> {
try {
const rawData = this.releaseGroup(releaseGroupId, {inc: 'tags+artists+releases'});
const releaseGroup : mbReleaseGroup = {
id: rawData.id,
title: rawData.title,
artist: rawData["artist-credit"].map((artist: {artist: RawArtist}) => {
return {
id: artist.artist.id,
name: artist.artist.name,
sortName: artist.artist["sort-name"],
type: (artist.artist.type as mbArtistType) || mbArtistType.OTHER
}
}),
type: (rawData["primary-type"] as mbReleaseGroupType) || mbReleaseGroupType.OTHER,
firstReleased: new Date(rawData["first-release-date"]),
tags: rawData.tags.map((tag: Tag) => tag.name),
};
return releaseGroup;
} catch (e) {
throw new Error(`[MusicBrainz] Failed to fetch release group details: ${e.message}`);
}
};
public async getRelease(releaseId : string): Promise<mbRelease> {
try {
const rawData = this.release(releaseId, {inc: 'tags+artists+recordings'});
const release : mbRelease = {
id: rawData.id,
title: rawData.title,
artist: rawData["artist-credit"].map((artist: {artist: RawArtist}) => {
return {
id: artist.artist.id,
name: artist.artist.name,
sortName: artist.artist["sort-name"],
type: (artist.artist.type as mbArtistType) || mbArtistType.OTHER
}
}),
date: new Date(rawData["release-events"][0].date),
tracks: rawData.media.map((media: {
"track-count": number
title: string
format: string
position: number
"track-offset": number
tracks: {
title: string
position: number
id: string
length: number
recording: {
disambiguation: string
"first-release-date": string
title: string
id: string
length: number
tags: Tag[]
video: boolean
}
number: string
}[];
"format-id": string
}) => {
return media.tracks.map((track: {
title: string
position: number
id: string
length: number
recording: {
disambiguation: string
"first-release-date": string
title: string
id: string
length: number
tags: Tag[]
video: boolean
}
number: string
}) => {
return {
id: track.id,
title: track.title,
length: track.recording.length,
tags: track.recording.tags.map((tag: Tag) => tag.name),
}
})
}).flat(),
tags: rawData.tags.map((tag: Tag) => tag.name),
};
return release;
} catch (e) {
throw new Error(`[MusicBrainz] Failed to fetch release details: ${e.message}`);
}
};
public async getWork(workId : string): Promise<mbWork> {
try {
const rawData = this.work(workId, {inc: 'tags+artist-rels'});
const work : mbWork = {
id: rawData.id,
title: rawData.title,
type: (rawData.type as mbWorkType) || mbWorkType.OTHER,
artist: rawData.relations.map((relation: {artist: RawArtist}) => {
return {
id: relation.artist.id,
name: relation.artist.name,
sortName: relation.artist["sort-name"],
type: (relation.artist.type as mbArtistType) || mbArtistType.OTHER
}
}),
tags: rawData.tags.map((tag: Tag) => tag.name),
};
return work;
} catch (e) {
throw new Error(`[MusicBrainz] Failed to fetch work details: ${e.message}`);
}
};
}
export default MusicBrainz;

@ -0,0 +1,105 @@
// Purpose: Interfaces for MusicBrainz data.
export enum mbArtistType {
PERSON = 'Person',
GROUP = 'Group',
ORCHESTRA = 'Orchestra',
CHOIR = 'Choir',
CHARACTER = 'Character',
OTHER = 'Other',
};
export interface mbArtist {
id: string;
name: string;
sortName: string;
type: mbArtistType;
recordings?: mbRecording[];
releases?: mbRelease[];
releaseGroups?: mbReleaseGroup[];
works?: mbWork[];
gender?: string;
area?: string;
beginDate?: string;
endDate?: string;
tags: string[];
};
export interface mbRecording {
id: string;
title: string;
artist: mbArtist[];
length: number;
firstReleased?: Date;
tags: string[];
};
export interface mbRelease {
id: string;
title: string;
artist: mbArtist[];
date?: Date;
tracks?: mbRecording[];
tags: string[];
};
export enum mbReleaseGroupType {
ALBUM = 'Album',
SINGLE = 'Single',
EP = 'EP',
BROADCAST = 'Broadcast',
OTHER = 'Other',
};
export interface mbReleaseGroup {
id: string;
title: string;
artist: mbArtist[];
type: mbReleaseGroupType;
firstReleased?: Date;
releases?: mbRelease[];
tags: string[];
};
export enum mbWorkType {
ARIA = 'Aria',
BALLET = 'Ballet',
CANTATA = 'Cantata',
CONCERTO = 'Concerto',
SONATA = 'Sonata',
SUITE = 'Suite',
MADRIGAL = 'Madrigal',
MASS = 'Mass',
MOTET = 'Motet',
OPERA = 'Opera',
ORATORIO = 'Oratorio',
OVERTURE = 'Overture',
PARTITA = 'Partita',
QUARTET = 'Quartet',
SONG_CYCLE = 'Song-cycle',
SYMPHONY = 'Symphony',
SONG = 'Song',
SYMPHONIC_POEM = 'Symphonic poem',
ZARZUELA = 'Zarzuela',
ETUDE = 'Étude',
POEM = 'Poem',
SOUNDTRACK = 'Soundtrack',
PROSE = 'Prose',
OPERETTA = 'Operetta',
AUDIO_DRAMA = 'Audio drama',
BEIJING_OPERA = 'Beijing opera',
PLAY = 'Play',
MUSICAL = 'Musical',
INCIDENTAL_MUSIC = 'Incidental music',
OTHER = 'Other',
};
export interface mbWork {
id: string;
title: string;
type: mbWorkType;
artist: mbArtist[];
tags: string[];
};
Loading…
Cancel
Save