diff --git a/package.json b/package.json index 97c025503..9cc84548a 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "node-cache": "5.1.2", "node-gyp": "9.3.1", "node-schedule": "2.1.1", + "nodebrainz": "^2.1.1", "nodemailer": "6.9.1", "openpgp": "5.7.0", "plex-api": "5.3.2", diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index f23e9aceb..4a0fd18cb 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -37,7 +37,7 @@ interface JellyfinMediaFolder { } export interface JellyfinLibrary { - type: 'show' | 'movie'; + type: 'show' | 'movie' | 'music'; key: string; title: string; agent: string; @@ -47,7 +47,15 @@ export interface JellyfinLibraryItem { Name: string; Id: string; HasSubtitles: boolean; - Type: 'Movie' | 'Episode' | 'Season' | 'Series'; + HasLyrics: boolean; + Type: + | 'Movie' + | 'Episode' + | 'Season' + | 'Series' + | 'Audio' + | 'MusicAlbum' + | 'MusicArtist'; LocationType: 'FileSystem' | 'Offline' | 'Remote' | 'Virtual'; SeriesName?: string; SeriesId?: string; @@ -84,6 +92,11 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem { Tmdb?: string; Imdb?: string; Tvdb?: string; + MusicBrainzAlbumArtist?: string; + MusicBrainzArtist?: string; + MusicBrainzAlbum?: string; + MusicBrainzReleaseGroup?: string; + MusicBrainzTrack?: string; }; MediaSources?: JellyfinMediaSource[]; Width?: number; @@ -252,13 +265,7 @@ class JellyfinAPI extends ExternalAPI { } private mapLibraries(mediaFolders: JellyfinMediaFolder[]): JellyfinLibrary[] { - const excludedTypes = [ - 'music', - 'books', - 'musicvideos', - 'homevideos', - 'boxsets', - ]; + const excludedTypes = ['books', 'musicvideos', 'homevideos', 'boxsets']; return mediaFolders .filter((Item: JellyfinMediaFolder) => { @@ -271,7 +278,14 @@ class JellyfinAPI extends ExternalAPI { return { key: Item.Id, title: Item.Name, - type: Item.CollectionType === 'movies' ? 'movie' : 'show', + type: + Item.CollectionType === 'movies' + ? 'movie' + : Item.CollectionType === 'tvshows' + ? 'show' + : Item.CollectionType === 'music' + ? 'music' + : 'show', agent: 'jellyfin', }; }); diff --git a/server/api/plexapi.ts b/server/api/plexapi.ts index f6b8f3cb0..5464b8e81 100644 --- a/server/api/plexapi.ts +++ b/server/api/plexapi.ts @@ -227,7 +227,7 @@ class PlexAPI { options: { addedAt: number } = { addedAt: Date.now() - 1000 * 60 * 60, }, - mediaType: 'movie' | 'show' + mediaType: 'movie' | 'show' | 'music' ): Promise { const response = await this.plexClient.query({ uri: `/library/sections/${id}/all?type=${ diff --git a/server/api/servarr/base.ts b/server/api/servarr/base.ts index c004b4746..a4dcd9100 100644 --- a/server/api/servarr/base.ts +++ b/server/api/servarr/base.ts @@ -1,7 +1,7 @@ import ExternalAPI from '@server/api/externalapi'; import type { AvailableCacheIds } from '@server/lib/cache'; import cacheManager from '@server/lib/cache'; -import type { DVRSettings } from '@server/lib/settings'; +import type { ArrSettings } from '@server/lib/settings'; export interface SystemStatus { version: string; @@ -79,7 +79,7 @@ interface QueueResponse { } class ServarrBase extends ExternalAPI { - static buildUrl(settings: DVRSettings, path?: string): string { + static buildUrl(settings: ArrSettings, path?: string): string { return `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${ settings.port }${settings.baseUrl ?? ''}${path}`; diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 1932670e4..1207ee8ce 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -94,6 +94,10 @@ class Media { @Index() public imdbId?: string; + @Column({ nullable: true }) + @Index() + public mbId?: string; + @Column({ type: 'int', default: MediaStatus.UNKNOWN }) public status: MediaStatus; diff --git a/server/lib/cache.ts b/server/lib/cache.ts index 011205e7f..84d09657e 100644 --- a/server/lib/cache.ts +++ b/server/lib/cache.ts @@ -2,8 +2,10 @@ import NodeCache from 'node-cache'; export type AvailableCacheIds = | 'tmdb' + | 'musicbrainz' | 'radarr' | 'sonarr' + | 'lidarr' | 'rt' | 'imdb' | 'github' @@ -46,8 +48,13 @@ class CacheManager { stdTtl: 21600, checkPeriod: 60 * 30, }), + musicbrainz: new Cache('musicbrainz', 'MusicBrainz API', { + stdTtl: 21600, + checkPeriod: 60 * 30, + }), radarr: new Cache('radarr', 'Radarr API'), sonarr: new Cache('sonarr', 'Sonarr API'), + lidarr: new Cache('lidarr', 'Lidarr API'), rt: new Cache('rt', 'Rotten Tomatoes API', { stdTtl: 43200, checkPeriod: 60 * 30, diff --git a/server/lib/scanners/baseScanner.ts b/server/lib/scanners/baseScanner.ts index f0f3db7e6..0582253e2 100644 --- a/server/lib/scanners/baseScanner.ts +++ b/server/lib/scanners/baseScanner.ts @@ -27,6 +27,7 @@ export interface MediaIds { tmdbId: number; imdbId?: string; tvdbId?: number; + mbId?: string; isHama?: boolean; } @@ -79,12 +80,20 @@ class BaseScanner { this.updateRate = updateRate ?? UPDATE_RATE; } - private async getExisting(tmdbId: number, mediaType: MediaType) { + private async getExisting(id: number | string, mediaType: MediaType) { const mediaRepository = getRepository(Media); - const existing = await mediaRepository.findOne({ - where: { tmdbId: tmdbId, mediaType }, - }); + let existing: Media | null; + + if (mediaType === MediaType.MOVIE || mediaType === MediaType.TV) { + existing = await mediaRepository.findOne({ + where: { tmdbId: id as number, mediaType }, + }); + } else { + existing = await mediaRepository.findOne({ + where: { mbId: id as string, mediaType }, + }); + } return existing; } diff --git a/server/lib/scanners/jellyfin/index.ts b/server/lib/scanners/jellyfin/index.ts index f5b0f66a2..a67274351 100644 --- a/server/lib/scanners/jellyfin/index.ts +++ b/server/lib/scanners/jellyfin/index.ts @@ -1,5 +1,6 @@ import type { JellyfinLibraryItem } from '@server/api/jellyfin'; import JellyfinAPI from '@server/api/jellyfin'; +import MusicBrainz from '@server/api/musicbrainz'; import TheMovieDb from '@server/api/themoviedb'; import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces'; import { MediaStatus, MediaType } from '@server/constants/media'; @@ -29,6 +30,7 @@ interface SyncStatus { class JellyfinScanner { private sessionId: string; private tmdb: TheMovieDb; + private musicbrainz: MusicBrainz; private jfClient: JellyfinAPI; private items: JellyfinLibraryItem[] = []; private progress = 0; @@ -42,6 +44,7 @@ class JellyfinScanner { constructor({ isRecentOnly }: { isRecentOnly?: boolean } = {}) { this.tmdb = new TheMovieDb(); + this.musicbrainz = new MusicBrainz(); this.isRecentOnly = isRecentOnly ?? false; } diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 63f952363..137272e2e 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -10,7 +10,7 @@ export interface Library { id: string; name: string; enabled: boolean; - type: 'show' | 'movie'; + type: 'show' | 'movie' | 'music'; lastScan?: number; } @@ -53,7 +53,7 @@ export interface TautulliSettings { externalUrl?: string; } -export interface DVRSettings { +export interface ArrSettings { id: number; name: string; hostname: string; @@ -64,8 +64,6 @@ export interface DVRSettings { activeProfileId: number; activeProfileName: string; activeDirectory: string; - tags: number[]; - is4k: boolean; isDefault: boolean; externalUrl?: string; syncEnabled: boolean; @@ -73,6 +71,11 @@ export interface DVRSettings { tagRequests: boolean; } +export interface DVRSettings extends ArrSettings { + tags: number[]; + is4k: boolean; +} + export interface RadarrSettings extends DVRSettings { minimumAvailability: string; } @@ -89,6 +92,10 @@ export interface SonarrSettings extends DVRSettings { enableSeasonFolders: boolean; } +export interface LidarrSettings extends ArrSettings { + tags: string[]; +} + interface Quota { quotaLimit?: number; quotaDays?: number; @@ -104,6 +111,7 @@ export interface MainSettings { defaultQuotas: { movie: Quota; tv: Quota; + music: Quota; }; hideAvailable: boolean; localLogin: boolean; @@ -267,6 +275,7 @@ export type JobId = | 'plex-watchlist-sync' | 'radarr-scan' | 'sonarr-scan' + | 'lidarr-scan' | 'download-sync' | 'download-sync-reset' | 'jellyfin-recently-added-scan' @@ -284,6 +293,7 @@ interface AllSettings { tautulli: TautulliSettings; radarr: RadarrSettings[]; sonarr: SonarrSettings[]; + lidarr: LidarrSettings[]; public: PublicSettings; notifications: NotificationSettings; jobs: Record; @@ -311,6 +321,7 @@ class Settings { defaultQuotas: { movie: {}, tv: {}, + music: {}, }, hideAvailable: false, localLogin: true, @@ -340,6 +351,7 @@ class Settings { tautulli: {}, radarr: [], sonarr: [], + lidarr: [], public: { initialized: false, }, @@ -445,6 +457,9 @@ class Settings { 'sonarr-scan': { schedule: '0 30 4 * * *', }, + 'lidarr-scan': { + schedule: '0 45 4 * * *', + }, 'availability-sync': { schedule: '0 0 5 * * *', }, @@ -522,6 +537,14 @@ class Settings { this.data.sonarr = data; } + get lidarr(): LidarrSettings[] { + return this.data.lidarr; + } + + set lidarr(data: LidarrSettings[]) { + this.data.lidarr = data; + } + get public(): PublicSettings { return this.data.public; }