mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
fix: availability sync file detection (#3371)
* fix: added extra check for unmonitored movies in radarr * feat: created new radarr/sonarr routes to grab existing series data * refactor: updated job routes to check by external service id * fix: season check will now also look at episode file count
This commit is contained in:
@@ -76,6 +76,15 @@ export interface SonarrSeries {
|
|||||||
ignoreEpisodesWithoutFiles?: boolean;
|
ignoreEpisodesWithoutFiles?: boolean;
|
||||||
searchForMissingEpisodes?: boolean;
|
searchForMissingEpisodes?: boolean;
|
||||||
};
|
};
|
||||||
|
statistics: {
|
||||||
|
seasonCount: number;
|
||||||
|
episodeFileCount: number;
|
||||||
|
episodeCount: number;
|
||||||
|
totalEpisodeCount: number;
|
||||||
|
sizeOnDisk: number;
|
||||||
|
releaseGroups: string[];
|
||||||
|
percentOfEpisodes: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddSeriesOptions {
|
export interface AddSeriesOptions {
|
||||||
@@ -116,6 +125,16 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getSeriesById(id: number): Promise<SonarrSeries> {
|
||||||
|
try {
|
||||||
|
const response = await this.axios.get<SonarrSeries>(`/series/${id}`);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`[Sonarr] Failed to retrieve series by ID: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
|
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
|
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
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 type { RadarrMovie } from '@server/api/servarr/radarr';
|
||||||
import RadarrAPI from '@server/api/servarr/radarr';
|
import RadarrAPI from '@server/api/servarr/radarr';
|
||||||
import type { SonarrSeason } 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 { MediaStatus } from '@server/constants/media';
|
import { MediaStatus } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
@@ -47,158 +48,150 @@ class AvailabilitySync {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const media of this.loadAvailableMediaPaginated(pageSize)) {
|
for await (const media of this.loadAvailableMediaPaginated(pageSize)) {
|
||||||
try {
|
if (!this.running) {
|
||||||
if (!this.running) {
|
throw new Error('Job aborted');
|
||||||
throw new Error('Job aborted');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const mediaExists = await this.mediaExists(media);
|
const mediaExists = await this.mediaExists(media);
|
||||||
|
|
||||||
//We can not delete media so if both versions do not exist, we will change both columns to unknown or null
|
// We can not delete media so if both versions do not exist, we will change both columns to unknown or null
|
||||||
if (!mediaExists) {
|
if (!mediaExists) {
|
||||||
if (
|
if (
|
||||||
media.status !== MediaStatus.UNKNOWN ||
|
media.status !== MediaStatus.UNKNOWN ||
|
||||||
media.status4k !== MediaStatus.UNKNOWN
|
media.status4k !== MediaStatus.UNKNOWN
|
||||||
) {
|
) {
|
||||||
const request = await requestRepository.find({
|
const request = await requestRepository.find({
|
||||||
relations: {
|
relations: {
|
||||||
media: true,
|
media: true,
|
||||||
},
|
},
|
||||||
where: { media: { id: media.id } },
|
where: { media: { id: media.id } },
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`${
|
|
||||||
media.mediaType === 'tv' ? media.tvdbId : media.tmdbId
|
|
||||||
} does not exist in any of your media instances. We will change its status to unknown.`,
|
|
||||||
{ label: 'AvailabilitySync' }
|
|
||||||
);
|
|
||||||
|
|
||||||
await mediaRepository.update(media.id, {
|
|
||||||
status: MediaStatus.UNKNOWN,
|
|
||||||
status4k: MediaStatus.UNKNOWN,
|
|
||||||
serviceId: null,
|
|
||||||
serviceId4k: null,
|
|
||||||
externalServiceId: null,
|
|
||||||
externalServiceId4k: null,
|
|
||||||
externalServiceSlug: null,
|
|
||||||
externalServiceSlug4k: null,
|
|
||||||
ratingKey: null,
|
|
||||||
ratingKey4k: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
await requestRepository.remove(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media.mediaType === 'tv') {
|
|
||||||
// ok, the show itself exists, but do all it's seasons?
|
|
||||||
const seasons = await seasonRepository.find({
|
|
||||||
where: [
|
|
||||||
{ status: MediaStatus.AVAILABLE, media: { id: media.id } },
|
|
||||||
{
|
|
||||||
status: MediaStatus.PARTIALLY_AVAILABLE,
|
|
||||||
media: { id: media.id },
|
|
||||||
},
|
|
||||||
{ status4k: MediaStatus.AVAILABLE, media: { id: media.id } },
|
|
||||||
{
|
|
||||||
status4k: MediaStatus.PARTIALLY_AVAILABLE,
|
|
||||||
media: { id: media.id },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let didDeleteSeasons = false;
|
logger.info(
|
||||||
for (const season of seasons) {
|
`Media ID ${media.id} does not exist in any of your media instances. Status will be changed to unknown.`,
|
||||||
if (
|
{ label: 'AvailabilitySync' }
|
||||||
!mediaExists &&
|
);
|
||||||
(season.status !== MediaStatus.UNKNOWN ||
|
|
||||||
season.status4k !== MediaStatus.UNKNOWN)
|
await mediaRepository.update(media.id, {
|
||||||
) {
|
status: MediaStatus.UNKNOWN,
|
||||||
await seasonRepository.update(
|
status4k: MediaStatus.UNKNOWN,
|
||||||
{ id: season.id },
|
serviceId: null,
|
||||||
|
serviceId4k: null,
|
||||||
|
externalServiceId: null,
|
||||||
|
externalServiceId4k: null,
|
||||||
|
externalServiceSlug: null,
|
||||||
|
externalServiceSlug4k: null,
|
||||||
|
ratingKey: null,
|
||||||
|
ratingKey4k: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await requestRepository.remove(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.mediaType === 'tv') {
|
||||||
|
// ok, the show itself exists, but do all it's seasons?
|
||||||
|
const seasons = await seasonRepository.find({
|
||||||
|
where: [
|
||||||
|
{ status: MediaStatus.AVAILABLE, media: { id: media.id } },
|
||||||
|
{
|
||||||
|
status: MediaStatus.PARTIALLY_AVAILABLE,
|
||||||
|
media: { id: media.id },
|
||||||
|
},
|
||||||
|
{ status4k: MediaStatus.AVAILABLE, media: { id: media.id } },
|
||||||
|
{
|
||||||
|
status4k: MediaStatus.PARTIALLY_AVAILABLE,
|
||||||
|
media: { id: media.id },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let didDeleteSeasons = false;
|
||||||
|
for (const season of seasons) {
|
||||||
|
if (
|
||||||
|
!mediaExists &&
|
||||||
|
(season.status !== MediaStatus.UNKNOWN ||
|
||||||
|
season.status4k !== MediaStatus.UNKNOWN)
|
||||||
|
) {
|
||||||
|
await seasonRepository.update(
|
||||||
|
{ id: season.id },
|
||||||
|
{
|
||||||
|
status: MediaStatus.UNKNOWN,
|
||||||
|
status4k: MediaStatus.UNKNOWN,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const seasonExists = await this.seasonExists(media, season);
|
||||||
|
|
||||||
|
if (!seasonExists) {
|
||||||
|
logger.info(
|
||||||
|
`Removing season ${season.seasonNumber}, media ID ${media.id} because it does not exist in any of your media instances.`,
|
||||||
|
{ label: 'AvailabilitySync' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
season.status !== MediaStatus.UNKNOWN ||
|
||||||
|
season.status4k !== MediaStatus.UNKNOWN
|
||||||
|
) {
|
||||||
|
await seasonRepository.update(
|
||||||
|
{ id: season.id },
|
||||||
|
{
|
||||||
|
status: MediaStatus.UNKNOWN,
|
||||||
|
status4k: MediaStatus.UNKNOWN,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seasonToBeDeleted = await seasonRequestRepository.findOne(
|
||||||
{
|
{
|
||||||
status: MediaStatus.UNKNOWN,
|
relations: {
|
||||||
status4k: MediaStatus.UNKNOWN,
|
request: {
|
||||||
|
media: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
request: {
|
||||||
|
media: {
|
||||||
|
id: media.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seasonNumber: season.seasonNumber,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
const seasonExists = await this.seasonExists(media, season);
|
|
||||||
|
|
||||||
if (!seasonExists) {
|
if (seasonToBeDeleted) {
|
||||||
logger.info(
|
await seasonRequestRepository.remove(seasonToBeDeleted);
|
||||||
`Removing season ${season.seasonNumber}, media id: ${media.tvdbId} because it does not exist in any of your media instances.`,
|
|
||||||
{ label: 'AvailabilitySync' }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
season.status !== MediaStatus.UNKNOWN ||
|
|
||||||
season.status4k !== MediaStatus.UNKNOWN
|
|
||||||
) {
|
|
||||||
await seasonRepository.update(
|
|
||||||
{ id: season.id },
|
|
||||||
{
|
|
||||||
status: MediaStatus.UNKNOWN,
|
|
||||||
status4k: MediaStatus.UNKNOWN,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const seasonToBeDeleted =
|
|
||||||
await seasonRequestRepository.findOne({
|
|
||||||
relations: {
|
|
||||||
request: {
|
|
||||||
media: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
request: {
|
|
||||||
media: {
|
|
||||||
id: media.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
seasonNumber: season.seasonNumber,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (seasonToBeDeleted) {
|
|
||||||
await seasonRequestRepository.remove(seasonToBeDeleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
didDeleteSeasons = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
didDeleteSeasons = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (didDeleteSeasons) {
|
if (didDeleteSeasons) {
|
||||||
if (
|
if (
|
||||||
media.status === MediaStatus.AVAILABLE ||
|
media.status === MediaStatus.AVAILABLE ||
|
||||||
media.status4k === MediaStatus.AVAILABLE
|
media.status4k === MediaStatus.AVAILABLE
|
||||||
) {
|
) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Marking media id: ${media.tvdbId} as PARTIALLY_AVAILABLE because we deleted some of its seasons.`,
|
`Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (media.status === MediaStatus.AVAILABLE) {
|
if (media.status === MediaStatus.AVAILABLE) {
|
||||||
await mediaRepository.update(media.id, {
|
await mediaRepository.update(media.id, {
|
||||||
status: MediaStatus.PARTIALLY_AVAILABLE,
|
status: MediaStatus.PARTIALLY_AVAILABLE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.status4k === MediaStatus.AVAILABLE) {
|
if (media.status4k === MediaStatus.AVAILABLE) {
|
||||||
await mediaRepository.update(media.id, {
|
await mediaRepository.update(media.id, {
|
||||||
status4k: MediaStatus.PARTIALLY_AVAILABLE,
|
status4k: MediaStatus.PARTIALLY_AVAILABLE,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
|
||||||
logger.error('Failure with media.', {
|
|
||||||
errorMessage: ex.message,
|
|
||||||
label: 'AvailabilitySync',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -254,9 +247,9 @@ class AvailabilitySync {
|
|||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`${media.tmdbId} does not exist in your ${is4k ? '4k' : 'non-4k'} ${
|
`Media ID ${media.id} does not exist in your ${is4k ? '4k' : 'non-4k'} ${
|
||||||
isTVType ? 'sonarr' : 'radarr'
|
isTVType ? 'Sonarr' : 'Radarr'
|
||||||
} and plex instance. We will change its status to unknown.`,
|
} and Plex instance. Status will be changed to unknown.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -306,46 +299,70 @@ class AvailabilitySync {
|
|||||||
apiKey: server.apiKey,
|
apiKey: server.apiKey,
|
||||||
url: RadarrAPI.buildUrl(server, '/api/v3'),
|
url: RadarrAPI.buildUrl(server, '/api/v3'),
|
||||||
});
|
});
|
||||||
const meta = await api.getMovieByTmdbId(media.tmdbId);
|
try {
|
||||||
|
// Check if both exist or if a single non-4k or 4k exists
|
||||||
|
// If both do not exist we will return false
|
||||||
|
|
||||||
//check if both exist or if a single non-4k or 4k exists
|
let meta: RadarrMovie | undefined;
|
||||||
//if both do not exist we will return false
|
|
||||||
if (!server.is4k && !meta.id) {
|
|
||||||
existsInRadarr = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (server.is4k && !meta.id) {
|
if (!server.is4k && media.externalServiceId) {
|
||||||
existsInRadarr4k = false;
|
meta = await api.getMovie({ id: media.externalServiceId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k && media.externalServiceId4k) {
|
||||||
|
meta = await api.getMovie({ id: media.externalServiceId4k });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!server.is4k && (!meta || !meta.hasFile)) {
|
||||||
|
existsInRadarr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k && (!meta || !meta.hasFile)) {
|
||||||
|
existsInRadarr4k = false;
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
logger.debug(
|
||||||
|
`Failure retrieving media ID ${media.id} from your ${
|
||||||
|
!server.is4k ? 'non-4K' : '4K'
|
||||||
|
} Radarr.`,
|
||||||
|
{
|
||||||
|
errorMessage: ex.message,
|
||||||
|
label: 'AvailabilitySync',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!server.is4k) {
|
||||||
|
existsInRadarr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k) {
|
||||||
|
existsInRadarr4k = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsInRadarr && existsInRadarr4k) {
|
// If only a single non-4k or 4k exists, then change entity columns accordingly
|
||||||
return true;
|
// Related media request will then be deleted
|
||||||
}
|
if (
|
||||||
|
!existsInRadarr &&
|
||||||
if (!existsInRadarr && existsInPlex) {
|
(existsInRadarr4k || existsInPlex4k) &&
|
||||||
return true;
|
!existsInPlex
|
||||||
}
|
) {
|
||||||
|
|
||||||
if (!existsInRadarr4k && existsInPlex4k) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//if only a single non-4k or 4k exists, then change entity columns accordingly
|
|
||||||
//related media request will then be deleted
|
|
||||||
if (!existsInRadarr && existsInRadarr4k && !existsInPlex) {
|
|
||||||
if (media.status !== MediaStatus.UNKNOWN) {
|
if (media.status !== MediaStatus.UNKNOWN) {
|
||||||
this.mediaUpdater(media, false);
|
this.mediaUpdater(media, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsInRadarr && !existsInRadarr4k && !existsInPlex4k) {
|
if (
|
||||||
|
(existsInRadarr || existsInPlex) &&
|
||||||
|
!existsInRadarr4k &&
|
||||||
|
!existsInPlex4k
|
||||||
|
) {
|
||||||
if (media.status4k !== MediaStatus.UNKNOWN) {
|
if (media.status4k !== MediaStatus.UNKNOWN) {
|
||||||
this.mediaUpdater(media, true);
|
this.mediaUpdater(media, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsInRadarr || existsInRadarr4k) {
|
if (existsInRadarr || existsInRadarr4k || existsInPlex || existsInPlex4k) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,10 +374,6 @@ class AvailabilitySync {
|
|||||||
existsInPlex: boolean,
|
existsInPlex: boolean,
|
||||||
existsInPlex4k: boolean
|
existsInPlex4k: boolean
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!media.tvdbId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let existsInSonarr = true;
|
let existsInSonarr = true;
|
||||||
let existsInSonarr4k = true;
|
let existsInSonarr4k = true;
|
||||||
|
|
||||||
@@ -369,49 +382,75 @@ class AvailabilitySync {
|
|||||||
apiKey: server.apiKey,
|
apiKey: server.apiKey,
|
||||||
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
|
// Check if both exist or if a single non-4k or 4k exists
|
||||||
|
// If both do not exist we will return false
|
||||||
|
|
||||||
const meta = await api.getSeriesByTvdbId(media.tvdbId);
|
let meta: SonarrSeries | undefined;
|
||||||
|
|
||||||
this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] = meta.seasons;
|
if (!server.is4k && media.externalServiceId) {
|
||||||
|
meta = await api.getSeriesById(media.externalServiceId);
|
||||||
|
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
|
||||||
|
meta.seasons;
|
||||||
|
}
|
||||||
|
|
||||||
//check if both exist or if a single non-4k or 4k exists
|
if (server.is4k && media.externalServiceId4k) {
|
||||||
//if both do not exist we will return false
|
meta = await api.getSeriesById(media.externalServiceId4k);
|
||||||
if (!server.is4k && !meta.id) {
|
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] =
|
||||||
existsInSonarr = false;
|
meta.seasons;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.is4k && !meta.id) {
|
if (!server.is4k && (!meta || meta.statistics.episodeFileCount === 0)) {
|
||||||
existsInSonarr4k = false;
|
existsInSonarr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k && (!meta || meta.statistics.episodeFileCount === 0)) {
|
||||||
|
existsInSonarr4k = false;
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
logger.debug(
|
||||||
|
`Failure retrieving media ID ${media.id} from your ${
|
||||||
|
!server.is4k ? 'non-4K' : '4K'
|
||||||
|
} Sonarr.`,
|
||||||
|
{
|
||||||
|
errorMessage: ex.message,
|
||||||
|
label: 'AvailabilitySync',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!server.is4k) {
|
||||||
|
existsInSonarr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k) {
|
||||||
|
existsInSonarr4k = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsInSonarr && existsInSonarr4k) {
|
// If only a single non-4k or 4k exists, then change entity columns accordingly
|
||||||
return true;
|
// Related media request will then be deleted
|
||||||
}
|
if (
|
||||||
|
!existsInSonarr &&
|
||||||
if (!existsInSonarr && existsInPlex) {
|
(existsInSonarr4k || existsInPlex4k) &&
|
||||||
return true;
|
!existsInPlex
|
||||||
}
|
) {
|
||||||
|
|
||||||
if (!existsInSonarr4k && existsInPlex4k) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//if only a single non-4k or 4k exists, then change entity columns accordingly
|
|
||||||
//related media request will then be deleted
|
|
||||||
if (!existsInSonarr && existsInSonarr4k && !existsInPlex) {
|
|
||||||
if (media.status !== MediaStatus.UNKNOWN) {
|
if (media.status !== MediaStatus.UNKNOWN) {
|
||||||
this.mediaUpdater(media, false);
|
this.mediaUpdater(media, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsInSonarr && !existsInSonarr4k && !existsInPlex4k) {
|
if (
|
||||||
|
(existsInSonarr || existsInPlex) &&
|
||||||
|
!existsInSonarr4k &&
|
||||||
|
!existsInPlex4k
|
||||||
|
) {
|
||||||
if (media.status4k !== MediaStatus.UNKNOWN) {
|
if (media.status4k !== MediaStatus.UNKNOWN) {
|
||||||
this.mediaUpdater(media, true);
|
this.mediaUpdater(media, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsInSonarr || existsInSonarr4k) {
|
if (existsInSonarr || existsInSonarr4k || existsInPlex || existsInPlex4k) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,10 +463,6 @@ class AvailabilitySync {
|
|||||||
seasonExistsInPlex: boolean,
|
seasonExistsInPlex: boolean,
|
||||||
seasonExistsInPlex4k: boolean
|
seasonExistsInPlex4k: boolean
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!media.tvdbId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let seasonExistsInSonarr = true;
|
let seasonExistsInSonarr = true;
|
||||||
let seasonExistsInSonarr4k = true;
|
let seasonExistsInSonarr4k = true;
|
||||||
|
|
||||||
@@ -441,35 +476,67 @@ class AvailabilitySync {
|
|||||||
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const seasons =
|
try {
|
||||||
this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] ??
|
// Here we can use the cache we built when we fetched the series with mediaExistsInSonarr
|
||||||
(await api.getSeriesByTvdbId(media.tvdbId)).seasons;
|
// If the cache does not have data, we will fetch with the api route
|
||||||
this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] = seasons;
|
|
||||||
|
|
||||||
const hasMonitoredSeason = seasons.find(
|
let seasons: SonarrSeason[] =
|
||||||
({ monitored, seasonNumber }) =>
|
this.sonarrSeasonsCache[
|
||||||
monitored && season.seasonNumber === seasonNumber
|
`${server.id}-${
|
||||||
);
|
!server.is4k ? media.externalServiceId : media.externalServiceId4k
|
||||||
|
}`
|
||||||
|
];
|
||||||
|
|
||||||
if (!server.is4k && !hasMonitoredSeason) {
|
if (!server.is4k && media.externalServiceId) {
|
||||||
seasonExistsInSonarr = false;
|
seasons =
|
||||||
|
this.sonarrSeasonsCache[
|
||||||
|
`${server.id}-${media.externalServiceId}`
|
||||||
|
] ?? (await api.getSeriesById(media.externalServiceId)).seasons;
|
||||||
|
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
|
||||||
|
seasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k && media.externalServiceId4k) {
|
||||||
|
seasons =
|
||||||
|
this.sonarrSeasonsCache[
|
||||||
|
`${server.id}-${media.externalServiceId4k}`
|
||||||
|
] ?? (await api.getSeriesById(media.externalServiceId4k)).seasons;
|
||||||
|
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] =
|
||||||
|
seasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seasonIsUnavailable = seasons?.find(
|
||||||
|
({ seasonNumber, statistics }) =>
|
||||||
|
season.seasonNumber === seasonNumber &&
|
||||||
|
statistics?.episodeFileCount === 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!server.is4k && seasonIsUnavailable) {
|
||||||
|
seasonExistsInSonarr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k && seasonIsUnavailable) {
|
||||||
|
seasonExistsInSonarr4k = false;
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
logger.debug(
|
||||||
|
`Failure retrieving media ID ${media.id} from your ${
|
||||||
|
!server.is4k ? 'non-4K' : '4K'
|
||||||
|
} Sonarr.`,
|
||||||
|
{
|
||||||
|
errorMessage: ex.message,
|
||||||
|
label: 'AvailabilitySync',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!server.is4k) {
|
||||||
|
seasonExistsInSonarr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k) {
|
||||||
|
seasonExistsInSonarr4k = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.is4k && !hasMonitoredSeason) {
|
|
||||||
seasonExistsInSonarr4k = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seasonExistsInSonarr && seasonExistsInSonarr4k) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!seasonExistsInSonarr && seasonExistsInPlex) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!seasonExistsInSonarr4k && seasonExistsInPlex4k) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const seasonToBeDeleted = await seasonRequestRepository.findOne({
|
const seasonToBeDeleted = await seasonRequestRepository.findOne({
|
||||||
@@ -489,16 +556,16 @@ class AvailabilitySync {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
//if season does not exist, we will change status to unknown and delete related season request
|
// If season does not exist, we will change status to unknown and delete related season request
|
||||||
//if parent media request is empty(all related seasons have been removed), parent is automatically deleted
|
// If parent media request is empty(all related seasons have been removed), parent is automatically deleted
|
||||||
if (
|
if (
|
||||||
!seasonExistsInSonarr &&
|
!seasonExistsInSonarr &&
|
||||||
seasonExistsInSonarr4k &&
|
(seasonExistsInSonarr4k || seasonExistsInPlex4k) &&
|
||||||
!seasonExistsInPlex
|
!seasonExistsInPlex
|
||||||
) {
|
) {
|
||||||
if (season.status !== MediaStatus.UNKNOWN) {
|
if (season.status !== MediaStatus.UNKNOWN) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`${media.tvdbId}, season: ${season.seasonNumber} does not exist in your non-4k sonarr and plex instance. We will change its status to unknown.`,
|
`Season ${season.seasonNumber}, media ID ${media.id} does not exist in your non-4k Sonarr and Plex instance. Status will be changed to unknown.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
await seasonRepository.update(season.id, {
|
await seasonRepository.update(season.id, {
|
||||||
@@ -511,7 +578,7 @@ class AvailabilitySync {
|
|||||||
|
|
||||||
if (media.status === MediaStatus.AVAILABLE) {
|
if (media.status === MediaStatus.AVAILABLE) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Marking media id: ${media.tvdbId} as PARTIALLY_AVAILABLE because we deleted one of its seasons.`,
|
`Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
await mediaRepository.update(media.id, {
|
await mediaRepository.update(media.id, {
|
||||||
@@ -522,13 +589,13 @@ class AvailabilitySync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
seasonExistsInSonarr &&
|
(seasonExistsInSonarr || seasonExistsInPlex) &&
|
||||||
!seasonExistsInSonarr4k &&
|
!seasonExistsInSonarr4k &&
|
||||||
!seasonExistsInPlex4k
|
!seasonExistsInPlex4k
|
||||||
) {
|
) {
|
||||||
if (season.status4k !== MediaStatus.UNKNOWN) {
|
if (season.status4k !== MediaStatus.UNKNOWN) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`${media.tvdbId}, season: ${season.seasonNumber} does not exist in your 4k sonarr and plex instance. We will change its status to unknown.`,
|
`Season ${season.seasonNumber}, media ID ${media.id} does not exist in your 4k Sonarr and Plex instance. Status will be changed to unknown.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
await seasonRepository.update(season.id, {
|
await seasonRepository.update(season.id, {
|
||||||
@@ -541,7 +608,7 @@ class AvailabilitySync {
|
|||||||
|
|
||||||
if (media.status4k === MediaStatus.AVAILABLE) {
|
if (media.status4k === MediaStatus.AVAILABLE) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Marking media id: ${media.tvdbId} as PARTIALLY_AVAILABLE because we deleted one of its seasons.`,
|
`Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
await mediaRepository.update(media.id, {
|
await mediaRepository.update(media.id, {
|
||||||
@@ -551,7 +618,12 @@ class AvailabilitySync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seasonExistsInSonarr || seasonExistsInSonarr4k) {
|
if (
|
||||||
|
seasonExistsInSonarr ||
|
||||||
|
seasonExistsInSonarr4k ||
|
||||||
|
seasonExistsInPlex ||
|
||||||
|
seasonExistsInPlex4k
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,7 +637,7 @@ class AvailabilitySync {
|
|||||||
let existsInPlex = false;
|
let existsInPlex = false;
|
||||||
let existsInPlex4k = false;
|
let existsInPlex4k = false;
|
||||||
|
|
||||||
//check each plex instance to see if media exists
|
// Check each plex instance to see if media exists
|
||||||
try {
|
try {
|
||||||
if (ratingKey) {
|
if (ratingKey) {
|
||||||
const meta = await this.plexClient?.getMetadata(ratingKey);
|
const meta = await this.plexClient?.getMetadata(ratingKey);
|
||||||
@@ -573,6 +645,7 @@ class AvailabilitySync {
|
|||||||
existsInPlex = true;
|
existsInPlex = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ratingKey4k) {
|
if (ratingKey4k) {
|
||||||
const meta4k = await this.plexClient?.getMetadata(ratingKey4k);
|
const meta4k = await this.plexClient?.getMetadata(ratingKey4k);
|
||||||
if (meta4k) {
|
if (meta4k) {
|
||||||
@@ -580,18 +653,17 @@ class AvailabilitySync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
// TODO: oof, not the nicest way of handling this, but plex-api does not leave us with any other options...
|
|
||||||
if (!ex.message.includes('response code: 404')) {
|
if (!ex.message.includes('response code: 404')) {
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//base case for if both media versions exist in plex
|
// Base case if both media versions exist in plex
|
||||||
if (existsInPlex && existsInPlex4k) {
|
if (existsInPlex && existsInPlex4k) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
//we then check radarr or sonarr has that specific media. If not, then we will move to delete
|
// We then check radarr or sonarr has that specific media. If not, then we will move to delete
|
||||||
//if a non-4k or 4k version exists in at least one of the instances, we will only update that specific version
|
// If a non-4k or 4k version exists in at least one of the instances, we will only update that specific version
|
||||||
if (media.mediaType === 'movie') {
|
if (media.mediaType === 'movie') {
|
||||||
const existsInRadarr = await this.mediaExistsInRadarr(
|
const existsInRadarr = await this.mediaExistsInRadarr(
|
||||||
media,
|
media,
|
||||||
@@ -599,10 +671,10 @@ class AvailabilitySync {
|
|||||||
existsInPlex4k
|
existsInPlex4k
|
||||||
);
|
);
|
||||||
|
|
||||||
//if true, media exists in at least one radarr or plex instance.
|
// If true, media exists in at least one radarr or plex instance.
|
||||||
if (existsInRadarr) {
|
if (existsInRadarr) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`${media.tmdbId} exists in at least one radarr or plex instance. Media will be updated if set to available.`,
|
`${media.id} exists in at least one Radarr or Plex instance. Media will be updated if set to available.`,
|
||||||
{
|
{
|
||||||
label: 'AvailabilitySync',
|
label: 'AvailabilitySync',
|
||||||
}
|
}
|
||||||
@@ -619,10 +691,10 @@ class AvailabilitySync {
|
|||||||
existsInPlex4k
|
existsInPlex4k
|
||||||
);
|
);
|
||||||
|
|
||||||
//if true, media exists in at least one sonarr or plex instance.
|
// If true, media exists in at least one sonarr or plex instance.
|
||||||
if (existsInSonarr) {
|
if (existsInSonarr) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`${media.tvdbId} exists in at least one sonarr or plex instance. Media will be updated if set to available.`,
|
`${media.id} exists in at least one Sonarr or Plex instance. Media will be updated if set to available.`,
|
||||||
{
|
{
|
||||||
label: 'AvailabilitySync',
|
label: 'AvailabilitySync',
|
||||||
}
|
}
|
||||||
@@ -672,7 +744,7 @@ class AvailabilitySync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//base case for if both season versions exist in plex
|
// Base case if both season versions exist in plex
|
||||||
if (seasonExistsInPlex && seasonExistsInPlex4k) {
|
if (seasonExistsInPlex && seasonExistsInPlex4k) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -686,7 +758,7 @@ class AvailabilitySync {
|
|||||||
|
|
||||||
if (existsInSonarr) {
|
if (existsInSonarr) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`${media.tvdbId}, season: ${season.seasonNumber} exists in at least one sonarr or plex instance. Media will be updated if set to available.`,
|
`Season ${season.seasonNumber}, media ID ${media.id} exists in at least one Sonarr or Plex instance. Media will be updated if set to available.`,
|
||||||
{
|
{
|
||||||
label: 'AvailabilitySync',
|
label: 'AvailabilitySync',
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user