Compare commits

...

3 Commits

Author SHA1 Message Date
fallenbagel
19efb06faa Merge branch 'develop' into feat-media-availability-sync-emby/jellyfin-support 2023-11-30 09:14:51 +05:00
fallenbagel
724b2f93b3 fix(availabilitysync): use the correct 4k jellyfinMediaId 2023-11-15 17:27:04 +05:00
fallenbagel
4676d4f0bb feat(job): media availability support for jellyfin/emby
This refactors the media availability job to support jellyfin/emby for media removal automatically.
Needs further testing on 4k items (as I have not yet tested with 4k), however, non-4k items work as
intended.

fix #406, fix #193, fix #516, fix #362, fix #84
2023-11-05 17:24:46 +05:00
4 changed files with 467 additions and 72 deletions

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import availabilitySync from '@server/lib/availabilitySync';
import logger from '@server/logger'; import logger from '@server/logger';
import type { AxiosInstance } from 'axios'; import type { AxiosInstance } from 'axios';
import axios from 'axios'; import axios from 'axios';
@@ -241,7 +242,9 @@ class JellyfinAPI {
} }
} }
public async getItemData(id: string): Promise<JellyfinLibraryItemExtended> { public async getItemData(
id: string
): Promise<JellyfinLibraryItemExtended | undefined> {
try { try {
const contents = await this.axios.get<any>( const contents = await this.axios.get<any>(
`/Users/${this.userId}/Items/${id}` `/Users/${this.userId}/Items/${id}`
@@ -249,6 +252,11 @@ class JellyfinAPI {
return contents.data; return contents.data;
} catch (e) { } catch (e) {
if (availabilitySync.running) {
if (e.response && e.response.status === 500) {
return undefined;
}
}
logger.error( logger.error(
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`, `Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' } { label: 'Jellyfin API' }

View File

@@ -1,4 +1,5 @@
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import availabilitySync from '@server/lib/availabilitySync';
import downloadTracker from '@server/lib/downloadtracker'; import downloadTracker from '@server/lib/downloadtracker';
import ImageProxy from '@server/lib/imageproxy'; import ImageProxy from '@server/lib/imageproxy';
import { import {
@@ -167,7 +168,7 @@ export const startJobs = (): void => {
}); });
// Checks if media is still available in plex/sonarr/radarr libs // Checks if media is still available in plex/sonarr/radarr libs
/* scheduledJobs.push({ scheduledJobs.push({
id: 'availability-sync', id: 'availability-sync',
name: 'Media Availability Sync', name: 'Media Availability Sync',
type: 'process', type: 'process',
@@ -182,7 +183,6 @@ export const startJobs = (): void => {
running: () => availabilitySync.running, running: () => availabilitySync.running,
cancelFn: () => availabilitySync.cancel(), cancelFn: () => availabilitySync.cancel(),
}); });
*/
// Run download sync every minute // Run download sync every minute
scheduledJobs.push({ scheduledJobs.push({

View File

@@ -1,9 +1,12 @@
import type { JellyfinLibraryItem } from '@server/api/jellyfin';
import JellyfinAPI from '@server/api/jellyfin';
import type { PlexMetadata } from '@server/api/plexapi'; import type { PlexMetadata } from '@server/api/plexapi';
import PlexAPI from '@server/api/plexapi'; import PlexAPI from '@server/api/plexapi';
import RadarrAPI, { type RadarrMovie } from '@server/api/servarr/radarr'; import RadarrAPI, { type RadarrMovie } from '@server/api/servarr/radarr';
import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr'; import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr';
import SonarrAPI from '@server/api/servarr/sonarr'; import SonarrAPI from '@server/api/servarr/sonarr';
import { MediaRequestStatus, MediaStatus } from '@server/constants/media'; import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server';
import { getRepository } from '@server/datasource'; import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media'; import Media from '@server/entity/Media';
import MediaRequest from '@server/entity/MediaRequest'; import MediaRequest from '@server/entity/MediaRequest';
@@ -18,14 +21,20 @@ class AvailabilitySync {
public running = false; public running = false;
private plexClient: PlexAPI; private plexClient: PlexAPI;
private plexSeasonsCache: Record<string, PlexMetadata[]>; private plexSeasonsCache: Record<string, PlexMetadata[]>;
private jellyfinClient: JellyfinAPI;
private jellyfinSeasonsCache: Record<string, JellyfinLibraryItem[]>;
private sonarrSeasonsCache: Record<string, SonarrSeason[]>; private sonarrSeasonsCache: Record<string, SonarrSeason[]>;
private radarrServers: RadarrSettings[]; private radarrServers: RadarrSettings[];
private sonarrServers: SonarrSettings[]; private sonarrServers: SonarrSettings[];
async run() { async run() {
const settings = getSettings(); const settings = getSettings();
const mediaServerType = getSettings().main.mediaServerType;
this.running = true; this.running = true;
this.plexSeasonsCache = {}; this.plexSeasonsCache = {};
this.jellyfinSeasonsCache = {};
this.sonarrSeasonsCache = {}; this.sonarrSeasonsCache = {};
this.radarrServers = settings.radarr.filter((server) => server.syncEnabled); this.radarrServers = settings.radarr.filter((server) => server.syncEnabled);
this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled); this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled);
@@ -37,13 +46,53 @@ class AvailabilitySync {
const pageSize = 50; const pageSize = 50;
const userRepository = getRepository(User); const userRepository = getRepository(User);
const admin = await userRepository.findOne({
select: { id: true, plexToken: true },
where: { id: 1 },
});
if (admin) { // If it is plex admin is selected using plexToken if jellyfin admin is selected using jellyfinUserID
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
let admin = null;
if (mediaServerType === MediaServerType.PLEX) {
admin = await userRepository.findOne({
select: { id: true, plexToken: true },
where: { id: 1 },
});
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
admin = await userRepository.findOne({
where: { id: 1 },
select: [
'id',
'jellyfinAuthToken',
'jellyfinUserId',
'jellyfinDeviceId',
],
order: { id: 'ASC' },
});
}
if (mediaServerType === MediaServerType.PLEX) {
if (admin && admin.plexToken) {
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
} else {
logger.error('Plex admin is not configured.');
}
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
if (admin) {
this.jellyfinClient = new JellyfinAPI(
settings.jellyfin.hostname ?? '',
admin.jellyfinAuthToken,
admin.jellyfinDeviceId
);
this.jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
} else {
logger.error('Jellyfin admin is not configured.');
}
} else { } else {
logger.error('An admin is not configured.'); logger.error('An admin is not configured.');
} }
@@ -60,41 +109,84 @@ class AvailabilitySync {
let movieExists = false; let movieExists = false;
let movieExists4k = false; let movieExists4k = false;
const { existsInPlex } = await this.mediaExistsInPlex(media, false); // if (mediaServerType === MediaServerType.PLEX) {
const { existsInPlex: existsInPlex4k } = await this.mediaExistsInPlex( // await this.mediaExistsInPlex(media, false);
media, // } else if (
true // mediaServerType === MediaServerType.JELLYFIN ||
); // mediaServerType === MediaServerType.EMBY
// ) {
// await this.mediaExistsInJellyfin(media, false);
// }
const existsInRadarr = await this.mediaExistsInRadarr(media, false); const existsInRadarr = await this.mediaExistsInRadarr(media, false);
const existsInRadarr4k = await this.mediaExistsInRadarr(media, true); const existsInRadarr4k = await this.mediaExistsInRadarr(media, true);
if (existsInPlex || existsInRadarr) { // plex
movieExists = true; if (mediaServerType === MediaServerType.PLEX) {
logger.info( const { existsInPlex } = await this.mediaExistsInPlex(media, false);
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, const { existsInPlex: existsInPlex4k } =
{ await this.mediaExistsInPlex(media, true);
label: 'AvailabilitySync',
} if (existsInPlex || existsInRadarr) {
); movieExists = true;
logger.info(
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
label: 'AvailabilitySync',
}
);
}
if (existsInPlex4k || existsInRadarr4k) {
movieExists4k = true;
logger.info(
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
label: 'AvailabilitySync',
}
);
}
} }
if (existsInPlex4k || existsInRadarr4k) { //jellyfin
movieExists4k = true; if (
logger.info( mediaServerType === MediaServerType.JELLYFIN ||
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, mediaServerType === MediaServerType.EMBY
{ ) {
label: 'AvailabilitySync', const { existsInJellyfin } = await this.mediaExistsInJellyfin(
} media,
false
); );
const { existsInJellyfin: existsInJellyfin4k } =
await this.mediaExistsInJellyfin(media, true);
if (existsInJellyfin || existsInRadarr) {
movieExists = true;
logger.info(
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
label: 'AvailabilitySync',
}
);
}
if (existsInJellyfin4k || existsInRadarr4k) {
movieExists4k = true;
logger.info(
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
label: 'AvailabilitySync',
}
);
}
} }
if (!movieExists && media.status === MediaStatus.AVAILABLE) { if (!movieExists && media.status === MediaStatus.AVAILABLE) {
await this.mediaUpdater(media, false); await this.mediaUpdater(media, false, mediaServerType);
} }
if (!movieExists4k && media.status4k === MediaStatus.AVAILABLE) { if (!movieExists4k && media.status4k === MediaStatus.AVAILABLE) {
await this.mediaUpdater(media, true); await this.mediaUpdater(media, true, mediaServerType);
} }
} }
@@ -104,6 +196,8 @@ class AvailabilitySync {
let showExists = false; let showExists = false;
let showExists4k = false; let showExists4k = false;
//plex
const { existsInPlex, seasonsMap: plexSeasonsMap = new Map() } = const { existsInPlex, seasonsMap: plexSeasonsMap = new Map() } =
await this.mediaExistsInPlex(media, false); await this.mediaExistsInPlex(media, false);
const { const {
@@ -111,6 +205,16 @@ class AvailabilitySync {
seasonsMap: plexSeasonsMap4k = new Map(), seasonsMap: plexSeasonsMap4k = new Map(),
} = await this.mediaExistsInPlex(media, true); } = await this.mediaExistsInPlex(media, true);
//jellyfin
const {
existsInJellyfin,
seasonsMap: jellyfinSeasonsMap = new Map(),
} = await this.mediaExistsInJellyfin(media, false);
const {
existsInJellyfin: existsInJellyfin4k,
seasonsMap: jellyfinSeasonsMap4k = new Map(),
} = await this.mediaExistsInJellyfin(media, true);
const { existsInSonarr, seasonsMap: sonarrSeasonsMap } = const { existsInSonarr, seasonsMap: sonarrSeasonsMap } =
await this.mediaExistsInSonarr(media, false); await this.mediaExistsInSonarr(media, false);
const { const {
@@ -118,24 +222,60 @@ class AvailabilitySync {
seasonsMap: sonarrSeasonsMap4k, seasonsMap: sonarrSeasonsMap4k,
} = await this.mediaExistsInSonarr(media, true); } = await this.mediaExistsInSonarr(media, true);
if (existsInPlex || existsInSonarr) { //plex
showExists = true; if (mediaServerType === MediaServerType.PLEX) {
logger.info( if (existsInPlex || existsInSonarr) {
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, showExists = true;
{ logger.info(
label: 'AvailabilitySync', `The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
} {
); label: 'AvailabilitySync',
}
);
}
} }
if (existsInPlex4k || existsInSonarr4k) { if (mediaServerType === MediaServerType.PLEX) {
showExists4k = true; if (existsInPlex4k || existsInSonarr4k) {
logger.info( showExists4k = true;
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`, logger.info(
{ `The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
label: 'AvailabilitySync', {
} label: 'AvailabilitySync',
); }
);
}
}
//jellyfin
if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
if (existsInJellyfin || existsInSonarr) {
showExists = true;
logger.info(
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
label: 'AvailabilitySync',
}
);
}
}
if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
if (existsInJellyfin4k || existsInSonarr4k) {
showExists4k = true;
logger.info(
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
{
label: 'AvailabilitySync',
}
);
}
} }
// Here we will create a final map that will cross compare // Here we will create a final map that will cross compare
@@ -155,11 +295,48 @@ class AvailabilitySync {
filteredSeasonsMap.set(season.seasonNumber, false) filteredSeasonsMap.set(season.seasonNumber, false)
); );
const finalSeasons = new Map([ // non-4k
...filteredSeasonsMap, const finalSeasons: Map<number, boolean> = new Map();
...plexSeasonsMap,
...sonarrSeasonsMap, if (mediaServerType === MediaServerType.PLEX) {
]); const plexMap = new Map([
...plexSeasonsMap,
...filteredSeasonsMap,
...sonarrSeasonsMap,
]);
plexMap.forEach((value, key) => {
finalSeasons.set(key, value);
});
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
// Adding values from jellyfinSeasonsMap
jellyfinSeasonsMap.forEach((value, key) => {
finalSeasons.set(key, value);
});
// Adding values from filteredSeasonsMap and handling missing keys
filteredSeasonsMap.forEach((value, key) => {
// Check if the key is missing in jellyfinSeasonsMap
if (!finalSeasons.has(key)) {
finalSeasons.set(key, value);
}
});
// Adding values from sonarrSeasonsMap and handling missing keys
sonarrSeasonsMap.forEach((value, key) => {
// Check if the key is missing in jellyfinSeasonsMap and filteredSeasonsMap
if (!finalSeasons.has(key)) {
finalSeasons.set(key, value);
}
});
}
// ...(mediaServerType === MediaServerType.PLEX ? plexSeasonsMap : []),
// ...(mediaServerType === MediaServerType.JELLYFIN ||
// mediaServerType === MediaServerType.EMBY ? jellyfinSeasonsMap
// : []),
const filteredSeasonsMap4k: Map<number, boolean> = new Map(); const filteredSeasonsMap4k: Map<number, boolean> = new Map();
@@ -173,18 +350,74 @@ class AvailabilitySync {
filteredSeasonsMap4k.set(season.seasonNumber, false) filteredSeasonsMap4k.set(season.seasonNumber, false)
); );
const finalSeasons4k = new Map([ // const finalSeasons4k: Map<any, any> = new Map<any, any>([
...filteredSeasonsMap4k, // ...(mediaServerType === MediaServerType.PLEX
...plexSeasonsMap4k, // ? plexSeasonsMap4k
...sonarrSeasonsMap4k, // : []),
]); // ...(mediaServerType === MediaServerType.JELLYFIN ||
// mediaServerType === MediaServerType.EMBY
// ? jellyfinSeasonsMap4k
// : []),
// ...filteredSeasonsMap4k,
// ...sonarrSeasonsMap4k,
// ]);
// 4k
const finalSeasons4k: Map<number, boolean> = new Map();
if (mediaServerType === MediaServerType.PLEX) {
const plexMap4k = new Map([
...plexSeasonsMap4k,
...filteredSeasonsMap4k,
...sonarrSeasonsMap4k,
]);
plexMap4k.forEach((value, key) => {
finalSeasons4k.set(key, value);
});
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
// Adding values from jellyfinSeasonsMap
jellyfinSeasonsMap4k.forEach((value, key) => {
finalSeasons4k.set(key, value);
});
// Adding values from filteredSeasonsMap and handling missing keys
filteredSeasonsMap4k.forEach((value, key) => {
// Check if the key is missing in jellyfinSeasonsMap
if (!finalSeasons4k.has(key)) {
finalSeasons4k.set(key, value);
}
});
// Adding values from sonarrSeasonsMap and handling missing keys
sonarrSeasonsMap4k.forEach((value, key) => {
// Check if the key is missing in jellyfinSeasonsMap and filteredSeasonsMap
if (!finalSeasons4k.has(key)) {
finalSeasons4k.set(key, value);
}
});
}
// TODO: Figure out how to run seasonUpdater for each season
if ([...finalSeasons.values()].includes(false)) { if ([...finalSeasons.values()].includes(false)) {
await this.seasonUpdater(media, finalSeasons, false); await this.seasonUpdater(
media,
finalSeasons,
false,
mediaServerType
);
} }
if ([...finalSeasons4k.values()].includes(false)) { if ([...finalSeasons4k.values()].includes(false)) {
await this.seasonUpdater(media, finalSeasons4k, true); await this.seasonUpdater(
media,
finalSeasons4k,
true,
mediaServerType
);
} }
if ( if (
@@ -192,7 +425,7 @@ class AvailabilitySync {
(media.status === MediaStatus.AVAILABLE || (media.status === MediaStatus.AVAILABLE ||
media.status === MediaStatus.PARTIALLY_AVAILABLE) media.status === MediaStatus.PARTIALLY_AVAILABLE)
) { ) {
await this.mediaUpdater(media, false); await this.mediaUpdater(media, false, mediaServerType);
} }
if ( if (
@@ -200,7 +433,7 @@ class AvailabilitySync {
(media.status4k === MediaStatus.AVAILABLE || (media.status4k === MediaStatus.AVAILABLE ||
media.status4k === MediaStatus.PARTIALLY_AVAILABLE) media.status4k === MediaStatus.PARTIALLY_AVAILABLE)
) { ) {
await this.mediaUpdater(media, true); await this.mediaUpdater(media, true, mediaServerType);
} }
} }
} }
@@ -272,7 +505,11 @@ class AvailabilitySync {
return mediaStatus; return mediaStatus;
} }
private async mediaUpdater(media: Media, is4k: boolean): Promise<void> { private async mediaUpdater(
media: Media,
is4k: boolean,
mediaServerType: MediaServerType
): Promise<void> {
const mediaRepository = getRepository(Media); const mediaRepository = getRepository(Media);
const requestRepository = getRepository(MediaRequest); const requestRepository = getRepository(MediaRequest);
@@ -320,17 +557,32 @@ class AvailabilitySync {
mediaStatus === MediaStatus.PROCESSING mediaStatus === MediaStatus.PROCESSING
? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] ? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']
: null; : null;
media[is4k ? 'ratingKey4k' : 'ratingKey'] = if (mediaServerType === MediaServerType.PLEX) {
mediaStatus === MediaStatus.PROCESSING media[is4k ? 'ratingKey4k' : 'ratingKey'] =
? media[is4k ? 'ratingKey4k' : 'ratingKey'] mediaStatus === MediaStatus.PROCESSING
: null; ? media[is4k ? 'ratingKey4k' : 'ratingKey']
: undefined;
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
media[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId'] =
mediaStatus === MediaStatus.PROCESSING
? media[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId']
: undefined;
}
logger.info( logger.info(
`The ${is4k ? '4K' : 'non-4K'} ${ `The ${is4k ? '4K' : 'non-4K'} ${
media.mediaType === 'movie' ? 'movie' : 'show' media.mediaType === 'movie' ? 'movie' : 'show'
} [TMDB ID ${media.tmdbId}] was not found in any ${ } [TMDB ID ${media.tmdbId}] was not found in any ${
media.mediaType === 'movie' ? 'Radarr' : 'Sonarr' media.mediaType === 'movie' ? 'Radarr' : 'Sonarr'
} and Plex instance. Status will be changed to unknown.`, } and ${
mediaServerType === MediaServerType.PLEX
? 'plex'
: mediaServerType === MediaServerType.JELLYFIN
? 'jellyfin'
: 'emby'
} instance. Status will be changed to unknown.`,
{ label: 'AvailabilitySync' } { label: 'AvailabilitySync' }
); );
@@ -358,7 +610,8 @@ class AvailabilitySync {
private async seasonUpdater( private async seasonUpdater(
media: Media, media: Media,
seasons: Map<number, boolean>, seasons: Map<number, boolean>,
is4k: boolean is4k: boolean,
mediaServerType: MediaServerType
): Promise<void> { ): Promise<void> {
const mediaRepository = getRepository(Media); const mediaRepository = getRepository(Media);
const seasonRequestRepository = getRepository(SeasonRequest); const seasonRequestRepository = getRepository(SeasonRequest);
@@ -370,6 +623,8 @@ class AvailabilitySync {
); );
const seasonKeys = [...seasonsPendingRemoval.keys()]; const seasonKeys = [...seasonsPendingRemoval.keys()];
// let isSeasonRemoved = false;
try { try {
// Need to check and see if there are any related season // Need to check and see if there are any related season
// requests. If they are, we will need to delete them. // requests. If they are, we will need to delete them.
@@ -420,7 +675,13 @@ class AvailabilitySync {
media.tmdbId media.tmdbId
}] was not found in any ${ }] was not found in any ${
media.mediaType === 'tv' ? 'Sonarr' : 'Radarr' media.mediaType === 'tv' ? 'Sonarr' : 'Radarr'
} and Plex instance. Status will be changed to unknown.`, } and ${
mediaServerType === MediaServerType.PLEX
? 'plex'
: mediaServerType === MediaServerType.JELLYFIN
? 'jellyfin'
: 'emby'
} instance. Status will be changed to unknown.`,
{ label: 'AvailabilitySync' } { label: 'AvailabilitySync' }
); );
} catch (ex) { } catch (ex) {
@@ -604,6 +865,7 @@ class AvailabilitySync {
return seasonExists; return seasonExists;
} }
// Plex
private async mediaExistsInPlex( private async mediaExistsInPlex(
media: Media, media: Media,
is4k: boolean is4k: boolean
@@ -719,6 +981,123 @@ class AvailabilitySync {
return seasonExistsInPlex; return seasonExistsInPlex;
} }
// Jellyfin
private async mediaExistsInJellyfin(
media: Media,
is4k: boolean
): Promise<{ existsInJellyfin: boolean; seasonsMap?: Map<number, boolean> }> {
const ratingKey = media.jellyfinMediaId;
const ratingKey4k = media.jellyfinMediaId4k;
let existsInJellyfin = false;
let preventSeasonSearch = false;
// Check each jellyfin instance to see if the media still exists
// If found, we will assume the media exists and prevent removal
// We can use the cache we built when we fetched the series with mediaExistsInJellyfin
try {
let jellyfinMedia: JellyfinLibraryItem | undefined;
if (ratingKey && !is4k) {
jellyfinMedia = await this.jellyfinClient?.getItemData(ratingKey);
if (media.mediaType === 'tv' && jellyfinMedia !== undefined) {
this.jellyfinSeasonsCache[ratingKey] =
await this.jellyfinClient?.getSeasons(ratingKey);
}
}
if (ratingKey4k && is4k) {
jellyfinMedia = await this.jellyfinClient?.getItemData(ratingKey4k);
if (media.mediaType === 'tv' && jellyfinMedia !== undefined) {
this.jellyfinSeasonsCache[ratingKey4k] =
await this.jellyfinClient?.getSeasons(ratingKey4k);
}
}
if (jellyfinMedia) {
existsInJellyfin = true;
}
} catch (ex) {
if (!ex.message.includes('404' || '500')) {
existsInJellyfin = false;
preventSeasonSearch = true;
logger.debug(
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${
media.mediaType === 'tv' ? 'show' : 'movie'
} [TMDB ID ${media.tmdbId}] from Jellyfin.`,
{
errorMessage: ex.message,
label: 'AvailabilitySync',
}
);
}
}
// Here we check each season in jellyfin for availability
// If the API returns an error other than a 404,
// we will have to prevent the season check from happening
if (media.mediaType === 'tv') {
const seasonsMap: Map<number, boolean> = new Map();
if (!preventSeasonSearch) {
const filteredSeasons = media.seasons.filter(
(season) =>
season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE ||
season[is4k ? 'status4k' : 'status'] ===
MediaStatus.PARTIALLY_AVAILABLE
);
for (const season of filteredSeasons) {
const seasonExists = await this.seasonExistsInJellyfin(
media,
season,
is4k
);
if (seasonExists) {
seasonsMap.set(season.seasonNumber, true);
}
}
}
return { existsInJellyfin, seasonsMap };
}
return { existsInJellyfin };
}
private async seasonExistsInJellyfin(
media: Media,
season: Season,
is4k: boolean
): Promise<boolean> {
const ratingKey = media.jellyfinMediaId;
const ratingKey4k = media.jellyfinMediaId4k;
let seasonExistsInJellyfin = false;
// Check each jellyfin instance to see if the season exists
let jellyfinSeasons: JellyfinLibraryItem[] | undefined;
if (ratingKey && !is4k) {
jellyfinSeasons = this.jellyfinSeasonsCache[ratingKey];
}
if (ratingKey4k && is4k) {
jellyfinSeasons = this.jellyfinSeasonsCache[ratingKey4k];
}
const seasonIsAvailable = jellyfinSeasons?.find(
(jellyfinSeason) => jellyfinSeason.IndexNumber === season.seasonNumber
);
if (seasonIsAvailable) {
seasonExistsInJellyfin = true;
}
return seasonExistsInJellyfin;
}
} }
const availabilitySync = new AvailabilitySync(); const availabilitySync = new AvailabilitySync();

View File

@@ -62,7 +62,7 @@ class JellyfinScanner {
const metadata = await this.jfClient.getItemData(jellyfinitem.Id); const metadata = await this.jfClient.getItemData(jellyfinitem.Id);
const newMedia = new Media(); const newMedia = new Media();
if (!metadata.Id) { if (!metadata?.Id) {
logger.debug('No Id metadata for this title. Skipping', { logger.debug('No Id metadata for this title. Skipping', {
label: 'Plex Sync', label: 'Plex Sync',
ratingKey: jellyfinitem.Id, ratingKey: jellyfinitem.Id,
@@ -197,6 +197,14 @@ class JellyfinScanner {
jellyfinitem.SeriesId ?? jellyfinitem.SeasonId ?? jellyfinitem.Id; jellyfinitem.SeriesId ?? jellyfinitem.SeasonId ?? jellyfinitem.Id;
const metadata = await this.jfClient.getItemData(Id); const metadata = await this.jfClient.getItemData(Id);
if (!metadata?.Id) {
logger.debug('No Id metadata for this title. Skipping', {
label: 'Plex Sync',
ratingKey: jellyfinitem.Id,
});
return;
}
if (metadata.ProviderIds.Tvdb) { if (metadata.ProviderIds.Tvdb) {
tvShow = await this.tmdb.getShowByTvdbId({ tvShow = await this.tmdb.getShowByTvdbId({
tvdbId: Number(metadata.ProviderIds.Tvdb), tvdbId: Number(metadata.ProviderIds.Tvdb),
@@ -275,7 +283,7 @@ class JellyfinScanner {
episode.Id episode.Id
); );
ExtendedEpisodeData.MediaSources?.some((MediaSource) => { ExtendedEpisodeData?.MediaSources?.some((MediaSource) => {
return MediaSource.MediaStreams.some((MediaStream) => { return MediaSource.MediaStreams.some((MediaStream) => {
if (MediaStream.Type === 'Video') { if (MediaStream.Type === 'Video') {
if ((MediaStream.Width ?? 0) >= 2000) { if ((MediaStream.Width ?? 0) >= 2000) {