mirror of
https://github.com/sct/overseerr.git
synced 2025-09-30 15:40:40 +02:00
feat: Radarr & Sonarr Sync (#734)
This commit is contained in:
@@ -442,7 +442,11 @@ class JobPlexSync {
|
||||
);
|
||||
// Total episodes that are in standard definition (not 4k)
|
||||
const totalStandard = episodes.filter((episode) =>
|
||||
episode.Media.some((media) => media.videoResolution !== '4k')
|
||||
!this.enable4kShow
|
||||
? true
|
||||
: episode.Media.some(
|
||||
(media) => media.videoResolution !== '4k'
|
||||
)
|
||||
).length;
|
||||
|
||||
// Total episodes that are in 4k
|
||||
@@ -461,9 +465,9 @@ class JobPlexSync {
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: existingSeason.status;
|
||||
existingSeason.status4k =
|
||||
total4k === season.episode_count
|
||||
this.enable4kShow && total4k === season.episode_count
|
||||
? MediaStatus.AVAILABLE
|
||||
: total4k > 0
|
||||
: this.enable4kShow && total4k > 0
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: existingSeason.status4k;
|
||||
} else {
|
||||
@@ -479,9 +483,9 @@ class JobPlexSync {
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN,
|
||||
status4k:
|
||||
total4k === season.episode_count
|
||||
this.enable4kShow && total4k === season.episode_count
|
||||
? MediaStatus.AVAILABLE
|
||||
: total4k > 0
|
||||
: this.enable4kShow && total4k > 0
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN,
|
||||
})
|
||||
@@ -563,13 +567,15 @@ class JobPlexSync {
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN;
|
||||
media.status4k = isAll4kSeasons
|
||||
? MediaStatus.AVAILABLE
|
||||
: media.seasons.some(
|
||||
(season) => season.status4k !== MediaStatus.UNKNOWN
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN;
|
||||
media.status4k =
|
||||
isAll4kSeasons && this.enable4kShow
|
||||
? MediaStatus.AVAILABLE
|
||||
: this.enable4kShow &&
|
||||
media.seasons.some(
|
||||
(season) => season.status4k !== MediaStatus.UNKNOWN
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN;
|
||||
await mediaRepository.save(media);
|
||||
this.log(`Updating existing title: ${tvShow.name}`);
|
||||
} else {
|
||||
@@ -586,13 +592,15 @@ class JobPlexSync {
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN,
|
||||
status4k: isAll4kSeasons
|
||||
? MediaStatus.AVAILABLE
|
||||
: newSeasons.some(
|
||||
(season) => season.status4k !== MediaStatus.UNKNOWN
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN,
|
||||
status4k:
|
||||
isAll4kSeasons && this.enable4kShow
|
||||
? MediaStatus.AVAILABLE
|
||||
: this.enable4kShow &&
|
||||
newSeasons.some(
|
||||
(season) => season.status4k !== MediaStatus.UNKNOWN
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN,
|
||||
});
|
||||
await mediaRepository.save(newMedia);
|
||||
this.log(`Saved ${tvShow.name}`);
|
||||
@@ -772,7 +780,8 @@ class JobPlexSync {
|
||||
this.log(
|
||||
this.isRecentOnly
|
||||
? 'Recently Added Scan Complete'
|
||||
: 'Full Scan Complete'
|
||||
: 'Full Scan Complete',
|
||||
'info'
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error('Sync interrupted', {
|
||||
|
248
server/job/radarrsync/index.ts
Normal file
248
server/job/radarrsync/index.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { uniqWith } from 'lodash';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import RadarrAPI, { RadarrMovie } from '../../api/radarr';
|
||||
import { MediaStatus, MediaType } from '../../constants/media';
|
||||
import Media from '../../entity/Media';
|
||||
import { getSettings, RadarrSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
|
||||
const BUNDLE_SIZE = 50;
|
||||
const UPDATE_RATE = 4 * 1000;
|
||||
|
||||
interface SyncStatus {
|
||||
running: boolean;
|
||||
progress: number;
|
||||
total: number;
|
||||
currentServer: RadarrSettings;
|
||||
servers: RadarrSettings[];
|
||||
}
|
||||
|
||||
class JobRadarrSync {
|
||||
private running = false;
|
||||
private progress = 0;
|
||||
private enable4k = false;
|
||||
private sessionId: string;
|
||||
private servers: RadarrSettings[];
|
||||
private currentServer: RadarrSettings;
|
||||
private radarrApi: RadarrAPI;
|
||||
private items: RadarrMovie[] = [];
|
||||
|
||||
public async run() {
|
||||
const settings = getSettings();
|
||||
const sessionId = uuid();
|
||||
this.sessionId = sessionId;
|
||||
this.log('Radarr sync starting', 'info', { sessionId });
|
||||
|
||||
try {
|
||||
this.running = true;
|
||||
|
||||
// Remove any duplicate Radarr servers and assign them to the servers field
|
||||
this.servers = uniqWith(settings.radarr, (radarrA, radarrB) => {
|
||||
return (
|
||||
radarrA.hostname === radarrB.hostname &&
|
||||
radarrA.port === radarrB.port &&
|
||||
radarrA.baseUrl === radarrB.baseUrl
|
||||
);
|
||||
});
|
||||
|
||||
this.enable4k = settings.radarr.some((radarr) => radarr.is4k);
|
||||
if (this.enable4k) {
|
||||
this.log(
|
||||
'At least one 4K Radarr server was detected. 4K movie detection is now enabled.',
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
for (const server of this.servers) {
|
||||
this.currentServer = server;
|
||||
if (server.syncEnabled) {
|
||||
this.log(
|
||||
`Beginning to process Radarr server: ${server.name}`,
|
||||
'info'
|
||||
);
|
||||
|
||||
this.radarrApi = new RadarrAPI({
|
||||
apiKey: server.apiKey,
|
||||
url: RadarrAPI.buildRadarrUrl(server, '/api/v3'),
|
||||
});
|
||||
|
||||
this.items = await this.radarrApi.getMovies();
|
||||
|
||||
await this.loop({ sessionId });
|
||||
} else {
|
||||
this.log(`Sync not enabled. Skipping Radarr server: ${server.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.log('Radarr sync complete', 'info');
|
||||
} catch (e) {
|
||||
this.log('Something went wrong.', 'error', { errorMessage: e.message });
|
||||
} finally {
|
||||
// If a new scanning session hasnt started, set running back to false
|
||||
if (this.sessionId === sessionId) {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public status(): SyncStatus {
|
||||
return {
|
||||
running: this.running,
|
||||
progress: this.progress,
|
||||
total: this.items.length,
|
||||
currentServer: this.currentServer,
|
||||
servers: this.servers,
|
||||
};
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
private async processRadarrMovie(radarrMovie: RadarrMovie) {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const server4k = this.enable4k && this.currentServer.is4k;
|
||||
|
||||
const media = await mediaRepository.findOne({
|
||||
where: { tmdbId: radarrMovie.tmdbId },
|
||||
});
|
||||
|
||||
if (media) {
|
||||
let isChanged = false;
|
||||
if (media.status === MediaStatus.AVAILABLE) {
|
||||
this.log(`Movie already available: ${radarrMovie.title}`);
|
||||
} else {
|
||||
media[server4k ? 'status4k' : 'status'] = radarrMovie.downloaded
|
||||
? MediaStatus.AVAILABLE
|
||||
: MediaStatus.PROCESSING;
|
||||
this.log(
|
||||
`Updated existing ${server4k ? '4K ' : ''}movie ${
|
||||
radarrMovie.title
|
||||
} to status ${MediaStatus[media[server4k ? 'status4k' : 'status']]}`
|
||||
);
|
||||
isChanged = true;
|
||||
}
|
||||
|
||||
if (
|
||||
media[server4k ? 'serviceId4k' : 'serviceId'] !== this.currentServer.id
|
||||
) {
|
||||
media[server4k ? 'serviceId4k' : 'serviceId'] = this.currentServer.id;
|
||||
this.log(`Updated service ID for media entity: ${radarrMovie.title}`);
|
||||
isChanged = true;
|
||||
}
|
||||
|
||||
if (
|
||||
media[server4k ? 'externalServiceId4k' : 'externalServiceId'] !==
|
||||
radarrMovie.id
|
||||
) {
|
||||
media[server4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||
radarrMovie.id;
|
||||
this.log(
|
||||
`Updated external service ID for media entity: ${radarrMovie.title}`
|
||||
);
|
||||
isChanged = true;
|
||||
}
|
||||
|
||||
if (
|
||||
media[server4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] !==
|
||||
radarrMovie.titleSlug
|
||||
) {
|
||||
media[server4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||
radarrMovie.titleSlug;
|
||||
this.log(
|
||||
`Updated external service slug for media entity: ${radarrMovie.title}`
|
||||
);
|
||||
isChanged = true;
|
||||
}
|
||||
|
||||
if (isChanged) {
|
||||
await mediaRepository.save(media);
|
||||
}
|
||||
} else {
|
||||
const newMedia = new Media({
|
||||
tmdbId: radarrMovie.tmdbId,
|
||||
imdbId: radarrMovie.imdbId,
|
||||
mediaType: MediaType.MOVIE,
|
||||
serviceId: !server4k ? this.currentServer.id : undefined,
|
||||
serviceId4k: server4k ? this.currentServer.id : undefined,
|
||||
externalServiceId: !server4k ? radarrMovie.id : undefined,
|
||||
externalServiceId4k: server4k ? radarrMovie.id : undefined,
|
||||
status:
|
||||
!server4k && radarrMovie.downloaded
|
||||
? MediaStatus.AVAILABLE
|
||||
: !server4k
|
||||
? MediaStatus.PROCESSING
|
||||
: MediaStatus.UNKNOWN,
|
||||
status4k:
|
||||
server4k && radarrMovie.downloaded
|
||||
? MediaStatus.AVAILABLE
|
||||
: server4k
|
||||
? MediaStatus.PROCESSING
|
||||
: MediaStatus.UNKNOWN,
|
||||
});
|
||||
|
||||
this.log(
|
||||
`Added media for movie ${radarrMovie.title} and set status to ${
|
||||
MediaStatus[newMedia[server4k ? 'status4k' : 'status']]
|
||||
}`
|
||||
);
|
||||
await mediaRepository.save(newMedia);
|
||||
}
|
||||
}
|
||||
|
||||
private async processItems(items: RadarrMovie[]) {
|
||||
await Promise.all(
|
||||
items.map(async (radarrMovie) => {
|
||||
await this.processRadarrMovie(radarrMovie);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async loop({
|
||||
start = 0,
|
||||
end = BUNDLE_SIZE,
|
||||
sessionId,
|
||||
}: {
|
||||
start?: number;
|
||||
end?: number;
|
||||
sessionId?: string;
|
||||
} = {}) {
|
||||
const slicedItems = this.items.slice(start, end);
|
||||
|
||||
if (!this.running) {
|
||||
throw new Error('Sync was aborted.');
|
||||
}
|
||||
|
||||
if (this.sessionId !== sessionId) {
|
||||
throw new Error('New session was started. Old session aborted.');
|
||||
}
|
||||
|
||||
if (start < this.items.length) {
|
||||
this.progress = start;
|
||||
await this.processItems(slicedItems);
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
this.loop({
|
||||
start: start + BUNDLE_SIZE,
|
||||
end: end + BUNDLE_SIZE,
|
||||
sessionId,
|
||||
})
|
||||
.then(() => resolve())
|
||||
.catch((e) => reject(new Error(e.message)));
|
||||
}, UPDATE_RATE)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private log(
|
||||
message: string,
|
||||
level: 'info' | 'error' | 'debug' | 'warn' = 'debug',
|
||||
optional?: Record<string, unknown>
|
||||
): void {
|
||||
logger[level](message, { label: 'Radarr Sync', ...optional });
|
||||
}
|
||||
}
|
||||
|
||||
export const jobRadarrSync = new JobRadarrSync();
|
@@ -1,10 +1,17 @@
|
||||
import schedule from 'node-schedule';
|
||||
import { jobPlexFullSync, jobPlexRecentSync } from './plexsync';
|
||||
import logger from '../logger';
|
||||
import { jobRadarrSync } from './radarrsync';
|
||||
import { jobSonarrSync } from './sonarrsync';
|
||||
import downloadTracker from '../lib/downloadtracker';
|
||||
|
||||
interface ScheduledJob {
|
||||
id: string;
|
||||
job: schedule.Job;
|
||||
name: string;
|
||||
type: 'process' | 'command';
|
||||
running?: () => boolean;
|
||||
cancelFn?: () => void;
|
||||
}
|
||||
|
||||
export const scheduledJobs: ScheduledJob[] = [];
|
||||
@@ -12,21 +19,80 @@ export const scheduledJobs: ScheduledJob[] = [];
|
||||
export const startJobs = (): void => {
|
||||
// Run recently added plex sync every 5 minutes
|
||||
scheduledJobs.push({
|
||||
id: 'plex-recently-added-sync',
|
||||
name: 'Plex Recently Added Sync',
|
||||
type: 'process',
|
||||
job: schedule.scheduleJob('0 */5 * * * *', () => {
|
||||
logger.info('Starting scheduled job: Plex Recently Added Sync', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
jobPlexRecentSync.run();
|
||||
}),
|
||||
running: () => jobPlexRecentSync.status().running,
|
||||
cancelFn: () => jobPlexRecentSync.cancel(),
|
||||
});
|
||||
|
||||
// Run full plex sync every 24 hours
|
||||
scheduledJobs.push({
|
||||
id: 'plex-full-sync',
|
||||
name: 'Plex Full Library Sync',
|
||||
type: 'process',
|
||||
job: schedule.scheduleJob('0 0 3 * * *', () => {
|
||||
logger.info('Starting scheduled job: Plex Full Sync', { label: 'Jobs' });
|
||||
jobPlexFullSync.run();
|
||||
}),
|
||||
running: () => jobPlexFullSync.status().running,
|
||||
cancelFn: () => jobPlexFullSync.cancel(),
|
||||
});
|
||||
|
||||
// Run full radarr sync every 24 hours
|
||||
scheduledJobs.push({
|
||||
id: 'radarr-sync',
|
||||
name: 'Radarr Sync',
|
||||
type: 'process',
|
||||
job: schedule.scheduleJob('0 0 4 * * *', () => {
|
||||
logger.info('Starting scheduled job: Radarr Sync', { label: 'Jobs' });
|
||||
jobRadarrSync.run();
|
||||
}),
|
||||
running: () => jobRadarrSync.status().running,
|
||||
cancelFn: () => jobRadarrSync.cancel(),
|
||||
});
|
||||
|
||||
// Run full sonarr sync every 24 hours
|
||||
scheduledJobs.push({
|
||||
id: 'sonarr-sync',
|
||||
name: 'Sonarr Sync',
|
||||
type: 'process',
|
||||
job: schedule.scheduleJob('0 30 4 * * *', () => {
|
||||
logger.info('Starting scheduled job: Sonarr Sync', { label: 'Jobs' });
|
||||
jobSonarrSync.run();
|
||||
}),
|
||||
running: () => jobSonarrSync.status().running,
|
||||
cancelFn: () => jobSonarrSync.cancel(),
|
||||
});
|
||||
|
||||
// Run download sync
|
||||
scheduledJobs.push({
|
||||
id: 'download-sync',
|
||||
name: 'Download Sync',
|
||||
type: 'command',
|
||||
job: schedule.scheduleJob('0 * * * * *', () => {
|
||||
logger.debug('Starting scheduled job: Download Sync', { label: 'Jobs' });
|
||||
downloadTracker.updateDownloads();
|
||||
}),
|
||||
});
|
||||
|
||||
// Reset download sync
|
||||
scheduledJobs.push({
|
||||
id: 'download-sync-reset',
|
||||
name: 'Download Sync Reset',
|
||||
type: 'command',
|
||||
job: schedule.scheduleJob('0 0 1 * * *', () => {
|
||||
logger.info('Starting scheduled job: Download Sync Reset', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
downloadTracker.resetDownloadTracker();
|
||||
}),
|
||||
});
|
||||
|
||||
logger.info('Scheduled jobs loaded', { label: 'Jobs' });
|
||||
|
358
server/job/sonarrsync/index.ts
Normal file
358
server/job/sonarrsync/index.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import { uniqWith } from 'lodash';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import SonarrAPI, { SonarrSeries } from '../../api/sonarr';
|
||||
import TheMovieDb, { TmdbTvDetails } from '../../api/themoviedb';
|
||||
import { MediaStatus, MediaType } from '../../constants/media';
|
||||
import Media from '../../entity/Media';
|
||||
import Season from '../../entity/Season';
|
||||
import { getSettings, SonarrSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
|
||||
const BUNDLE_SIZE = 50;
|
||||
const UPDATE_RATE = 4 * 1000;
|
||||
|
||||
interface SyncStatus {
|
||||
running: boolean;
|
||||
progress: number;
|
||||
total: number;
|
||||
currentServer: SonarrSettings;
|
||||
servers: SonarrSettings[];
|
||||
}
|
||||
|
||||
class JobSonarrSync {
|
||||
private running = false;
|
||||
private progress = 0;
|
||||
private enable4k = false;
|
||||
private sessionId: string;
|
||||
private servers: SonarrSettings[];
|
||||
private currentServer: SonarrSettings;
|
||||
private sonarrApi: SonarrAPI;
|
||||
private items: SonarrSeries[] = [];
|
||||
|
||||
public async run() {
|
||||
const settings = getSettings();
|
||||
const sessionId = uuid();
|
||||
this.sessionId = sessionId;
|
||||
this.log('Sonarr sync starting', 'info', { sessionId });
|
||||
|
||||
try {
|
||||
this.running = true;
|
||||
|
||||
// Remove any duplicate Sonarr servers and assign them to the servers field
|
||||
this.servers = uniqWith(settings.sonarr, (sonarrA, sonarrB) => {
|
||||
return (
|
||||
sonarrA.hostname === sonarrB.hostname &&
|
||||
sonarrA.port === sonarrB.port &&
|
||||
sonarrA.baseUrl === sonarrB.baseUrl
|
||||
);
|
||||
});
|
||||
|
||||
this.enable4k = settings.sonarr.some((sonarr) => sonarr.is4k);
|
||||
if (this.enable4k) {
|
||||
this.log(
|
||||
'At least one 4K Sonarr server was detected. 4K movie detection is now enabled.',
|
||||
'info'
|
||||
);
|
||||
}
|
||||
|
||||
for (const server of this.servers) {
|
||||
this.currentServer = server;
|
||||
if (server.syncEnabled) {
|
||||
this.log(
|
||||
`Beginning to process Sonarr server: ${server.name}`,
|
||||
'info'
|
||||
);
|
||||
|
||||
this.sonarrApi = new SonarrAPI({
|
||||
apiKey: server.apiKey,
|
||||
url: SonarrAPI.buildSonarrUrl(server, '/api/v3'),
|
||||
});
|
||||
|
||||
this.items = await this.sonarrApi.getSeries();
|
||||
|
||||
await this.loop({ sessionId });
|
||||
} else {
|
||||
this.log(`Sync not enabled. Skipping Sonarr server: ${server.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.log('Sonarr sync complete', 'info');
|
||||
} catch (e) {
|
||||
this.log('Something went wrong.', 'error', { errorMessage: e.message });
|
||||
} finally {
|
||||
// If a new scanning session hasnt started, set running back to false
|
||||
if (this.sessionId === sessionId) {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public status(): SyncStatus {
|
||||
return {
|
||||
running: this.running,
|
||||
progress: this.progress,
|
||||
total: this.items.length,
|
||||
currentServer: this.currentServer,
|
||||
servers: this.servers,
|
||||
};
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
private async processSonarrSeries(sonarrSeries: SonarrSeries) {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const server4k = this.enable4k && this.currentServer.is4k;
|
||||
|
||||
const media = await mediaRepository.findOne({
|
||||
where: { tvdbId: sonarrSeries.tvdbId },
|
||||
});
|
||||
|
||||
const currentSeasonsAvailable = (media?.seasons ?? []).filter(
|
||||
(season) =>
|
||||
season[server4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
||||
).length;
|
||||
|
||||
const newSeasons: Season[] = [];
|
||||
|
||||
for (const season of sonarrSeries.seasons) {
|
||||
const existingSeason = media?.seasons.find(
|
||||
(es) => es.seasonNumber === season.seasonNumber
|
||||
);
|
||||
|
||||
// We are already tracking this season so we can work on it directly
|
||||
if (existingSeason) {
|
||||
if (
|
||||
existingSeason[server4k ? 'status4k' : 'status'] !==
|
||||
MediaStatus.AVAILABLE &&
|
||||
season.statistics
|
||||
) {
|
||||
existingSeason[server4k ? 'status4k' : 'status'] =
|
||||
season.statistics.episodeFileCount ===
|
||||
season.statistics.totalEpisodeCount
|
||||
? MediaStatus.AVAILABLE
|
||||
: season.statistics.episodeFileCount > 0
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: season.monitored
|
||||
? MediaStatus.PROCESSING
|
||||
: existingSeason[server4k ? 'status4k' : 'status'];
|
||||
}
|
||||
} else {
|
||||
if (season.statistics && season.seasonNumber !== 0) {
|
||||
const allEpisodes =
|
||||
season.statistics.episodeFileCount ===
|
||||
season.statistics.totalEpisodeCount;
|
||||
newSeasons.push(
|
||||
new Season({
|
||||
seasonNumber: season.seasonNumber,
|
||||
status:
|
||||
!server4k && allEpisodes
|
||||
? MediaStatus.AVAILABLE
|
||||
: !server4k && season.statistics.episodeFileCount > 0
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: !server4k && season.monitored
|
||||
? MediaStatus.PROCESSING
|
||||
: MediaStatus.UNKNOWN,
|
||||
status4k:
|
||||
server4k && allEpisodes
|
||||
? MediaStatus.AVAILABLE
|
||||
: server4k && season.statistics.episodeFileCount > 0
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: !server4k && season.monitored
|
||||
? MediaStatus.PROCESSING
|
||||
: MediaStatus.UNKNOWN,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filteredSeasons = sonarrSeries.seasons.filter(
|
||||
(s) => s.seasonNumber !== 0
|
||||
);
|
||||
|
||||
const isAllSeasons =
|
||||
(media?.seasons ?? []).filter(
|
||||
(s) => s[server4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
||||
).length +
|
||||
newSeasons.filter(
|
||||
(s) => s[server4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
||||
).length >=
|
||||
filteredSeasons.length;
|
||||
|
||||
if (media) {
|
||||
media.seasons = [...media.seasons, ...newSeasons];
|
||||
|
||||
const newSeasonsAvailable = (media?.seasons ?? []).filter(
|
||||
(season) =>
|
||||
season[server4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
||||
).length;
|
||||
|
||||
if (newSeasonsAvailable > currentSeasonsAvailable) {
|
||||
this.log(
|
||||
`Detected ${newSeasonsAvailable - currentSeasonsAvailable} new ${
|
||||
server4k ? '4K ' : ''
|
||||
}season(s) for ${sonarrSeries.title}`,
|
||||
'debug'
|
||||
);
|
||||
media.lastSeasonChange = new Date();
|
||||
}
|
||||
|
||||
if (
|
||||
media[server4k ? 'serviceId4k' : 'serviceId'] !== this.currentServer.id
|
||||
) {
|
||||
media[server4k ? 'serviceId4k' : 'serviceId'] = this.currentServer.id;
|
||||
this.log(`Updated service ID for media entity: ${sonarrSeries.title}`);
|
||||
}
|
||||
|
||||
if (
|
||||
media[server4k ? 'externalServiceId4k' : 'externalServiceId'] !==
|
||||
sonarrSeries.id
|
||||
) {
|
||||
media[server4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||
sonarrSeries.id;
|
||||
this.log(
|
||||
`Updated external service ID for media entity: ${sonarrSeries.title}`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
media[server4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] !==
|
||||
sonarrSeries.titleSlug
|
||||
) {
|
||||
media[server4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||
sonarrSeries.titleSlug;
|
||||
this.log(
|
||||
`Updated external service slug for media entity: ${sonarrSeries.title}`
|
||||
);
|
||||
}
|
||||
|
||||
media[server4k ? 'status4k' : 'status'] = isAllSeasons
|
||||
? MediaStatus.AVAILABLE
|
||||
: media.seasons.some((season) => season.status !== MediaStatus.UNKNOWN)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: MediaStatus.UNKNOWN;
|
||||
|
||||
await mediaRepository.save(media);
|
||||
} else {
|
||||
const tmdb = new TheMovieDb();
|
||||
let tvShow: TmdbTvDetails;
|
||||
|
||||
try {
|
||||
tvShow = await tmdb.getShowByTvdbId({
|
||||
tvdbId: sonarrSeries.tvdbId,
|
||||
});
|
||||
} catch (e) {
|
||||
this.log(
|
||||
'Failed to create new media item during sync. TVDB ID is missing from TMDB?',
|
||||
'warn',
|
||||
{ sonarrSeries, errorMessage: e.message }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const newMedia = new Media({
|
||||
tmdbId: tvShow.id,
|
||||
tvdbId: sonarrSeries.tvdbId,
|
||||
mediaType: MediaType.TV,
|
||||
serviceId: !server4k ? this.currentServer.id : undefined,
|
||||
serviceId4k: server4k ? this.currentServer.id : undefined,
|
||||
externalServiceId: !server4k ? sonarrSeries.id : undefined,
|
||||
externalServiceId4k: server4k ? sonarrSeries.id : undefined,
|
||||
externalServiceSlug: !server4k ? sonarrSeries.titleSlug : undefined,
|
||||
externalServiceSlug4k: server4k ? sonarrSeries.titleSlug : undefined,
|
||||
seasons: newSeasons,
|
||||
status:
|
||||
!server4k && isAllSeasons
|
||||
? MediaStatus.AVAILABLE
|
||||
: !server4k &&
|
||||
newSeasons.some(
|
||||
(s) =>
|
||||
s.status === MediaStatus.PARTIALLY_AVAILABLE ||
|
||||
s.status === MediaStatus.AVAILABLE
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: !server4k
|
||||
? MediaStatus.PROCESSING
|
||||
: MediaStatus.UNKNOWN,
|
||||
status4k:
|
||||
server4k && isAllSeasons
|
||||
? MediaStatus.AVAILABLE
|
||||
: server4k &&
|
||||
newSeasons.some(
|
||||
(s) =>
|
||||
s.status4k === MediaStatus.PARTIALLY_AVAILABLE ||
|
||||
s.status4k === MediaStatus.AVAILABLE
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: server4k
|
||||
? MediaStatus.PROCESSING
|
||||
: MediaStatus.UNKNOWN,
|
||||
});
|
||||
|
||||
this.log(
|
||||
`Added media for series ${sonarrSeries.title} and set status to ${
|
||||
MediaStatus[newMedia[server4k ? 'status4k' : 'status']]
|
||||
}`
|
||||
);
|
||||
await mediaRepository.save(newMedia);
|
||||
}
|
||||
}
|
||||
|
||||
private async processItems(items: SonarrSeries[]) {
|
||||
await Promise.all(
|
||||
items.map(async (sonarrSeries) => {
|
||||
await this.processSonarrSeries(sonarrSeries);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async loop({
|
||||
start = 0,
|
||||
end = BUNDLE_SIZE,
|
||||
sessionId,
|
||||
}: {
|
||||
start?: number;
|
||||
end?: number;
|
||||
sessionId?: string;
|
||||
} = {}) {
|
||||
const slicedItems = this.items.slice(start, end);
|
||||
|
||||
if (!this.running) {
|
||||
throw new Error('Sync was aborted.');
|
||||
}
|
||||
|
||||
if (this.sessionId !== sessionId) {
|
||||
throw new Error('New session was started. Old session aborted.');
|
||||
}
|
||||
|
||||
if (start < this.items.length) {
|
||||
this.progress = start;
|
||||
await this.processItems(slicedItems);
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
this.loop({
|
||||
start: start + BUNDLE_SIZE,
|
||||
end: end + BUNDLE_SIZE,
|
||||
sessionId,
|
||||
})
|
||||
.then(() => resolve())
|
||||
.catch((e) => reject(new Error(e.message)));
|
||||
}, UPDATE_RATE)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private log(
|
||||
message: string,
|
||||
level: 'info' | 'error' | 'debug' | 'warn' = 'debug',
|
||||
optional?: Record<string, unknown>
|
||||
): void {
|
||||
logger[level](message, { label: 'Sonarr Sync', ...optional });
|
||||
}
|
||||
}
|
||||
|
||||
export const jobSonarrSync = new JobSonarrSync();
|
Reference in New Issue
Block a user