parent
666cf3949c
commit
ad3707e065
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[];
|
||||
};
|
@ -0,0 +1,216 @@
|
||||
import logger from '@server/logger';
|
||||
import ServarrBase from './base';
|
||||
|
||||
export interface LidarrMusicOptions {
|
||||
title: string;
|
||||
qualityProfileId: number;
|
||||
tags: number[];
|
||||
profileId: number;
|
||||
year: number;
|
||||
rootFolderPath: string;
|
||||
mbId: number;
|
||||
monitored?: boolean;
|
||||
searchNow?: boolean;
|
||||
}
|
||||
|
||||
export interface LidarrMusic {
|
||||
id: number;
|
||||
title: string;
|
||||
isAvailable: boolean;
|
||||
monitored: boolean;
|
||||
mbId: number;
|
||||
imdbId: string;
|
||||
titleSlug: string;
|
||||
folderName: string;
|
||||
path: string;
|
||||
profileId: number;
|
||||
qualityProfileId: number;
|
||||
added: string;
|
||||
hasFile: boolean;
|
||||
}
|
||||
|
||||
|
||||
class LidarrAPI extends ServarrBase<{ musicId: number }> {
|
||||
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
||||
super({ url, apiKey, cacheName: 'lidarr', apiName: 'Lidarr' });
|
||||
}
|
||||
|
||||
public getMusics = async (): Promise<LidarrMusic[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<LidarrMusic[]>('/music');
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Lidarr] Failed to retrieve musics: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getMusic = async ({ id }: { id: number }): Promise<LidarrMusic> => {
|
||||
try {
|
||||
const response = await this.axios.get<LidarrMusic>(`/music/${id}`);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Lidarr] Failed to retrieve music: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public async getMusicBymbId(id: number): Promise<LidarrMusic> {
|
||||
try {
|
||||
const response = await this.axios.get<LidarrMusic[]>('/music/lookup', {
|
||||
params: {
|
||||
term: `musicbrainz:${id}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.data[0]) {
|
||||
throw new Error('Music not found');
|
||||
}
|
||||
|
||||
return response.data[0];
|
||||
} catch (e) {
|
||||
logger.error('Error retrieving music by MUSICBRAINZ ID', {
|
||||
label: 'Lidarr API',
|
||||
errorMessage: e.message,
|
||||
mbId: id,
|
||||
});
|
||||
throw new Error('Music not found');
|
||||
}
|
||||
}
|
||||
|
||||
public addMusic = async (
|
||||
options: LidarrMusicOptions
|
||||
): Promise<LidarrMusic> => {
|
||||
try {
|
||||
const music = await this.getMusicBymbId(options.mbId);
|
||||
|
||||
if (music.hasFile) {
|
||||
logger.info(
|
||||
'Title already exists and is available. Skipping add and returning success',
|
||||
{
|
||||
label: 'Lidarr',
|
||||
music,
|
||||
}
|
||||
);
|
||||
return music;
|
||||
}
|
||||
|
||||
// music exists in Lidarr but is neither downloaded nor monitored
|
||||
if (music.id && !music.monitored) {
|
||||
const response = await this.axios.put<LidarrMusic>(`/music`, {
|
||||
...music,
|
||||
title: options.title,
|
||||
qualityProfileId: options.qualityProfileId,
|
||||
profileId: options.profileId,
|
||||
titleSlug: options.mbId.toString(),
|
||||
mbId: options.mbId,
|
||||
year: options.year,
|
||||
tags: options.tags,
|
||||
rootFolderPath: options.rootFolderPath,
|
||||
monitored: options.monitored,
|
||||
addOptions: {
|
||||
searchForMusic: options.searchNow,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data.monitored) {
|
||||
logger.info(
|
||||
'Found existing title in Lidarr and set it to monitored.',
|
||||
{
|
||||
label: 'Lidarr',
|
||||
musicId: response.data.id,
|
||||
musicTitle: response.data.title,
|
||||
}
|
||||
);
|
||||
logger.debug('Lidarr update details', {
|
||||
label: 'Lidarr',
|
||||
music: response.data,
|
||||
});
|
||||
|
||||
if (options.searchNow) {
|
||||
this.searchMusic(response.data.id);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} else {
|
||||
logger.error('Failed to update existing music in Lidarr.', {
|
||||
label: 'Lidarr',
|
||||
options,
|
||||
});
|
||||
throw new Error('Failed to update existing music in Lidarr');
|
||||
}
|
||||
}
|
||||
|
||||
if (music.id) {
|
||||
logger.info(
|
||||
'Music is already monitored in Lidarr. Skipping add and returning success',
|
||||
{ label: 'Lidarr' }
|
||||
);
|
||||
return music;
|
||||
}
|
||||
|
||||
const response = await this.axios.post<LidarrMusic>(`/music`, {
|
||||
title: options.title,
|
||||
qualityProfileId: options.qualityProfileId,
|
||||
profileId: options.profileId,
|
||||
titleSlug: options.mbId.toString(),
|
||||
mbId: options.mbId,
|
||||
year: options.year,
|
||||
rootFolderPath: options.rootFolderPath,
|
||||
monitored: options.monitored,
|
||||
tags: options.tags,
|
||||
addOptions: {
|
||||
searchForMusic: options.searchNow,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data.id) {
|
||||
logger.info('Lidarr accepted request', { label: 'Lidarr' });
|
||||
logger.debug('Lidarr add details', {
|
||||
label: 'Lidarr',
|
||||
music: response.data,
|
||||
});
|
||||
} else {
|
||||
logger.error('Failed to add music to Lidarr', {
|
||||
label: 'Lidarr',
|
||||
options,
|
||||
});
|
||||
throw new Error('Failed to add music to Lidarr');
|
||||
}
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Failed to add music to Lidarr. This might happen if the music already exists, in which case you can safely ignore this error.',
|
||||
{
|
||||
label: 'Lidarr',
|
||||
errorMessage: e.message,
|
||||
options,
|
||||
response: e?.response?.data,
|
||||
}
|
||||
);
|
||||
throw new Error('Failed to add music to Lidarr');
|
||||
}
|
||||
};
|
||||
|
||||
public async searchMusic(musicId: number): Promise<void> {
|
||||
logger.info('Executing music search command', {
|
||||
label: 'Lidarr API',
|
||||
musicId,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.runCommand('MusicsSearch', { musicIds: [musicId] });
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong while executing Lidarr music search.',
|
||||
{
|
||||
label: 'Lidarr API',
|
||||
errorMessage: e.message,
|
||||
musicId,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default LidarrAPI;
|
@ -0,0 +1 @@
|
||||
declare module 'nodebrainz';
|
Loading…
Reference in new issue