Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Lidarr and MusicBrainz support #782

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
34 changes: 24 additions & 10 deletions server/api/jellyfin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface JellyfinMediaFolder {
}

export interface JellyfinLibrary {
type: 'show' | 'movie';
type: 'show' | 'movie' | 'music';
key: string;
title: string;
agent: string;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) => {
Expand All @@ -271,7 +278,14 @@ class JellyfinAPI extends ExternalAPI {
return <JellyfinLibrary>{
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',
};
});
Expand Down
2 changes: 1 addition & 1 deletion server/api/plexapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ class PlexAPI {
options: { addedAt: number } = {
addedAt: Date.now() - 1000 * 60 * 60,
},
mediaType: 'movie' | 'show'
mediaType: 'movie' | 'show' | 'music'
): Promise<PlexLibraryItem[]> {
const response = await this.plexClient.query<PlexLibraryResponse>({
uri: `/library/sections/${id}/all?type=${
Expand Down
4 changes: 2 additions & 2 deletions server/api/servarr/base.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -79,7 +79,7 @@ interface QueueResponse<QueueItemAppendT> {
}

class ServarrBase<QueueItemAppendT> 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}`;
Expand Down
4 changes: 4 additions & 0 deletions server/entity/Media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
7 changes: 7 additions & 0 deletions server/lib/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import NodeCache from 'node-cache';

export type AvailableCacheIds =
| 'tmdb'
| 'musicbrainz'
| 'radarr'
| 'sonarr'
| 'lidarr'
| 'rt'
| 'imdb'
| 'github'
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 13 additions & 4 deletions server/lib/scanners/baseScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface MediaIds {
tmdbId: number;
imdbId?: string;
tvdbId?: number;
mbId?: string;
isHama?: boolean;
}

Expand Down Expand Up @@ -79,12 +80,20 @@ class BaseScanner<T> {
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;
}
Expand Down
3 changes: 3 additions & 0 deletions server/lib/scanners/jellyfin/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -42,6 +44,7 @@ class JellyfinScanner {

constructor({ isRecentOnly }: { isRecentOnly?: boolean } = {}) {
this.tmdb = new TheMovieDb();
this.musicbrainz = new MusicBrainz();
this.isRecentOnly = isRecentOnly ?? false;
}

Expand Down
31 changes: 27 additions & 4 deletions server/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface Library {
id: string;
name: string;
enabled: boolean;
type: 'show' | 'movie';
type: 'show' | 'movie' | 'music';
lastScan?: number;
}

Expand Down Expand Up @@ -53,7 +53,7 @@ export interface TautulliSettings {
externalUrl?: string;
}

export interface DVRSettings {
export interface ArrSettings {
id: number;
name: string;
hostname: string;
Expand All @@ -64,15 +64,18 @@ export interface DVRSettings {
activeProfileId: number;
activeProfileName: string;
activeDirectory: string;
tags: number[];
is4k: boolean;
isDefault: boolean;
externalUrl?: string;
syncEnabled: boolean;
preventSearch: boolean;
tagRequests: boolean;
}

export interface DVRSettings extends ArrSettings {
tags: number[];
is4k: boolean;
}

export interface RadarrSettings extends DVRSettings {
minimumAvailability: string;
}
Expand All @@ -89,6 +92,10 @@ export interface SonarrSettings extends DVRSettings {
enableSeasonFolders: boolean;
}

export interface LidarrSettings extends ArrSettings {
tags: string[];
}

interface Quota {
quotaLimit?: number;
quotaDays?: number;
Expand All @@ -104,6 +111,7 @@ export interface MainSettings {
defaultQuotas: {
movie: Quota;
tv: Quota;
music: Quota;
};
hideAvailable: boolean;
localLogin: boolean;
Expand Down Expand Up @@ -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'
Expand All @@ -284,6 +293,7 @@ interface AllSettings {
tautulli: TautulliSettings;
radarr: RadarrSettings[];
sonarr: SonarrSettings[];
lidarr: LidarrSettings[];
public: PublicSettings;
notifications: NotificationSettings;
jobs: Record<JobId, JobSettings>;
Expand Down Expand Up @@ -311,6 +321,7 @@ class Settings {
defaultQuotas: {
movie: {},
tv: {},
music: {},
},
hideAvailable: false,
localLogin: true,
Expand Down Expand Up @@ -340,6 +351,7 @@ class Settings {
tautulli: {},
radarr: [],
sonarr: [],
lidarr: [],
public: {
initialized: false,
},
Expand Down Expand Up @@ -445,6 +457,9 @@ class Settings {
'sonarr-scan': {
schedule: '0 30 4 * * *',
},
'lidarr-scan': {
schedule: '0 45 4 * * *',
},
'availability-sync': {
schedule: '0 0 5 * * *',
},
Expand Down Expand Up @@ -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;
}
Expand Down