Files
sct-overseerr/server/lib/scanners/sonarr/index.ts
Ahmed Siddiqui c2d4c61fae feat: add support for requesting "Specials" for TV Shows (#3724)
* feat: add support for requesting "Specials" for TV Shows

This commit is responsible for adding support in Overseerr for requesting "Special" episodes for TV
Shows. This request has become especially pertinent when you consider shows like "Doctor Who". These
shows have Specials that are critical to understanding the plot of a TV show.

fix #779

* chore(yarn.lock): undo inappropriate changes to yarn.lock

I was informed by @sct in a comment on the #3724 PR that it was not appropriate to commit the
changes that ended up being made to the yarn.lock file. This commit is responsible, then, for
undoing the changes to the yarn.lock file that ended up being submitted.

* refactor: change loose equality to strict equality

I received a comment from OwsleyJr pointing out that we are using loose equality when we could
alternatively just be using strict equality to increase the robustness of our code. This commit
does exactly that by squashing out previous usages of loose equality in my commits and replacing
them with strict equality

* refactor: move 'Specials' string to a global message

Owsley pointed out that we are redefining the 'Specials' string multiple times throughout this PR.
Instead, we can just move it as a global message. This commit does exactly that. It squashes out and
previous declarations of the 'Specials' string inside the src files, and moves it directly to the
global messages file.
2024-10-21 05:02:10 +00:00

140 lines
4.1 KiB
TypeScript

import type { SonarrSeries } from '@server/api/servarr/sonarr';
import SonarrAPI from '@server/api/servarr/sonarr';
import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import type {
ProcessableSeason,
RunnableScanner,
StatusBase,
} from '@server/lib/scanners/baseScanner';
import BaseScanner from '@server/lib/scanners/baseScanner';
import type { SonarrSettings } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import { uniqWith } from 'lodash';
type SyncStatus = StatusBase & {
currentServer: SonarrSettings;
servers: SonarrSettings[];
};
class SonarrScanner
extends BaseScanner<SonarrSeries>
implements RunnableScanner<SyncStatus>
{
private servers: SonarrSettings[];
private currentServer: SonarrSettings;
private sonarrApi: SonarrAPI;
constructor() {
super('Sonarr Scan', { bundleSize: 50 });
}
public status(): SyncStatus {
return {
running: this.running,
progress: this.progress,
total: this.items.length,
currentServer: this.currentServer,
servers: this.servers,
};
}
public async run(): Promise<void> {
const settings = getSettings();
const sessionId = this.startRun();
try {
this.servers = uniqWith(settings.sonarr, (sonarrA, sonarrB) => {
return (
sonarrA.hostname === sonarrB.hostname &&
sonarrA.port === sonarrB.port &&
sonarrA.baseUrl === sonarrB.baseUrl
);
});
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.buildUrl(server, '/api/v3'),
});
this.items = await this.sonarrApi.getSeries();
await this.loop(this.processSonarrSeries.bind(this), { sessionId });
} else {
this.log(`Sync not enabled. Skipping Sonarr server: ${server.name}`);
}
}
this.log('Sonarr scan complete', 'info');
} catch (e) {
this.log('Scan interrupted', 'error', { errorMessage: e.message });
} finally {
this.endRun(sessionId);
}
}
private async processSonarrSeries(sonarrSeries: SonarrSeries) {
try {
const mediaRepository = getRepository(Media);
const server4k = this.enable4kShow && this.currentServer.is4k;
const processableSeasons: ProcessableSeason[] = [];
let tvShow: TmdbTvDetails;
const media = await mediaRepository.findOne({
where: { tvdbId: sonarrSeries.tvdbId },
});
if (!media || !media.tmdbId) {
tvShow = await this.tmdb.getShowByTvdbId({
tvdbId: sonarrSeries.tvdbId,
});
} else {
tvShow = await this.tmdb.getTvShow({ tvId: media.tmdbId });
}
const tmdbId = tvShow.id;
const filteredSeasons = sonarrSeries.seasons.filter((sn) =>
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber)
);
for (const season of filteredSeasons) {
const totalAvailableEpisodes = season.statistics?.episodeFileCount ?? 0;
processableSeasons.push({
seasonNumber: season.seasonNumber,
episodes: !server4k ? totalAvailableEpisodes : 0,
episodes4k: server4k ? totalAvailableEpisodes : 0,
totalEpisodes: season.statistics?.totalEpisodeCount ?? 0,
processing: season.monitored && totalAvailableEpisodes === 0,
is4kOverride: server4k,
});
}
await this.processShow(tmdbId, sonarrSeries.tvdbId, processableSeasons, {
serviceId: this.currentServer.id,
externalServiceId: sonarrSeries.id,
externalServiceSlug: sonarrSeries.titleSlug,
title: sonarrSeries.title,
is4k: server4k,
});
} catch (e) {
this.log('Failed to process Sonarr media', 'error', {
errorMessage: e.message,
title: sonarrSeries.title,
});
}
}
}
export const sonarrScanner = new SonarrScanner();