Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 159 additions & 55 deletions server/subscriber/MediaRequestSubscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import { MediaRequest } from '@server/entity/MediaRequest';
import Season from '@server/entity/Season';
import SeasonRequest from '@server/entity/SeasonRequest';
import notificationManager, { Notification } from '@server/lib/notifications';
import { getSettings } from '@server/lib/settings';
Expand All @@ -27,7 +28,7 @@ import type {
RemoveEvent,
UpdateEvent,
} from 'typeorm';
import { EventSubscriber } from 'typeorm';
import { EventSubscriber, Not } from 'typeorm';

const sanitizeDisplayName = (displayName: string): string => {
return displayName
Expand Down Expand Up @@ -397,10 +398,23 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
await mediaRepository.save(media);
})
.catch(async () => {
const requestRepository = getRepository(MediaRequest);
try {
const requestRepository = getRepository(MediaRequest);

entity.status = MediaRequestStatus.FAILED;
requestRepository.save(entity);
if (entity.status !== MediaRequestStatus.FAILED) {
entity.status = MediaRequestStatus.FAILED;
await requestRepository.save(entity);
}
} catch (saveError) {
logger.error('Failed to mark request as FAILED', {
label: 'Media Request',
requestId: entity.id,
errorMessage:
saveError instanceof Error
? saveError.message
: String(saveError),
});
}

logger.warn(
'Something went wrong sending movie request to Radarr, marking status as FAILED',
Expand Down Expand Up @@ -503,7 +517,6 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe

const media = await mediaRepository.findOne({
where: { id: entity.media.id },
relations: { requests: true },
});

if (!media) {
Expand Down Expand Up @@ -690,7 +703,6 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
// We grab media again here to make sure we have the latest version of it
const media = await mediaRepository.findOne({
where: { id: entity.media.id },
relations: { requests: true },
});

if (!media) {
Expand All @@ -707,10 +719,23 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
await mediaRepository.save(media);
})
.catch(async () => {
const requestRepository = getRepository(MediaRequest);
try {
const requestRepository = getRepository(MediaRequest);

entity.status = MediaRequestStatus.FAILED;
requestRepository.save(entity);
if (entity.status !== MediaRequestStatus.FAILED) {
entity.status = MediaRequestStatus.FAILED;
await requestRepository.save(entity);
}
} catch (saveError) {
logger.error('Failed to mark request as FAILED', {
label: 'Media Request',
requestId: entity.id,
errorMessage:
saveError instanceof Error
? saveError.message
: String(saveError),
});
}

logger.warn(
'Something went wrong sending series request to Sonarr, marking status as FAILED',
Expand Down Expand Up @@ -758,7 +783,6 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOne({
where: { id: entity.media.id },
relations: { requests: true },
});
if (!media) {
logger.error('Media data not found', {
Expand All @@ -768,26 +792,29 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
});
return;
}

const statusKey = entity.is4k ? 'status4k' : 'status';
const seasonRequestRepository = getRepository(SeasonRequest);
const requestRepository = getRepository(MediaRequest);

if (
entity.status === MediaRequestStatus.APPROVED &&
// Do not update the status if the item is already partially available or available
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
media[entity.is4k ? 'status4k' : 'status'] !==
MediaStatus.PARTIALLY_AVAILABLE &&
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.PROCESSING
media[statusKey] !== MediaStatus.AVAILABLE &&
media[statusKey] !== MediaStatus.PARTIALLY_AVAILABLE &&
media[statusKey] !== MediaStatus.PROCESSING
) {
media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
mediaRepository.save(media);
media[statusKey] = MediaStatus.PROCESSING;
await mediaRepository.save(media);
}

if (
media.mediaType === MediaType.MOVIE &&
entity.status === MediaRequestStatus.DECLINED &&
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
media[statusKey] !== MediaStatus.DELETED
) {
media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
media[statusKey] = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
}

/**
Expand All @@ -799,14 +826,71 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
if (
media.mediaType === MediaType.TV &&
entity.status === MediaRequestStatus.DECLINED &&
media.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING
).length === 0 &&
media[entity.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING &&
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
media[statusKey] === MediaStatus.PENDING
) {
media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
const pendingCount = await requestRepository.count({
where: {
media: { id: media.id },
status: MediaRequestStatus.PENDING,
is4k: entity.is4k,
id: Not(entity.id),
},
});

if (pendingCount === 0) {
// Re-fetch media without requests to avoid cascade issues
const freshMedia = await mediaRepository.findOne({
where: { id: media.id },
});
if (freshMedia) {
freshMedia[statusKey] = MediaStatus.UNKNOWN;
await mediaRepository.save(freshMedia);
}
}
}

// Reset season statuses when a TV request is declined
if (
media.mediaType === MediaType.TV &&
entity.status === MediaRequestStatus.DECLINED
) {
const seasonRepository = getRepository(Season);
const actualSeasons = await seasonRepository.find({
where: { media: { id: media.id } },
});

for (const seasonRequest of entity.seasons) {
seasonRequest.status = MediaRequestStatus.DECLINED;
await seasonRequestRepository.save(seasonRequest);

const season = actualSeasons.find(
(s) => s.seasonNumber === seasonRequest.seasonNumber
);

if (season && season[statusKey] === MediaStatus.PENDING) {
const otherActiveRequests = await requestRepository
.createQueryBuilder('request')
.leftJoinAndSelect('request.seasons', 'season')
.where('request.mediaId = :mediaId', { mediaId: media.id })
.andWhere('request.id != :requestId', { requestId: entity.id })
.andWhere('request.is4k = :is4k', { is4k: entity.is4k })
.andWhere('request.status NOT IN (:...statuses)', {
statuses: [
MediaRequestStatus.DECLINED,
MediaRequestStatus.COMPLETED,
],
})
.andWhere('season.seasonNumber = :seasonNumber', {
seasonNumber: season.seasonNumber,
})
.getCount();

if (otherActiveRequests === 0) {
season[statusKey] = MediaStatus.UNKNOWN;
await seasonRepository.save(season);
}
}
}
}

// Approve child seasons if parent is approved
Expand All @@ -830,54 +914,74 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
relations: { requests: true },
});

if (!fullMedia) return;

if (
const needsStatusUpdate =
!fullMedia.requests.some((request) => !request.is4k) &&
fullMedia.status !== MediaStatus.AVAILABLE
) {
fullMedia.status = MediaStatus.UNKNOWN;
}
fullMedia.status !== MediaStatus.AVAILABLE;

if (
const needs4kStatusUpdate =
!fullMedia.requests.some((request) => request.is4k) &&
fullMedia.status4k !== MediaStatus.AVAILABLE
) {
fullMedia.status4k = MediaStatus.UNKNOWN;
}
fullMedia.status4k !== MediaStatus.AVAILABLE;

await manager.save(fullMedia);
if (needsStatusUpdate || needs4kStatusUpdate) {
// Re-fetch WITHOUT requests to avoid cascade issues on save
const cleanMedia = await manager.findOneOrFail(Media, {
where: { id: entity.media.id },
});

if (needsStatusUpdate) {
cleanMedia.status = MediaStatus.UNKNOWN;
}
if (needs4kStatusUpdate) {
cleanMedia.status4k = MediaStatus.UNKNOWN;
}

await manager.save(cleanMedia);
}
}

public afterUpdate(event: UpdateEvent<MediaRequest>): void {
public async afterUpdate(event: UpdateEvent<MediaRequest>): Promise<void> {
if (!event.entity) {
return;
}

this.sendToRadarr(event.entity as MediaRequest);
this.sendToSonarr(event.entity as MediaRequest);

this.updateParentStatus(event.entity as MediaRequest);
try {
await this.sendToRadarr(event.entity as MediaRequest);
await this.sendToSonarr(event.entity as MediaRequest);
await this.updateParentStatus(event.entity as MediaRequest);

if (event.entity.status === MediaRequestStatus.COMPLETED) {
if (event.entity.media.mediaType === MediaType.MOVIE) {
this.notifyAvailableMovie(event.entity as MediaRequest, event);
}
if (event.entity.media.mediaType === MediaType.TV) {
this.notifyAvailableSeries(event.entity as MediaRequest, event);
if (event.entity.status === MediaRequestStatus.COMPLETED) {
if (event.entity.media.mediaType === MediaType.MOVIE) {
await this.notifyAvailableMovie(event.entity as MediaRequest, event);
}
if (event.entity.media.mediaType === MediaType.TV) {
await this.notifyAvailableSeries(event.entity as MediaRequest, event);
}
}
} catch (e) {
logger.error('Error in afterUpdate subscriber', {
label: 'Media Request',
requestId: (event.entity as MediaRequest).id,
errorMessage: e instanceof Error ? e.message : String(e),
});
}
}

public afterInsert(event: InsertEvent<MediaRequest>): void {
public async afterInsert(event: InsertEvent<MediaRequest>): Promise<void> {
if (!event.entity) {
return;
}

this.sendToRadarr(event.entity as MediaRequest);
this.sendToSonarr(event.entity as MediaRequest);

this.updateParentStatus(event.entity as MediaRequest);
try {
await this.sendToRadarr(event.entity as MediaRequest);
await this.sendToSonarr(event.entity as MediaRequest);
await this.updateParentStatus(event.entity as MediaRequest);
} catch (e) {
logger.error('Error in afterInsert subscriber', {
label: 'Media Request',
requestId: (event.entity as MediaRequest).id,
errorMessage: e instanceof Error ? e.message : String(e),
});
}
}

public async afterRemove(event: RemoveEvent<MediaRequest>): Promise<void> {
Expand Down
7 changes: 6 additions & 1 deletion src/components/RequestModal/TvRequestModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,12 @@ const TvRequestModal = ({
).length > 0
) {
data.mediaInfo.requests
.filter((request) => request.is4k === is4k)
.filter(
(request) =>
request.is4k === is4k &&
request.status !== MediaRequestStatus.DECLINED &&
request.status !== MediaRequestStatus.COMPLETED
)
.forEach((request) => {
if (!seasonRequest) {
seasonRequest = request.seasons.find(
Expand Down
Loading