mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-11 09:06:55 -05:00
Compare commits
4 Commits
pr-2273
...
fallenbage
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fa68da481 | ||
|
|
cf5a85ba0b | ||
|
|
9cbd5f4260 | ||
|
|
09233a32b3 |
@@ -20,6 +20,7 @@ import type {
|
||||
StatusBase,
|
||||
} from '@server/lib/scanners/baseScanner';
|
||||
import BaseScanner from '@server/lib/scanners/baseScanner';
|
||||
import serviceAvailabilityChecker from '@server/lib/scanners/serviceAvailabilityChecker';
|
||||
import type { Library } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
@@ -125,6 +126,57 @@ class JellyfinScanner
|
||||
|
||||
const { tmdbId, imdbId, metadata } = extracted;
|
||||
|
||||
const mediaAddedAt = metadata.DateCreated
|
||||
? new Date(metadata.DateCreated)
|
||||
: undefined;
|
||||
|
||||
if (this.enable4kMovie) {
|
||||
const instanceAvailability =
|
||||
await serviceAvailabilityChecker.checkMovieAvailability(tmdbId);
|
||||
|
||||
if (instanceAvailability.hasStandard || instanceAvailability.has4k) {
|
||||
if (instanceAvailability.hasStandard) {
|
||||
await this.processMovie(tmdbId, {
|
||||
is4k: false,
|
||||
mediaAddedAt,
|
||||
jellyfinMediaId: metadata.Id,
|
||||
imdbId,
|
||||
title: metadata.Name,
|
||||
});
|
||||
}
|
||||
|
||||
if (instanceAvailability.has4k) {
|
||||
await this.processMovie(tmdbId, {
|
||||
is4k: true,
|
||||
mediaAddedAt,
|
||||
jellyfinMediaId: metadata.Id,
|
||||
imdbId,
|
||||
title: metadata.Name,
|
||||
});
|
||||
}
|
||||
|
||||
this.log(
|
||||
`Processed movie with service availability check: ${metadata.Name}`,
|
||||
'debug',
|
||||
{
|
||||
tmdbId,
|
||||
hasStandard: instanceAvailability.hasStandard,
|
||||
has4k: instanceAvailability.has4k,
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(
|
||||
`Movie not found in any Radarr instance, using resolution-based detection: ${metadata.Name}`,
|
||||
'debug',
|
||||
{
|
||||
tmdbId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const has4k = metadata.MediaSources?.some((MediaSource) => {
|
||||
return MediaSource.MediaStreams.filter(
|
||||
(MediaStream) => MediaStream.Type === 'Video'
|
||||
@@ -141,10 +193,6 @@ class JellyfinScanner
|
||||
});
|
||||
});
|
||||
|
||||
const mediaAddedAt = metadata.DateCreated
|
||||
? new Date(metadata.DateCreated)
|
||||
: undefined;
|
||||
|
||||
if (hasOtherResolution || (!this.enable4kMovie && has4k)) {
|
||||
await this.processMovie(tmdbId, {
|
||||
is4k: false,
|
||||
@@ -285,6 +333,34 @@ class JellyfinScanner
|
||||
? seasons
|
||||
: seasons.filter((sn) => sn.season_number !== 0);
|
||||
|
||||
let instanceAvailability: Awaited<
|
||||
ReturnType<typeof serviceAvailabilityChecker.checkShowAvailability>
|
||||
> | null = null;
|
||||
let useServiceBasedDetection = false;
|
||||
|
||||
if (this.enable4kShow && tvShow.external_ids?.tvdb_id) {
|
||||
instanceAvailability =
|
||||
await serviceAvailabilityChecker.checkShowAvailability(
|
||||
tvShow.external_ids.tvdb_id
|
||||
);
|
||||
|
||||
useServiceBasedDetection =
|
||||
instanceAvailability.hasStandard || instanceAvailability.has4k;
|
||||
|
||||
if (useServiceBasedDetection) {
|
||||
this.log(
|
||||
`Using service availability check for show: ${tvShow.name}`,
|
||||
'debug',
|
||||
{
|
||||
tvdbId: tvShow.external_ids.tvdb_id,
|
||||
hasStandard: instanceAvailability.hasStandard,
|
||||
has4k: instanceAvailability.has4k,
|
||||
seasons: instanceAvailability.seasons.length,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const season of filteredSeasons) {
|
||||
const matchedJellyfinSeason = jellyfinSeasons.find((md) => {
|
||||
if (tvdbSeasonFromAnidb) {
|
||||
@@ -306,7 +382,16 @@ class JellyfinScanner
|
||||
let totalStandard = 0;
|
||||
let total4k = 0;
|
||||
|
||||
if (!this.enable4kShow) {
|
||||
if (useServiceBasedDetection && instanceAvailability) {
|
||||
const serviceSeason = instanceAvailability.seasons.find(
|
||||
(s) => s.seasonNumber === season.season_number
|
||||
);
|
||||
|
||||
if (serviceSeason) {
|
||||
totalStandard = serviceSeason.episodesStandard;
|
||||
total4k = serviceSeason.episodes4k;
|
||||
}
|
||||
} else if (!this.enable4kShow) {
|
||||
const episodes = await this.jfClient.getEpisodes(
|
||||
Id,
|
||||
matchedJellyfinSeason.Id
|
||||
@@ -362,14 +447,6 @@ class JellyfinScanner
|
||||
)
|
||||
);
|
||||
|
||||
// Count in both if episode has both versions
|
||||
// TODO: Make this more robust in the future
|
||||
// Currently, this detection is based solely on file resolution, not which
|
||||
// Radarr/Sonarr instance the file came from. If a 4K request results in
|
||||
// 1080p files (no 4K release available yet), those files will be counted
|
||||
// as "standard" even though they're in the 4K library. This can cause
|
||||
// non-4K users to see content as "available" when they can't access it.
|
||||
// See issue https://github.com/seerr-team/seerr/issues/1744 for details.
|
||||
if (hasStandard) totalStandard += episodeCount;
|
||||
if (has4k) total4k += episodeCount;
|
||||
}
|
||||
@@ -452,6 +529,8 @@ class JellyfinScanner
|
||||
|
||||
const sessionId = this.startRun();
|
||||
|
||||
serviceAvailabilityChecker.clearCache();
|
||||
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOne({
|
||||
|
||||
193
server/lib/scanners/serviceAvailabilityChecker.ts
Normal file
193
server/lib/scanners/serviceAvailabilityChecker.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import RadarrAPI from '@server/api/servarr/radarr';
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
|
||||
interface InstanceAvailability {
|
||||
hasStandard: boolean;
|
||||
has4k: boolean;
|
||||
serviceStandardId?: number;
|
||||
service4kId?: number;
|
||||
externalStandardId?: number;
|
||||
external4kId?: number;
|
||||
}
|
||||
|
||||
interface SeasonInstanceAvailability {
|
||||
seasonNumber: number;
|
||||
episodesStandard: number;
|
||||
episodes4k: number;
|
||||
}
|
||||
|
||||
interface ShowInstanceAvailability extends InstanceAvailability {
|
||||
seasons: SeasonInstanceAvailability[];
|
||||
}
|
||||
|
||||
class ServiceAvailabilityChecker {
|
||||
private movieCache: Map<number, InstanceAvailability>;
|
||||
private showCache: Map<number, ShowInstanceAvailability>;
|
||||
|
||||
constructor() {
|
||||
this.movieCache = new Map();
|
||||
this.showCache = new Map();
|
||||
}
|
||||
|
||||
public clearCache(): void {
|
||||
this.movieCache.clear();
|
||||
this.showCache.clear();
|
||||
}
|
||||
|
||||
public async checkMovieAvailability(
|
||||
tmdbid: number
|
||||
): Promise<InstanceAvailability> {
|
||||
const cached = this.movieCache.get(tmdbid);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const settings = getSettings();
|
||||
const result: InstanceAvailability = {
|
||||
hasStandard: false,
|
||||
has4k: false,
|
||||
};
|
||||
|
||||
if (!settings.radarr || settings.radarr.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const radarrSettings of settings.radarr) {
|
||||
try {
|
||||
const radarr = this.createRadarrClient(radarrSettings);
|
||||
const movie = await radarr.getMovieByTmdbId(tmdbid);
|
||||
|
||||
if (movie?.hasFile) {
|
||||
if (radarrSettings.is4k) {
|
||||
result.has4k = true;
|
||||
result.service4kId = radarrSettings.id;
|
||||
result.external4kId = movie.id;
|
||||
} else {
|
||||
result.hasStandard = true;
|
||||
result.serviceStandardId = radarrSettings.id;
|
||||
result.externalStandardId = movie.id;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Found movie (TMDB: ${tmdbid}) in ${
|
||||
radarrSettings.is4k ? '4K' : 'Standard'
|
||||
} Radarr instance (name: ${radarrSettings.name})`,
|
||||
{
|
||||
label: 'Service Availability',
|
||||
radarrId: radarrSettings.id,
|
||||
movieId: movie?.id,
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
// movie not found in this instance, continue
|
||||
}
|
||||
}
|
||||
|
||||
this.movieCache.set(tmdbid, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async checkShowAvailability(
|
||||
tvdbid: number
|
||||
): Promise<ShowInstanceAvailability> {
|
||||
const cached = this.showCache.get(tvdbid);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const settings = getSettings();
|
||||
const result: ShowInstanceAvailability = {
|
||||
hasStandard: false,
|
||||
has4k: false,
|
||||
seasons: [],
|
||||
};
|
||||
|
||||
if (!settings.sonarr || settings.sonarr.length === 0) {
|
||||
return result;
|
||||
}
|
||||
const standardSeasons = new Map<number, number>();
|
||||
const seasons4k = new Map<number, number>();
|
||||
|
||||
for (const sonarrSettings of settings.sonarr) {
|
||||
try {
|
||||
const sonarr = this.createSonarrClient(sonarrSettings);
|
||||
const series = await sonarr.getSeriesByTvdbId(tvdbid);
|
||||
|
||||
if (series?.id && series.statistics?.episodeFileCount > 0) {
|
||||
if (sonarrSettings.is4k) {
|
||||
result.has4k = true;
|
||||
result.service4kId = sonarrSettings.id;
|
||||
result.external4kId = series.id;
|
||||
} else {
|
||||
result.hasStandard = true;
|
||||
result.serviceStandardId = sonarrSettings.id;
|
||||
result.externalStandardId = series.id;
|
||||
}
|
||||
|
||||
for (const season of series.seasons) {
|
||||
const episodeCount = season.statistics?.episodeFileCount ?? 0;
|
||||
if (episodeCount > 0) {
|
||||
const targetMap = sonarrSettings.is4k
|
||||
? seasons4k
|
||||
: standardSeasons;
|
||||
const current = targetMap.get(season.seasonNumber) ?? 0;
|
||||
targetMap.set(
|
||||
season.seasonNumber,
|
||||
Math.max(current, episodeCount)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Found series (TVDB: ${tvdbid}) in ${
|
||||
sonarrSettings.is4k ? '4K' : 'Standard'
|
||||
} Sonarr instance (name: ${sonarrSettings.name})`,
|
||||
{
|
||||
label: 'Service Availability',
|
||||
sonarrId: sonarrSettings.id,
|
||||
seriesId: series.id,
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// series not found in this instance, continue
|
||||
}
|
||||
}
|
||||
|
||||
const allSeasonNumbers = new Set([
|
||||
...standardSeasons.keys(),
|
||||
...seasons4k.keys(),
|
||||
]);
|
||||
|
||||
result.seasons = Array.from(allSeasonNumbers).map((seasonNumber) => ({
|
||||
seasonNumber,
|
||||
episodesStandard: standardSeasons.get(seasonNumber) ?? 0,
|
||||
episodes4k: seasons4k.get(seasonNumber) ?? 0,
|
||||
}));
|
||||
|
||||
this.showCache.set(tvdbid, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private createRadarrClient(settings: RadarrSettings): RadarrAPI {
|
||||
return new RadarrAPI({
|
||||
url: RadarrAPI.buildUrl(settings, '/api/v3'),
|
||||
apiKey: settings.apiKey,
|
||||
});
|
||||
}
|
||||
|
||||
private createSonarrClient(settings: SonarrSettings): SonarrAPI {
|
||||
return new SonarrAPI({
|
||||
url: SonarrAPI.buildUrl(settings, '/api/v3'),
|
||||
apiKey: settings.apiKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const serviceAvailabilityChecker = new ServiceAvailabilityChecker();
|
||||
|
||||
export default serviceAvailabilityChecker;
|
||||
Reference in New Issue
Block a user