mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
fix: availability sync requests (#3460)
* fix: modified media status handling when media has been deleted fix: requests will now be updated to completed on scan fix: modified components to display deleted as a status fix: corrected media status switching away from deleted fix: modified components to display deleted as a status fix: corrected media status switching away from deleted fix: base scanner will set requests to completed correctly fix: mark available button correctly sets requests as completed fix: status will now stay deleted after declined request refactor: request completion handling moved to entity fix: prevented notifications from sending to old deleted requests refactor: cleaned up code and added more detail to logs refactor: updated to reflect latest availability sync changes * fix: fetch requests only if necessary in db and remove unneeded code * fix: update request button logic to accomodate specials fix: remove completed filtering in tv details * fix: correctly set seasons status when using the manual button * refactor: improve reliability of season request completion refactor: remove seasonrequest code * fix: send notification for 4k movies fix: same for shows * feat: add completed filter to requests list refactor: correct label
This commit is contained in:
@@ -8,7 +8,6 @@ import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import MediaRequest from '@server/entity/MediaRequest';
|
||||
import type Season from '@server/entity/Season';
|
||||
import SeasonRequest from '@server/entity/SeasonRequest';
|
||||
import { User } from '@server/entity/User';
|
||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
@@ -32,7 +31,7 @@ class AvailabilitySync {
|
||||
|
||||
try {
|
||||
logger.info(`Starting availability sync...`, {
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
});
|
||||
const pageSize = 50;
|
||||
|
||||
@@ -74,7 +73,7 @@ class AvailabilitySync {
|
||||
logger.info(
|
||||
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
{
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -84,7 +83,7 @@ class AvailabilitySync {
|
||||
logger.info(
|
||||
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
{
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -123,7 +122,7 @@ class AvailabilitySync {
|
||||
logger.info(
|
||||
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
{
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -133,7 +132,7 @@ class AvailabilitySync {
|
||||
logger.info(
|
||||
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
{
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -207,11 +206,11 @@ class AvailabilitySync {
|
||||
} catch (ex) {
|
||||
logger.error('Failed to complete availability sync.', {
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
});
|
||||
} finally {
|
||||
logger.info(`Availability sync complete.`, {
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
});
|
||||
this.running = false;
|
||||
}
|
||||
@@ -243,105 +242,66 @@ class AvailabilitySync {
|
||||
} while (mediaPage.length > 0);
|
||||
}
|
||||
|
||||
private findMediaStatus(
|
||||
requests: MediaRequest[],
|
||||
is4k: boolean
|
||||
): MediaStatus {
|
||||
const filteredRequests = requests.filter(
|
||||
(request) => request.is4k === is4k
|
||||
);
|
||||
|
||||
let mediaStatus: MediaStatus;
|
||||
|
||||
if (
|
||||
filteredRequests.some(
|
||||
(request) => request.status === MediaRequestStatus.APPROVED
|
||||
)
|
||||
) {
|
||||
mediaStatus = MediaStatus.PROCESSING;
|
||||
} else if (
|
||||
filteredRequests.some(
|
||||
(request) => request.status === MediaRequestStatus.PENDING
|
||||
)
|
||||
) {
|
||||
mediaStatus = MediaStatus.PENDING;
|
||||
} else {
|
||||
mediaStatus = MediaStatus.UNKNOWN;
|
||||
}
|
||||
|
||||
return mediaStatus;
|
||||
}
|
||||
|
||||
private async mediaUpdater(media: Media, is4k: boolean): Promise<void> {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
try {
|
||||
// Find all related requests only if
|
||||
// the related media has an available status
|
||||
const requests = await requestRepository
|
||||
.createQueryBuilder('request')
|
||||
.leftJoinAndSelect('request.media', 'media')
|
||||
.where('(media.id = :id)', {
|
||||
id: media.id,
|
||||
})
|
||||
.andWhere(
|
||||
`(request.is4k = :is4k AND media.${
|
||||
is4k ? 'status4k' : 'status'
|
||||
} IN (:...mediaStatus))`,
|
||||
{
|
||||
mediaStatus: [
|
||||
MediaStatus.AVAILABLE,
|
||||
MediaStatus.PARTIALLY_AVAILABLE,
|
||||
],
|
||||
is4k: is4k,
|
||||
}
|
||||
)
|
||||
.getMany();
|
||||
|
||||
// Check if a season is processing or pending to
|
||||
// make sure we set the media to the correct status
|
||||
let mediaStatus = MediaStatus.UNKNOWN;
|
||||
// If media type is tv, check if a season is processing
|
||||
// to see if we need to keep the external metadata
|
||||
let isMediaProcessing = false;
|
||||
|
||||
if (media.mediaType === 'tv') {
|
||||
mediaStatus = this.findMediaStatus(requests, is4k);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
const request = await requestRepository
|
||||
.createQueryBuilder('request')
|
||||
.leftJoinAndSelect('request.media', 'media')
|
||||
.where('(media.id = :id)', {
|
||||
id: media.id,
|
||||
})
|
||||
.andWhere(
|
||||
'(request.is4k = :is4k AND request.status = :requestStatus)',
|
||||
{
|
||||
requestStatus: MediaRequestStatus.APPROVED,
|
||||
is4k: is4k,
|
||||
}
|
||||
)
|
||||
.getOne();
|
||||
|
||||
if (request) {
|
||||
isMediaProcessing = true;
|
||||
}
|
||||
}
|
||||
|
||||
media[is4k ? 'status4k' : 'status'] = mediaStatus;
|
||||
media[is4k ? 'serviceId4k' : 'serviceId'] =
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
? media[is4k ? 'serviceId4k' : 'serviceId']
|
||||
: null;
|
||||
// Set the non-4K or 4K media to deleted
|
||||
// and change related columns to null if media
|
||||
// is not processing
|
||||
media[is4k ? 'status4k' : 'status'] = MediaStatus.DELETED;
|
||||
media[is4k ? 'serviceId4k' : 'serviceId'] = isMediaProcessing
|
||||
? media[is4k ? 'serviceId4k' : 'serviceId']
|
||||
: null;
|
||||
media[is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
isMediaProcessing
|
||||
? media[is4k ? 'externalServiceId4k' : 'externalServiceId']
|
||||
: null;
|
||||
media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
isMediaProcessing
|
||||
? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']
|
||||
: null;
|
||||
media[is4k ? 'ratingKey4k' : 'ratingKey'] =
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
? media[is4k ? 'ratingKey4k' : 'ratingKey']
|
||||
: null;
|
||||
media[is4k ? 'ratingKey4k' : 'ratingKey'] = isMediaProcessing
|
||||
? media[is4k ? 'ratingKey4k' : 'ratingKey']
|
||||
: null;
|
||||
|
||||
logger.info(
|
||||
`The ${is4k ? '4K' : 'non-4K'} ${
|
||||
media.mediaType === 'movie' ? 'movie' : 'show'
|
||||
} [TMDB ID ${media.tmdbId}] was not found in any ${
|
||||
media.mediaType === 'movie' ? 'Radarr' : 'Sonarr'
|
||||
} and Plex instance. Status will be changed to unknown.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
} and Plex instance. Status will be changed to deleted.`,
|
||||
{ label: 'Availability Sync' }
|
||||
);
|
||||
|
||||
await mediaRepository.save({ media, ...media });
|
||||
|
||||
// Only delete media request if type is movie.
|
||||
// Type tv request deletion is handled
|
||||
// in the season request entity
|
||||
if (requests.length > 0 && media.mediaType === 'movie') {
|
||||
await requestRepository.remove(requests);
|
||||
}
|
||||
} catch (ex) {
|
||||
logger.debug(
|
||||
`Failure updating the ${is4k ? '4K' : 'non-4K'} ${
|
||||
@@ -349,7 +309,7 @@ class AvailabilitySync {
|
||||
} [TMDB ID ${media.tmdbId}].`,
|
||||
{
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -361,35 +321,21 @@ class AvailabilitySync {
|
||||
is4k: boolean
|
||||
): Promise<void> {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const seasonRequestRepository = getRepository(SeasonRequest);
|
||||
|
||||
// Filter out only the values that are false
|
||||
// (media that should be deleted)
|
||||
const seasonsPendingRemoval = new Map(
|
||||
// Disabled linter as only the value is needed from the filter
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
[...seasons].filter(([_, exists]) => !exists)
|
||||
);
|
||||
// Retrieve the season keys to pass into our log
|
||||
const seasonKeys = [...seasonsPendingRemoval.keys()];
|
||||
|
||||
try {
|
||||
// Need to check and see if there are any related season
|
||||
// requests. If they are, we will need to delete them.
|
||||
const seasonRequests = await seasonRequestRepository
|
||||
.createQueryBuilder('seasonRequest')
|
||||
.leftJoinAndSelect('seasonRequest.request', 'request')
|
||||
.leftJoinAndSelect('request.media', 'media')
|
||||
.where('(media.id = :id)', { id: media.id })
|
||||
.andWhere(
|
||||
'(request.is4k = :is4k AND seasonRequest.seasonNumber IN (:...seasonNumbers))',
|
||||
{
|
||||
seasonNumbers: seasonKeys,
|
||||
is4k: is4k,
|
||||
}
|
||||
)
|
||||
.getMany();
|
||||
|
||||
for (const mediaSeason of media.seasons) {
|
||||
if (seasonsPendingRemoval.has(mediaSeason.seasonNumber)) {
|
||||
mediaSeason[is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
||||
mediaSeason[is4k ? 'status4k' : 'status'] = MediaStatus.DELETED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,7 +343,7 @@ class AvailabilitySync {
|
||||
media.status = MediaStatus.PARTIALLY_AVAILABLE;
|
||||
logger.info(
|
||||
`Marking the non-4K show [TMDB ID ${media.tmdbId}] as PARTIALLY_AVAILABLE because season removal has occurred.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
{ label: 'Availability Sync' }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -405,23 +351,19 @@ class AvailabilitySync {
|
||||
media.status4k = MediaStatus.PARTIALLY_AVAILABLE;
|
||||
logger.info(
|
||||
`Marking the 4K show [TMDB ID ${media.tmdbId}] as PARTIALLY_AVAILABLE because season removal has occurred.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
{ label: 'Availability Sync' }
|
||||
);
|
||||
}
|
||||
|
||||
await mediaRepository.save({ media, ...media });
|
||||
|
||||
if (seasonRequests.length > 0) {
|
||||
await seasonRequestRepository.remove(seasonRequests);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`The ${is4k ? '4K' : 'non-4K'} season(s) [${seasonKeys}] [TMDB ID ${
|
||||
media.tmdbId
|
||||
}] was not found in any ${
|
||||
media.mediaType === 'tv' ? 'Sonarr' : 'Radarr'
|
||||
} and Plex instance. Status will be changed to unknown.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
} and Plex instance. Status will be changed to deleted.`,
|
||||
{ label: 'Availability Sync' }
|
||||
);
|
||||
} catch (ex) {
|
||||
logger.debug(
|
||||
@@ -430,7 +372,7 @@ class AvailabilitySync {
|
||||
} season(s) [${seasonKeys}], TMDB ID ${media.tmdbId}.`,
|
||||
{
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -444,7 +386,9 @@ class AvailabilitySync {
|
||||
|
||||
// Check for availability in all of the available radarr servers
|
||||
// If any find the media, we will assume the media exists
|
||||
for (const server of this.radarrServers) {
|
||||
for (const server of this.radarrServers.filter(
|
||||
(server) => server.is4k === is4k
|
||||
)) {
|
||||
const radarrAPI = new RadarrAPI({
|
||||
apiKey: server.apiKey,
|
||||
url: RadarrAPI.buildUrl(server, '/api/v3'),
|
||||
@@ -453,13 +397,13 @@ class AvailabilitySync {
|
||||
try {
|
||||
let radarr: RadarrMovie | undefined;
|
||||
|
||||
if (!server.is4k && media.externalServiceId && !is4k) {
|
||||
if (media.externalServiceId && !is4k) {
|
||||
radarr = await radarrAPI.getMovie({
|
||||
id: media.externalServiceId,
|
||||
});
|
||||
}
|
||||
|
||||
if (server.is4k && media.externalServiceId4k && is4k) {
|
||||
if (media.externalServiceId4k && is4k) {
|
||||
radarr = await radarrAPI.getMovie({
|
||||
id: media.externalServiceId4k,
|
||||
});
|
||||
@@ -477,7 +421,7 @@ class AvailabilitySync {
|
||||
}] from Radarr.`,
|
||||
{
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -496,7 +440,9 @@ class AvailabilitySync {
|
||||
|
||||
// Check for availability in all of the available sonarr servers
|
||||
// If any find the media, we will assume the media exists
|
||||
for (const server of this.sonarrServers) {
|
||||
for (const server of this.sonarrServers.filter((server) => {
|
||||
return server.is4k === is4k;
|
||||
})) {
|
||||
const sonarrAPI = new SonarrAPI({
|
||||
apiKey: server.apiKey,
|
||||
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
||||
@@ -505,13 +451,13 @@ class AvailabilitySync {
|
||||
try {
|
||||
let sonarr: SonarrSeries | undefined;
|
||||
|
||||
if (!server.is4k && media.externalServiceId && !is4k) {
|
||||
if (media.externalServiceId && !is4k) {
|
||||
sonarr = await sonarrAPI.getSeriesById(media.externalServiceId);
|
||||
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
|
||||
sonarr.seasons;
|
||||
}
|
||||
|
||||
if (server.is4k && media.externalServiceId4k && is4k) {
|
||||
if (media.externalServiceId4k && is4k) {
|
||||
sonarr = await sonarrAPI.getSeriesById(media.externalServiceId4k);
|
||||
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] =
|
||||
sonarr.seasons;
|
||||
@@ -530,7 +476,7 @@ class AvailabilitySync {
|
||||
}] from Sonarr.`,
|
||||
{
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -576,7 +522,9 @@ class AvailabilitySync {
|
||||
// Check each sonarr 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 mediaExistsInSonarr
|
||||
for (const server of this.sonarrServers) {
|
||||
for (const server of this.sonarrServers.filter(
|
||||
(server) => server.is4k === is4k
|
||||
)) {
|
||||
let sonarrSeasons: SonarrSeason[] | undefined;
|
||||
|
||||
if (media.externalServiceId && !is4k) {
|
||||
@@ -650,7 +598,7 @@ class AvailabilitySync {
|
||||
} [TMDB ID ${media.tmdbId}] from Plex.`,
|
||||
{
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -722,4 +670,5 @@ class AvailabilitySync {
|
||||
}
|
||||
|
||||
const availabilitySync = new AvailabilitySync();
|
||||
|
||||
export default availabilitySync;
|
||||
|
Reference in New Issue
Block a user