mirror of
https://github.com/sct/overseerr.git
synced 2025-09-27 20:42:03 +02:00
fix: handle currently available non-completed media (#4115)
* fix: check media status has changed before modifying request fix: refactor: code cleanup * fix: manually load database entity seasons
This commit is contained in:
@@ -1,19 +1,13 @@
|
|||||||
import RadarrAPI from '@server/api/servarr/radarr';
|
import RadarrAPI from '@server/api/servarr/radarr';
|
||||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||||
import {
|
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||||
MediaRequestStatus,
|
|
||||||
MediaStatus,
|
|
||||||
MediaType,
|
|
||||||
} from '@server/constants/media';
|
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import SeasonRequest from '@server/entity/SeasonRequest';
|
|
||||||
import type { DownloadingItem } from '@server/lib/downloadtracker';
|
import type { DownloadingItem } from '@server/lib/downloadtracker';
|
||||||
import downloadTracker from '@server/lib/downloadtracker';
|
import downloadTracker from '@server/lib/downloadtracker';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import {
|
import {
|
||||||
AfterLoad,
|
AfterLoad,
|
||||||
AfterUpdate,
|
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
@@ -315,111 +309,6 @@ class Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterUpdate()
|
|
||||||
public async updateRelatedMediaRequest(): Promise<void> {
|
|
||||||
const requestRepository = getRepository(MediaRequest);
|
|
||||||
const seasonRequestRepository = getRepository(SeasonRequest);
|
|
||||||
|
|
||||||
const validStatuses = [
|
|
||||||
MediaStatus.PARTIALLY_AVAILABLE,
|
|
||||||
MediaStatus.AVAILABLE,
|
|
||||||
MediaStatus.DELETED,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (
|
|
||||||
validStatuses.includes(this.status) ||
|
|
||||||
validStatuses.includes(this.status4k)
|
|
||||||
) {
|
|
||||||
const relatedRequests = await requestRepository.find({
|
|
||||||
relations: {
|
|
||||||
media: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
media: { id: this.id },
|
|
||||||
status: MediaRequestStatus.APPROVED,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check the media entity status and if available
|
|
||||||
// or deleted, set the related request to completed
|
|
||||||
if (relatedRequests.length > 0) {
|
|
||||||
const completedRequests: MediaRequest[] = [];
|
|
||||||
|
|
||||||
relatedRequests.forEach((request) => {
|
|
||||||
let shouldComplete = false;
|
|
||||||
|
|
||||||
if (
|
|
||||||
this[request.is4k ? 'status4k' : 'status'] ===
|
|
||||||
MediaStatus.AVAILABLE ||
|
|
||||||
this[request.is4k ? 'status4k' : 'status'] === MediaStatus.DELETED
|
|
||||||
) {
|
|
||||||
shouldComplete = true;
|
|
||||||
} else if (this.mediaType === 'tv') {
|
|
||||||
// For TV, check if all requested seasons are available or deleted
|
|
||||||
const allSeasonsReady = request.seasons.every((requestSeason) => {
|
|
||||||
const matchingSeason = this.seasons.find(
|
|
||||||
(mediaSeason) =>
|
|
||||||
mediaSeason.seasonNumber === requestSeason.seasonNumber
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!matchingSeason) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
matchingSeason[request.is4k ? 'status4k' : 'status'] ===
|
|
||||||
MediaStatus.AVAILABLE ||
|
|
||||||
matchingSeason[request.is4k ? 'status4k' : 'status'] ===
|
|
||||||
MediaStatus.DELETED
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
shouldComplete = allSeasonsReady;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldComplete) {
|
|
||||||
request.status = MediaRequestStatus.COMPLETED;
|
|
||||||
completedRequests.push(request);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await requestRepository.save(completedRequests);
|
|
||||||
|
|
||||||
// Handle season requests and mark them completed when
|
|
||||||
// that specific season becomes available
|
|
||||||
if (this.mediaType === 'tv') {
|
|
||||||
const seasonsToUpdate = relatedRequests.flatMap((request) => {
|
|
||||||
return request.seasons.filter((requestSeason) => {
|
|
||||||
const matchingSeason = this.seasons.find(
|
|
||||||
(mediaSeason) =>
|
|
||||||
mediaSeason.seasonNumber === requestSeason.seasonNumber
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!matchingSeason) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
matchingSeason[request.is4k ? 'status4k' : 'status'] ===
|
|
||||||
MediaStatus.AVAILABLE ||
|
|
||||||
matchingSeason[request.is4k ? 'status4k' : 'status'] ===
|
|
||||||
MediaStatus.DELETED
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
seasonsToUpdate.map((season) =>
|
|
||||||
seasonRequestRepository.update(season.id, {
|
|
||||||
status: MediaRequestStatus.COMPLETED,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Media;
|
export default Media;
|
||||||
|
@@ -1,7 +1,13 @@
|
|||||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
import {
|
||||||
|
MediaRequestStatus,
|
||||||
|
MediaStatus,
|
||||||
|
MediaType,
|
||||||
|
} from '@server/constants/media';
|
||||||
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';
|
||||||
|
import Season from '@server/entity/Season';
|
||||||
|
import SeasonRequest from '@server/entity/SeasonRequest';
|
||||||
import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm';
|
import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm';
|
||||||
import { EventSubscriber } from 'typeorm';
|
import { EventSubscriber } from 'typeorm';
|
||||||
|
|
||||||
@@ -25,7 +31,101 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public beforeUpdate(event: UpdateEvent<Media>): void {
|
private async updateRelatedMediaRequest(event: Media, is4k: boolean) {
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
const seasonRequestRepository = getRepository(SeasonRequest);
|
||||||
|
|
||||||
|
const relatedRequests = await requestRepository.find({
|
||||||
|
relations: {
|
||||||
|
media: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
media: { id: event.id },
|
||||||
|
status: MediaRequestStatus.APPROVED,
|
||||||
|
is4k,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check the media entity status and if available
|
||||||
|
// or deleted, set the related request to completed
|
||||||
|
if (relatedRequests.length > 0) {
|
||||||
|
const completedRequests: MediaRequest[] = [];
|
||||||
|
|
||||||
|
relatedRequests.forEach((request) => {
|
||||||
|
let shouldComplete = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
event[request.is4k ? 'status4k' : 'status'] ===
|
||||||
|
MediaStatus.AVAILABLE ||
|
||||||
|
event[request.is4k ? 'status4k' : 'status'] === MediaStatus.DELETED
|
||||||
|
) {
|
||||||
|
shouldComplete = true;
|
||||||
|
} else if (event.mediaType === 'tv') {
|
||||||
|
// For TV, check if all requested seasons are available or deleted
|
||||||
|
const allSeasonsReady = request.seasons.every((requestSeason) => {
|
||||||
|
const matchingSeason = event.seasons.find(
|
||||||
|
(mediaSeason) =>
|
||||||
|
mediaSeason.seasonNumber === requestSeason.seasonNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingSeason) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
matchingSeason[request.is4k ? 'status4k' : 'status'] ===
|
||||||
|
MediaStatus.AVAILABLE ||
|
||||||
|
matchingSeason[request.is4k ? 'status4k' : 'status'] ===
|
||||||
|
MediaStatus.DELETED
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldComplete = allSeasonsReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldComplete) {
|
||||||
|
request.status = MediaRequestStatus.COMPLETED;
|
||||||
|
completedRequests.push(request);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await requestRepository.save(completedRequests);
|
||||||
|
|
||||||
|
// Handle season requests and mark them completed when
|
||||||
|
// that specific season becomes available
|
||||||
|
if (event.mediaType === 'tv') {
|
||||||
|
const seasonsToUpdate = relatedRequests.flatMap((request) => {
|
||||||
|
return request.seasons.filter((requestSeason) => {
|
||||||
|
const matchingSeason = event.seasons.find(
|
||||||
|
(mediaSeason) =>
|
||||||
|
mediaSeason.seasonNumber === requestSeason.seasonNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingSeason) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
matchingSeason[request.is4k ? 'status4k' : 'status'] ===
|
||||||
|
MediaStatus.AVAILABLE ||
|
||||||
|
matchingSeason[request.is4k ? 'status4k' : 'status'] ===
|
||||||
|
MediaStatus.DELETED
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
seasonsToUpdate.map((season) =>
|
||||||
|
seasonRequestRepository.update(season.id, {
|
||||||
|
status: MediaRequestStatus.COMPLETED,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async beforeUpdate(event: UpdateEvent<Media>): Promise<void> {
|
||||||
if (!event.entity) {
|
if (!event.entity) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -43,6 +143,57 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
|
|||||||
) {
|
) {
|
||||||
this.updateChildRequestStatus(event.entity as Media, true);
|
this.updateChildRequestStatus(event.entity as Media, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manually load related seasons into databaseEntity
|
||||||
|
// for seasonStatusCheck in afterUpdate
|
||||||
|
const seasons = await event.manager
|
||||||
|
.getRepository(Season)
|
||||||
|
.createQueryBuilder('season')
|
||||||
|
.leftJoin('season.media', 'media')
|
||||||
|
.where('media.id = :id', { id: event.databaseEntity.id })
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
event.databaseEntity.seasons = seasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async afterUpdate(event: UpdateEvent<Media>): Promise<void> {
|
||||||
|
if (!event.entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validStatuses = [
|
||||||
|
MediaStatus.PARTIALLY_AVAILABLE,
|
||||||
|
MediaStatus.AVAILABLE,
|
||||||
|
MediaStatus.DELETED,
|
||||||
|
];
|
||||||
|
|
||||||
|
const seasonStatusCheck = (is4k: boolean) => {
|
||||||
|
return event.entity?.seasons?.some((season: Season, index: number) => {
|
||||||
|
const previousSeason = event.databaseEntity.seasons[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
season[is4k ? 'status4k' : 'status'] !==
|
||||||
|
previousSeason[is4k ? 'status4k' : 'status']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
(event.entity.status !== event.databaseEntity?.status ||
|
||||||
|
(event.entity.mediaType === MediaType.TV &&
|
||||||
|
seasonStatusCheck(false))) &&
|
||||||
|
validStatuses.includes(event.entity.status)
|
||||||
|
) {
|
||||||
|
this.updateRelatedMediaRequest(event.entity as Media, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(event.entity.status4k !== event.databaseEntity?.status4k ||
|
||||||
|
(event.entity.mediaType === MediaType.TV && seasonStatusCheck(true))) &&
|
||||||
|
validStatuses.includes(event.entity.status4k)
|
||||||
|
) {
|
||||||
|
this.updateRelatedMediaRequest(event.entity as Media, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public listenTo(): typeof Media {
|
public listenTo(): typeof Media {
|
||||||
|
Reference in New Issue
Block a user