Compare commits
13 Commits
develop
...
fix/availa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44901b3e4f | ||
|
|
8d96c0ce53 | ||
|
|
1cb691f9cd | ||
|
|
82bf2aa4df | ||
|
|
5c3ea9b036 | ||
|
|
d5e3f1cd0b | ||
|
|
390000d6d4 | ||
|
|
ca01b60e5e | ||
|
|
af6c88b5e3 | ||
|
|
53c51d1837 | ||
|
|
16677a4cdf | ||
|
|
05ea56188b | ||
|
|
1cb31f43ea |
@@ -1053,7 +1053,7 @@ components:
|
|||||||
status:
|
status:
|
||||||
type: number
|
type: number
|
||||||
example: 0
|
example: 0
|
||||||
description: Availability of the media. 1 = `UNKNOWN`, 2 = `PENDING`, 3 = `PROCESSING`, 4 = `PARTIALLY_AVAILABLE`, 5 = `AVAILABLE`
|
description: Availability of the media. 1 = `UNKNOWN`, 2 = `PENDING`, 3 = `PROCESSING`, 4 = `PARTIALLY_AVAILABLE`, 5 = `AVAILABLE`, 6 = `DELETED`
|
||||||
requests:
|
requests:
|
||||||
type: array
|
type: array
|
||||||
readOnly: true
|
readOnly: true
|
||||||
@@ -4955,6 +4955,7 @@ paths:
|
|||||||
processing,
|
processing,
|
||||||
unavailable,
|
unavailable,
|
||||||
failed,
|
failed,
|
||||||
|
deleted,
|
||||||
]
|
]
|
||||||
- in: query
|
- in: query
|
||||||
name: sort
|
name: sort
|
||||||
@@ -5694,7 +5695,16 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
enum: [all, available, partial, allavailable, processing, pending]
|
enum:
|
||||||
|
[
|
||||||
|
all,
|
||||||
|
available,
|
||||||
|
partial,
|
||||||
|
allavailable,
|
||||||
|
processing,
|
||||||
|
pending,
|
||||||
|
deleted,
|
||||||
|
]
|
||||||
- in: query
|
- in: query
|
||||||
name: sort
|
name: sort
|
||||||
schema:
|
schema:
|
||||||
@@ -5753,7 +5763,7 @@ paths:
|
|||||||
example: available
|
example: available
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum: [available, partial, processing, pending, unknown]
|
enum: [available, partial, processing, pending, unknown, deleted]
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export enum MediaRequestStatus {
|
|||||||
APPROVED,
|
APPROVED,
|
||||||
DECLINED,
|
DECLINED,
|
||||||
FAILED,
|
FAILED,
|
||||||
|
COMPLETED,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MediaType {
|
export enum MediaType {
|
||||||
@@ -16,4 +17,5 @@ export enum MediaStatus {
|
|||||||
PROCESSING,
|
PROCESSING,
|
||||||
PARTIALLY_AVAILABLE,
|
PARTIALLY_AVAILABLE,
|
||||||
AVAILABLE,
|
AVAILABLE,
|
||||||
|
DELETED,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
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 { MediaStatus, MediaType } from '@server/constants/media';
|
import {
|
||||||
|
MediaRequestStatus,
|
||||||
|
MediaStatus,
|
||||||
|
MediaType,
|
||||||
|
} from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
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';
|
||||||
@@ -8,6 +12,7 @@ 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,
|
||||||
@@ -309,6 +314,37 @@ class Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterUpdate()
|
||||||
|
public async updateRelatedMediaRequest(): Promise<void> {
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
relatedRequests.forEach((request) => {
|
||||||
|
if (
|
||||||
|
this[request.is4k ? 'status4k' : 'status'] ===
|
||||||
|
MediaStatus.AVAILABLE ||
|
||||||
|
this[request.is4k ? 'status4k' : 'status'] === MediaStatus.DELETED
|
||||||
|
) {
|
||||||
|
request.status = MediaRequestStatus.COMPLETED;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
requestRepository.save(relatedRequests);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Media;
|
export default Media;
|
||||||
|
|||||||
@@ -167,7 +167,8 @@ export class MediaRequest {
|
|||||||
// If there is an existing movie request that isn't declined, don't allow a new one.
|
// If there is an existing movie request that isn't declined, don't allow a new one.
|
||||||
if (
|
if (
|
||||||
requestBody.mediaType === MediaType.MOVIE &&
|
requestBody.mediaType === MediaType.MOVIE &&
|
||||||
existing[0].status !== MediaRequestStatus.DECLINED
|
existing[0].status !== MediaRequestStatus.DECLINED &&
|
||||||
|
existing[0].status !== MediaRequestStatus.COMPLETED
|
||||||
) {
|
) {
|
||||||
logger.warn('Duplicate request for media blocked', {
|
logger.warn('Duplicate request for media blocked', {
|
||||||
tmdbId: tmdbMedia.id,
|
tmdbId: tmdbMedia.id,
|
||||||
@@ -260,7 +261,8 @@ export class MediaRequest {
|
|||||||
.filter(
|
.filter(
|
||||||
(request) =>
|
(request) =>
|
||||||
request.is4k === requestBody.is4k &&
|
request.is4k === requestBody.is4k &&
|
||||||
request.status !== MediaRequestStatus.DECLINED
|
request.status !== MediaRequestStatus.DECLINED &&
|
||||||
|
request.status !== MediaRequestStatus.COMPLETED
|
||||||
)
|
)
|
||||||
.reduce((seasons, request) => {
|
.reduce((seasons, request) => {
|
||||||
const combinedSeasons = request.seasons.map(
|
const combinedSeasons = request.seasons.map(
|
||||||
@@ -279,7 +281,9 @@ export class MediaRequest {
|
|||||||
.filter(
|
.filter(
|
||||||
(season) =>
|
(season) =>
|
||||||
season[requestBody.is4k ? 'status4k' : 'status'] !==
|
season[requestBody.is4k ? 'status4k' : 'status'] !==
|
||||||
MediaStatus.UNKNOWN
|
MediaStatus.UNKNOWN &&
|
||||||
|
season[requestBody.is4k ? 'status4k' : 'status'] !==
|
||||||
|
MediaStatus.DELETED
|
||||||
)
|
)
|
||||||
.map((season) => season.seasonNumber),
|
.map((season) => season.seasonNumber),
|
||||||
];
|
];
|
||||||
@@ -583,7 +587,8 @@ export class MediaRequest {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
media.mediaType === MediaType.MOVIE &&
|
media.mediaType === MediaType.MOVIE &&
|
||||||
this.status === MediaRequestStatus.DECLINED
|
this.status === MediaRequestStatus.DECLINED &&
|
||||||
|
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
|
||||||
) {
|
) {
|
||||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
||||||
mediaRepository.save(media);
|
mediaRepository.save(media);
|
||||||
@@ -601,7 +606,8 @@ export class MediaRequest {
|
|||||||
media.requests.filter(
|
media.requests.filter(
|
||||||
(request) => request.status === MediaRequestStatus.PENDING
|
(request) => request.status === MediaRequestStatus.PENDING
|
||||||
).length === 0 &&
|
).length === 0 &&
|
||||||
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING
|
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING &&
|
||||||
|
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
|
||||||
) {
|
) {
|
||||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
||||||
mediaRepository.save(media);
|
mediaRepository.save(media);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { MediaStatus } from '@server/constants/media';
|
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import SeasonRequest from '@server/entity/SeasonRequest';
|
||||||
import {
|
import {
|
||||||
|
AfterUpdate,
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
@@ -35,6 +38,37 @@ class Season {
|
|||||||
constructor(init?: Partial<Season>) {
|
constructor(init?: Partial<Season>) {
|
||||||
Object.assign(this, init);
|
Object.assign(this, init);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterUpdate()
|
||||||
|
public async updateSeasonRequests(): Promise<void> {
|
||||||
|
const seasonRequestRepository = getRepository(SeasonRequest);
|
||||||
|
|
||||||
|
const relatedSeasonRequests = await seasonRequestRepository.find({
|
||||||
|
relations: {
|
||||||
|
request: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
request: { media: { id: (await this.media).id } },
|
||||||
|
seasonNumber: this.seasonNumber,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check seasons when/if they become available or deleted,
|
||||||
|
// then set the related season request to completed
|
||||||
|
relatedSeasonRequests.forEach((seasonRequest) => {
|
||||||
|
if (
|
||||||
|
this.seasonNumber === seasonRequest.seasonNumber &&
|
||||||
|
((!seasonRequest.request.is4k &&
|
||||||
|
(this.status === MediaStatus.AVAILABLE ||
|
||||||
|
this.status === MediaStatus.DELETED)) ||
|
||||||
|
(seasonRequest.request.is4k &&
|
||||||
|
this.status4k === MediaStatus.AVAILABLE) ||
|
||||||
|
this.status4k === MediaStatus.DELETED)
|
||||||
|
)
|
||||||
|
seasonRequest.status = MediaRequestStatus.COMPLETED;
|
||||||
|
});
|
||||||
|
seasonRequestRepository.save(relatedSeasonRequests);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Season;
|
export default Season;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MediaRequestStatus } from '@server/constants/media';
|
import { MediaRequestStatus } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import {
|
import {
|
||||||
AfterRemove,
|
AfterUpdate,
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
@@ -37,15 +37,25 @@ class SeasonRequest {
|
|||||||
Object.assign(this, init);
|
Object.assign(this, init);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterRemove()
|
@AfterUpdate()
|
||||||
public async handleRemoveParent(): Promise<void> {
|
public async updateMediaRequests(): Promise<void> {
|
||||||
const mediaRequestRepository = getRepository(MediaRequest);
|
const requestRepository = getRepository(MediaRequest);
|
||||||
const requestToBeDeleted = await mediaRequestRepository.findOneOrFail({
|
|
||||||
|
const relatedRequest = await requestRepository.findOne({
|
||||||
where: { id: this.request.id },
|
where: { id: this.request.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (requestToBeDeleted.seasons.length === 0) {
|
// Check the parent of the season request and
|
||||||
await mediaRequestRepository.delete({ id: this.request.id });
|
// if every season request is complete
|
||||||
|
// set the parent request to complete as well
|
||||||
|
const isRequestComplete = relatedRequest?.seasons.every(
|
||||||
|
(seasonRequest) => seasonRequest.status === MediaRequestStatus.COMPLETED
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isRequestComplete && relatedRequest) {
|
||||||
|
relatedRequest.status = MediaRequestStatus.COMPLETED;
|
||||||
|
|
||||||
|
requestRepository.save(relatedRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ 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 type Season from '@server/entity/Season';
|
import type Season from '@server/entity/Season';
|
||||||
import SeasonRequest from '@server/entity/SeasonRequest';
|
|
||||||
import { User } from '@server/entity/User';
|
import { User } from '@server/entity/User';
|
||||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
@@ -243,105 +242,66 @@ class AvailabilitySync {
|
|||||||
} while (mediaPage.length > 0);
|
} 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> {
|
private async mediaUpdater(media: Media, is4k: boolean): Promise<void> {
|
||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
const requestRepository = getRepository(MediaRequest);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Find all related requests only if
|
// If media type is tv, check if a season is processing
|
||||||
// the related media has an available status
|
//to see if we need to keep the external metadata
|
||||||
const requests = await requestRepository
|
let isMediaProcessing = false;
|
||||||
.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.mediaType === 'tv') {
|
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;
|
// Set the non-4K or 4K media to deleted
|
||||||
media[is4k ? 'serviceId4k' : 'serviceId'] =
|
// and change related columns to null if media
|
||||||
mediaStatus === MediaStatus.PROCESSING
|
// is not processing
|
||||||
? media[is4k ? 'serviceId4k' : 'serviceId']
|
media[is4k ? 'status4k' : 'status'] = MediaStatus.DELETED;
|
||||||
: null;
|
media[is4k ? 'serviceId4k' : 'serviceId'] = isMediaProcessing
|
||||||
|
? media[is4k ? 'serviceId4k' : 'serviceId']
|
||||||
|
: null;
|
||||||
media[is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
media[is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||||
mediaStatus === MediaStatus.PROCESSING
|
isMediaProcessing
|
||||||
? media[is4k ? 'externalServiceId4k' : 'externalServiceId']
|
? media[is4k ? 'externalServiceId4k' : 'externalServiceId']
|
||||||
: null;
|
: null;
|
||||||
media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||||
mediaStatus === MediaStatus.PROCESSING
|
isMediaProcessing
|
||||||
? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']
|
? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']
|
||||||
: null;
|
: null;
|
||||||
media[is4k ? 'ratingKey4k' : 'ratingKey'] =
|
media[is4k ? 'ratingKey4k' : 'ratingKey'] = isMediaProcessing
|
||||||
mediaStatus === MediaStatus.PROCESSING
|
? media[is4k ? 'ratingKey4k' : 'ratingKey']
|
||||||
? media[is4k ? 'ratingKey4k' : 'ratingKey']
|
: null;
|
||||||
: null;
|
|
||||||
|
|
||||||
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 Plex instance. Status will be changed to deleted.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
|
|
||||||
await mediaRepository.save({ media, ...media });
|
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) {
|
} catch (ex) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Failure updating the ${is4k ? '4K' : 'non-4K'} ${
|
`Failure updating the ${is4k ? '4K' : 'non-4K'} ${
|
||||||
@@ -361,35 +321,21 @@ class AvailabilitySync {
|
|||||||
is4k: boolean
|
is4k: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const mediaRepository = getRepository(Media);
|
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(
|
const seasonsPendingRemoval = new Map(
|
||||||
// Disabled linter as only the value is needed from the filter
|
// Disabled linter as only the value is needed from the filter
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
[...seasons].filter(([_, exists]) => !exists)
|
[...seasons].filter(([_, exists]) => !exists)
|
||||||
);
|
);
|
||||||
|
// Retrieve the season keys to pass into our log
|
||||||
const seasonKeys = [...seasonsPendingRemoval.keys()];
|
const seasonKeys = [...seasonsPendingRemoval.keys()];
|
||||||
|
|
||||||
try {
|
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) {
|
for (const mediaSeason of media.seasons) {
|
||||||
if (seasonsPendingRemoval.has(mediaSeason.seasonNumber)) {
|
if (seasonsPendingRemoval.has(mediaSeason.seasonNumber)) {
|
||||||
mediaSeason[is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
mediaSeason[is4k ? 'status4k' : 'status'] = MediaStatus.DELETED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,16 +357,12 @@ class AvailabilitySync {
|
|||||||
|
|
||||||
await mediaRepository.save({ media, ...media });
|
await mediaRepository.save({ media, ...media });
|
||||||
|
|
||||||
if (seasonRequests.length > 0) {
|
|
||||||
await seasonRequestRepository.remove(seasonRequests);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`The ${is4k ? '4K' : 'non-4K'} season(s) [${seasonKeys}] [TMDB ID ${
|
`The ${is4k ? '4K' : 'non-4K'} season(s) [${seasonKeys}] [TMDB ID ${
|
||||||
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 Plex instance. Status will be changed to deleted.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -445,41 +387,43 @@ class AvailabilitySync {
|
|||||||
// Check for availability in all of the available radarr servers
|
// Check for availability in all of the available radarr servers
|
||||||
// If any find the media, we will assume the media exists
|
// If any find the media, we will assume the media exists
|
||||||
for (const server of this.radarrServers) {
|
for (const server of this.radarrServers) {
|
||||||
const radarrAPI = new RadarrAPI({
|
if (server.is4k === is4k) {
|
||||||
apiKey: server.apiKey,
|
const radarrAPI = new RadarrAPI({
|
||||||
url: RadarrAPI.buildUrl(server, '/api/v3'),
|
apiKey: server.apiKey,
|
||||||
});
|
url: RadarrAPI.buildUrl(server, '/api/v3'),
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let radarr: RadarrMovie | undefined;
|
let radarr: RadarrMovie | undefined;
|
||||||
|
|
||||||
if (!server.is4k && media.externalServiceId && !is4k) {
|
if (media.externalServiceId && !is4k) {
|
||||||
radarr = await radarrAPI.getMovie({
|
radarr = await radarrAPI.getMovie({
|
||||||
id: media.externalServiceId,
|
id: media.externalServiceId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.is4k && media.externalServiceId4k && is4k) {
|
if (media.externalServiceId4k && is4k) {
|
||||||
radarr = await radarrAPI.getMovie({
|
radarr = await radarrAPI.getMovie({
|
||||||
id: media.externalServiceId4k,
|
id: media.externalServiceId4k,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (radarr && radarr.hasFile) {
|
if (radarr && radarr.hasFile) {
|
||||||
existsInRadarr = true;
|
existsInRadarr = true;
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (!ex.message.includes('404')) {
|
if (!ex.message.includes('404')) {
|
||||||
existsInRadarr = true;
|
existsInRadarr = true;
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
|
`Failure retrieving the ${
|
||||||
media.tmdbId
|
is4k ? '4K' : 'non-4K'
|
||||||
}] from Radarr.`,
|
} movie [TMDB ID ${media.tmdbId}] from Radarr.`,
|
||||||
{
|
{
|
||||||
errorMessage: ex.message,
|
errorMessage: ex.message,
|
||||||
label: 'AvailabilitySync',
|
label: 'AvailabilitySync',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,42 +441,45 @@ class AvailabilitySync {
|
|||||||
// Check for availability in all of the available sonarr servers
|
// Check for availability in all of the available sonarr servers
|
||||||
// If any find the media, we will assume the media exists
|
// If any find the media, we will assume the media exists
|
||||||
for (const server of this.sonarrServers) {
|
for (const server of this.sonarrServers) {
|
||||||
const sonarrAPI = new SonarrAPI({
|
if (server.is4k === is4k) {
|
||||||
apiKey: server.apiKey,
|
const sonarrAPI = new SonarrAPI({
|
||||||
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
apiKey: server.apiKey,
|
||||||
});
|
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let sonarr: SonarrSeries | undefined;
|
let sonarr: SonarrSeries | undefined;
|
||||||
|
|
||||||
if (!server.is4k && media.externalServiceId && !is4k) {
|
if (media.externalServiceId && !is4k) {
|
||||||
sonarr = await sonarrAPI.getSeriesById(media.externalServiceId);
|
sonarr = await sonarrAPI.getSeriesById(media.externalServiceId);
|
||||||
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
|
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
|
||||||
sonarr.seasons;
|
sonarr.seasons;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.is4k && media.externalServiceId4k && is4k) {
|
if (media.externalServiceId4k && is4k) {
|
||||||
sonarr = await sonarrAPI.getSeriesById(media.externalServiceId4k);
|
sonarr = await sonarrAPI.getSeriesById(media.externalServiceId4k);
|
||||||
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] =
|
this.sonarrSeasonsCache[
|
||||||
sonarr.seasons;
|
`${server.id}-${media.externalServiceId4k}`
|
||||||
}
|
] = sonarr.seasons;
|
||||||
|
}
|
||||||
|
|
||||||
if (sonarr && sonarr.statistics.episodeFileCount > 0) {
|
if (sonarr && sonarr.statistics.episodeFileCount > 0) {
|
||||||
existsInSonarr = true;
|
existsInSonarr = true;
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (!ex.message.includes('404')) {
|
if (!ex.message.includes('404')) {
|
||||||
existsInSonarr = true;
|
existsInSonarr = true;
|
||||||
preventSeasonSearch = true;
|
preventSeasonSearch = true;
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} show [TMDB ID ${
|
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} show [TMDB ID ${
|
||||||
media.tmdbId
|
media.tmdbId
|
||||||
}] from Sonarr.`,
|
}] from Sonarr.`,
|
||||||
{
|
{
|
||||||
errorMessage: ex.message,
|
errorMessage: ex.message,
|
||||||
label: 'AvailabilitySync',
|
label: 'AvailabilitySync',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -722,4 +669,5 @@ class AvailabilitySync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const availabilitySync = new AvailabilitySync();
|
const availabilitySync = new AvailabilitySync();
|
||||||
|
|
||||||
export default availabilitySync;
|
export default availabilitySync;
|
||||||
|
|||||||
@@ -281,7 +281,9 @@ class BaseScanner<T> {
|
|||||||
? MediaStatus.AVAILABLE
|
? MediaStatus.AVAILABLE
|
||||||
: season.episodes > 0
|
: season.episodes > 0
|
||||||
? MediaStatus.PARTIALLY_AVAILABLE
|
? MediaStatus.PARTIALLY_AVAILABLE
|
||||||
: !season.is4kOverride && season.processing
|
: !season.is4kOverride &&
|
||||||
|
season.processing &&
|
||||||
|
existingSeason.status !== MediaStatus.DELETED
|
||||||
? MediaStatus.PROCESSING
|
? MediaStatus.PROCESSING
|
||||||
: existingSeason.status;
|
: existingSeason.status;
|
||||||
|
|
||||||
@@ -294,7 +296,9 @@ class BaseScanner<T> {
|
|||||||
? MediaStatus.AVAILABLE
|
? MediaStatus.AVAILABLE
|
||||||
: this.enable4kShow && season.episodes4k > 0
|
: this.enable4kShow && season.episodes4k > 0
|
||||||
? MediaStatus.PARTIALLY_AVAILABLE
|
? MediaStatus.PARTIALLY_AVAILABLE
|
||||||
: season.is4kOverride && season.processing
|
: season.is4kOverride &&
|
||||||
|
season.processing &&
|
||||||
|
existingSeason.status4k !== MediaStatus.DELETED
|
||||||
? MediaStatus.PROCESSING
|
? MediaStatus.PROCESSING
|
||||||
: existingSeason.status4k;
|
: existingSeason.status4k;
|
||||||
} else {
|
} else {
|
||||||
@@ -401,13 +405,18 @@ class BaseScanner<T> {
|
|||||||
// the status
|
// the status
|
||||||
const shouldStayAvailable =
|
const shouldStayAvailable =
|
||||||
media.status === MediaStatus.AVAILABLE &&
|
media.status === MediaStatus.AVAILABLE &&
|
||||||
newSeasons.filter((season) => season.status !== MediaStatus.UNKNOWN)
|
newSeasons.filter(
|
||||||
.length === 0;
|
(season) =>
|
||||||
|
season.status !== MediaStatus.UNKNOWN &&
|
||||||
|
season.status !== MediaStatus.DELETED
|
||||||
|
).length === 0;
|
||||||
const shouldStayAvailable4k =
|
const shouldStayAvailable4k =
|
||||||
media.status4k === MediaStatus.AVAILABLE &&
|
media.status4k === MediaStatus.AVAILABLE &&
|
||||||
newSeasons.filter((season) => season.status4k !== MediaStatus.UNKNOWN)
|
newSeasons.filter(
|
||||||
.length === 0;
|
(season) =>
|
||||||
|
season.status4k !== MediaStatus.UNKNOWN &&
|
||||||
|
season.status4k !== MediaStatus.DELETED
|
||||||
|
).length === 0;
|
||||||
media.status =
|
media.status =
|
||||||
isAllStandardSeasons || shouldStayAvailable
|
isAllStandardSeasons || shouldStayAvailable
|
||||||
? MediaStatus.AVAILABLE
|
? MediaStatus.AVAILABLE
|
||||||
@@ -417,11 +426,13 @@ class BaseScanner<T> {
|
|||||||
season.status === MediaStatus.AVAILABLE
|
season.status === MediaStatus.AVAILABLE
|
||||||
)
|
)
|
||||||
? MediaStatus.PARTIALLY_AVAILABLE
|
? MediaStatus.PARTIALLY_AVAILABLE
|
||||||
: !seasons.length ||
|
: (!seasons.length && media.status !== MediaStatus.DELETED) ||
|
||||||
media.seasons.some(
|
media.seasons.some(
|
||||||
(season) => season.status === MediaStatus.PROCESSING
|
(season) => season.status === MediaStatus.PROCESSING
|
||||||
)
|
)
|
||||||
? MediaStatus.PROCESSING
|
? MediaStatus.PROCESSING
|
||||||
|
: media.status === MediaStatus.DELETED
|
||||||
|
? MediaStatus.DELETED
|
||||||
: MediaStatus.UNKNOWN;
|
: MediaStatus.UNKNOWN;
|
||||||
media.status4k =
|
media.status4k =
|
||||||
(isAll4kSeasons || shouldStayAvailable4k) && this.enable4kShow
|
(isAll4kSeasons || shouldStayAvailable4k) && this.enable4kShow
|
||||||
@@ -433,11 +444,13 @@ class BaseScanner<T> {
|
|||||||
season.status4k === MediaStatus.AVAILABLE
|
season.status4k === MediaStatus.AVAILABLE
|
||||||
)
|
)
|
||||||
? MediaStatus.PARTIALLY_AVAILABLE
|
? MediaStatus.PARTIALLY_AVAILABLE
|
||||||
: !seasons.length ||
|
: (!seasons.length && media.status4k !== MediaStatus.DELETED) ||
|
||||||
media.seasons.some(
|
media.seasons.some(
|
||||||
(season) => season.status4k === MediaStatus.PROCESSING
|
(season) => season.status4k === MediaStatus.PROCESSING
|
||||||
)
|
)
|
||||||
? MediaStatus.PROCESSING
|
? MediaStatus.PROCESSING
|
||||||
|
: media.status4k === MediaStatus.DELETED
|
||||||
|
? MediaStatus.DELETED
|
||||||
: MediaStatus.UNKNOWN;
|
: MediaStatus.UNKNOWN;
|
||||||
await mediaRepository.save(media);
|
await mediaRepository.save(media);
|
||||||
this.log(`Updating existing title: ${title}`);
|
this.log(`Updating existing title: ${title}`);
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import TautulliAPI from '@server/api/tautulli';
|
import TautulliAPI from '@server/api/tautulli';
|
||||||
import { MediaStatus, MediaType } 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 SeasonRequest from '@server/entity/SeasonRequest';
|
||||||
import { User } from '@server/entity/User';
|
import { User } from '@server/entity/User';
|
||||||
import type {
|
import type {
|
||||||
MediaResultsResponse,
|
MediaResultsResponse,
|
||||||
@@ -98,6 +104,8 @@ mediaRoutes.post<
|
|||||||
isAuthenticated(Permission.MANAGE_REQUESTS),
|
isAuthenticated(Permission.MANAGE_REQUESTS),
|
||||||
async (req, res, next) => {
|
async (req, res, next) => {
|
||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
const seasonRequestRepository = getRepository(SeasonRequest);
|
||||||
|
|
||||||
const media = await mediaRepository.findOne({
|
const media = await mediaRepository.findOne({
|
||||||
where: { id: Number(req.params.id) },
|
where: { id: Number(req.params.id) },
|
||||||
@@ -138,6 +146,35 @@ mediaRoutes.post<
|
|||||||
media.status = MediaStatus.UNKNOWN;
|
media.status = MediaStatus.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.params.status === 'available') {
|
||||||
|
// Here we check all related media requests and
|
||||||
|
// then set to completed if the media is marked
|
||||||
|
// as available
|
||||||
|
const requests = await requestRepository.find({
|
||||||
|
relations: {
|
||||||
|
media: true,
|
||||||
|
},
|
||||||
|
where: { media: { id: media.id }, is4k: is4k },
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestIds = requests.map((request) => request.id);
|
||||||
|
|
||||||
|
if (requestIds.length > 0) {
|
||||||
|
await requestRepository.update(
|
||||||
|
{ id: In(requestIds) },
|
||||||
|
{ status: MediaRequestStatus.COMPLETED }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
requests
|
||||||
|
.flatMap((request) => request.seasons)
|
||||||
|
.forEach(async (season) => {
|
||||||
|
await seasonRequestRepository.update(season.id, {
|
||||||
|
status: MediaRequestStatus.COMPLETED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await mediaRepository.save(media);
|
await mediaRepository.save(media);
|
||||||
|
|
||||||
return res.status(200).json(media);
|
return res.status(200).json(media);
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
|||||||
switch (req.query.filter) {
|
switch (req.query.filter) {
|
||||||
case 'approved':
|
case 'approved':
|
||||||
case 'processing':
|
case 'processing':
|
||||||
case 'available':
|
|
||||||
statusFilter = [MediaRequestStatus.APPROVED];
|
statusFilter = [MediaRequestStatus.APPROVED];
|
||||||
break;
|
break;
|
||||||
case 'pending':
|
case 'pending':
|
||||||
@@ -55,12 +54,18 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
|||||||
case 'failed':
|
case 'failed':
|
||||||
statusFilter = [MediaRequestStatus.FAILED];
|
statusFilter = [MediaRequestStatus.FAILED];
|
||||||
break;
|
break;
|
||||||
|
case 'completed':
|
||||||
|
case 'available':
|
||||||
|
case 'deleted':
|
||||||
|
statusFilter = [MediaRequestStatus.COMPLETED];
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
statusFilter = [
|
statusFilter = [
|
||||||
MediaRequestStatus.PENDING,
|
MediaRequestStatus.PENDING,
|
||||||
MediaRequestStatus.APPROVED,
|
MediaRequestStatus.APPROVED,
|
||||||
MediaRequestStatus.DECLINED,
|
MediaRequestStatus.DECLINED,
|
||||||
MediaRequestStatus.FAILED,
|
MediaRequestStatus.FAILED,
|
||||||
|
MediaRequestStatus.COMPLETED,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +84,9 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
|||||||
MediaStatus.PARTIALLY_AVAILABLE,
|
MediaStatus.PARTIALLY_AVAILABLE,
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
|
case 'deleted':
|
||||||
|
mediaStatusFilter = [MediaStatus.DELETED];
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
mediaStatusFilter = [
|
mediaStatusFilter = [
|
||||||
MediaStatus.UNKNOWN,
|
MediaStatus.UNKNOWN,
|
||||||
@@ -86,6 +94,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
|||||||
MediaStatus.PROCESSING,
|
MediaStatus.PROCESSING,
|
||||||
MediaStatus.PARTIALLY_AVAILABLE,
|
MediaStatus.PARTIALLY_AVAILABLE,
|
||||||
MediaStatus.AVAILABLE,
|
MediaStatus.AVAILABLE,
|
||||||
|
MediaStatus.DELETED,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,7 +400,8 @@ requestRoutes.put<{ requestId: string }>(
|
|||||||
(r) =>
|
(r) =>
|
||||||
r.is4k === request.is4k &&
|
r.is4k === request.is4k &&
|
||||||
r.id !== request.id &&
|
r.id !== request.id &&
|
||||||
r.status !== MediaRequestStatus.DECLINED
|
r.status !== MediaRequestStatus.DECLINED &&
|
||||||
|
r.status !== MediaRequestStatus.COMPLETED
|
||||||
)
|
)
|
||||||
.reduce((seasons, r) => {
|
.reduce((seasons, r) => {
|
||||||
const combinedSeasons = r.seasons.map(
|
const combinedSeasons = r.seasons.map(
|
||||||
|
|||||||
128
server/subscriber/MediaRequestSubscriber.ts
Normal file
128
server/subscriber/MediaRequestSubscriber.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
|
import {
|
||||||
|
MediaRequestStatus,
|
||||||
|
MediaStatus,
|
||||||
|
MediaType,
|
||||||
|
} from '@server/constants/media';
|
||||||
|
import { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
|
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||||
|
import logger from '@server/logger';
|
||||||
|
import { truncate } from 'lodash';
|
||||||
|
import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm';
|
||||||
|
import { EventSubscriber } from 'typeorm';
|
||||||
|
|
||||||
|
@EventSubscriber()
|
||||||
|
export class MediaRequestSubscriber
|
||||||
|
implements EntitySubscriberInterface<MediaRequest>
|
||||||
|
{
|
||||||
|
private async notifyAvailableMovie(entity: MediaRequest) {
|
||||||
|
if (entity.media.status === MediaStatus.AVAILABLE) {
|
||||||
|
const tmdb = new TheMovieDb();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const movie = await tmdb.getMovie({
|
||||||
|
movieId: entity.media.tmdbId,
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
|
||||||
|
event: `${entity.is4k ? '4K ' : ''}Movie Request Now Available`,
|
||||||
|
notifyAdmin: false,
|
||||||
|
notifySystem: true,
|
||||||
|
notifyUser: entity.requestedBy,
|
||||||
|
subject: `${movie.title}${
|
||||||
|
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
||||||
|
}`,
|
||||||
|
message: truncate(movie.overview, {
|
||||||
|
length: 500,
|
||||||
|
separator: /\s/,
|
||||||
|
omission: '…',
|
||||||
|
}),
|
||||||
|
media: entity.media,
|
||||||
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||||
|
request: entity,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Something went wrong sending media notification(s)', {
|
||||||
|
label: 'Notifications',
|
||||||
|
errorMessage: e.message,
|
||||||
|
mediaId: entity.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async notifyAvailableSeries(entity: MediaRequest) {
|
||||||
|
// Find all seasons in the related media entity
|
||||||
|
// and see if they are available, then we can check
|
||||||
|
// if the request contains the same seasons
|
||||||
|
const isMediaAvailable = entity.media.seasons
|
||||||
|
.filter(
|
||||||
|
(season) =>
|
||||||
|
season[entity.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
||||||
|
)
|
||||||
|
.every((seasonRequest) =>
|
||||||
|
entity.seasons.find(
|
||||||
|
(season) => season.seasonNumber === seasonRequest.seasonNumber
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entity.media.status === MediaStatus.AVAILABLE || isMediaAvailable) {
|
||||||
|
const tmdb = new TheMovieDb();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tv = await tmdb.getTvShow({ tvId: entity.media.tmdbId });
|
||||||
|
|
||||||
|
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
|
||||||
|
event: `${entity.is4k ? '4K ' : ''}Series Request Now Available`,
|
||||||
|
subject: `${tv.name}${
|
||||||
|
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
|
||||||
|
}`,
|
||||||
|
message: truncate(tv.overview, {
|
||||||
|
length: 500,
|
||||||
|
separator: /\s/,
|
||||||
|
omission: '…',
|
||||||
|
}),
|
||||||
|
notifyAdmin: false,
|
||||||
|
notifySystem: true,
|
||||||
|
notifyUser: entity.requestedBy,
|
||||||
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||||
|
media: entity.media,
|
||||||
|
extra: [
|
||||||
|
{
|
||||||
|
name: 'Requested Seasons',
|
||||||
|
value: entity.seasons
|
||||||
|
.map((season) => season.seasonNumber)
|
||||||
|
.join(', '),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
request: entity,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Something went wrong sending media notification(s)', {
|
||||||
|
label: 'Notifications',
|
||||||
|
errorMessage: e.message,
|
||||||
|
mediaId: entity.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public afterUpdate(event: UpdateEvent<MediaRequest>): void {
|
||||||
|
if (!event.entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.entity.status === MediaRequestStatus.COMPLETED) {
|
||||||
|
if (event.entity.media.mediaType === MediaType.MOVIE) {
|
||||||
|
this.notifyAvailableMovie(event.entity as MediaRequest);
|
||||||
|
}
|
||||||
|
if (event.entity.media.mediaType === MediaType.TV) {
|
||||||
|
this.notifyAvailableSeries(event.entity as MediaRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public listenTo(): typeof MediaRequest {
|
||||||
|
return MediaRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,179 +1,12 @@
|
|||||||
import TheMovieDb from '@server/api/themoviedb';
|
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 notificationManager, { Notification } from '@server/lib/notifications';
|
|
||||||
import logger from '@server/logger';
|
|
||||||
import { truncate } from 'lodash';
|
|
||||||
import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm';
|
import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm';
|
||||||
import { EventSubscriber, In, Not } from 'typeorm';
|
import { EventSubscriber } from 'typeorm';
|
||||||
|
|
||||||
@EventSubscriber()
|
@EventSubscriber()
|
||||||
export class MediaSubscriber implements EntitySubscriberInterface<Media> {
|
export class MediaSubscriber implements EntitySubscriberInterface<Media> {
|
||||||
private async notifyAvailableMovie(
|
|
||||||
entity: Media,
|
|
||||||
dbEntity: Media,
|
|
||||||
is4k: boolean
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
entity[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE &&
|
|
||||||
dbEntity[is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE
|
|
||||||
) {
|
|
||||||
if (entity.mediaType === MediaType.MOVIE) {
|
|
||||||
const requestRepository = getRepository(MediaRequest);
|
|
||||||
const relatedRequests = await requestRepository.find({
|
|
||||||
where: {
|
|
||||||
media: {
|
|
||||||
id: entity.id,
|
|
||||||
},
|
|
||||||
is4k,
|
|
||||||
status: Not(MediaRequestStatus.DECLINED),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (relatedRequests.length > 0) {
|
|
||||||
const tmdb = new TheMovieDb();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const movie = await tmdb.getMovie({ movieId: entity.tmdbId });
|
|
||||||
|
|
||||||
relatedRequests.forEach((request) => {
|
|
||||||
notificationManager.sendNotification(
|
|
||||||
Notification.MEDIA_AVAILABLE,
|
|
||||||
{
|
|
||||||
event: `${is4k ? '4K ' : ''}Movie Request Now Available`,
|
|
||||||
notifyAdmin: false,
|
|
||||||
notifySystem: true,
|
|
||||||
notifyUser: request.requestedBy,
|
|
||||||
subject: `${movie.title}${
|
|
||||||
movie.release_date
|
|
||||||
? ` (${movie.release_date.slice(0, 4)})`
|
|
||||||
: ''
|
|
||||||
}`,
|
|
||||||
message: truncate(movie.overview, {
|
|
||||||
length: 500,
|
|
||||||
separator: /\s/,
|
|
||||||
omission: '…',
|
|
||||||
}),
|
|
||||||
media: entity,
|
|
||||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
|
||||||
request,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Something went wrong sending media notification(s)', {
|
|
||||||
label: 'Notifications',
|
|
||||||
errorMessage: e.message,
|
|
||||||
mediaId: entity.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async notifyAvailableSeries(
|
|
||||||
entity: Media,
|
|
||||||
dbEntity: Media,
|
|
||||||
is4k: boolean
|
|
||||||
) {
|
|
||||||
const seasonRepository = getRepository(Season);
|
|
||||||
const newAvailableSeasons = entity.seasons
|
|
||||||
.filter(
|
|
||||||
(season) =>
|
|
||||||
season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
|
||||||
)
|
|
||||||
.map((season) => season.seasonNumber);
|
|
||||||
const oldSeasonIds = dbEntity.seasons.map((season) => season.id);
|
|
||||||
const oldSeasons = await seasonRepository.findBy({ id: In(oldSeasonIds) });
|
|
||||||
const oldAvailableSeasons = oldSeasons
|
|
||||||
.filter(
|
|
||||||
(season) =>
|
|
||||||
season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
|
||||||
)
|
|
||||||
.map((season) => season.seasonNumber);
|
|
||||||
|
|
||||||
const changedSeasons = newAvailableSeasons.filter(
|
|
||||||
(seasonNumber) => !oldAvailableSeasons.includes(seasonNumber)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (changedSeasons.length > 0) {
|
|
||||||
const tmdb = new TheMovieDb();
|
|
||||||
const requestRepository = getRepository(MediaRequest);
|
|
||||||
const processedSeasons: number[] = [];
|
|
||||||
|
|
||||||
for (const changedSeasonNumber of changedSeasons) {
|
|
||||||
const requests = await requestRepository.find({
|
|
||||||
where: {
|
|
||||||
media: {
|
|
||||||
id: entity.id,
|
|
||||||
},
|
|
||||||
is4k,
|
|
||||||
status: Not(MediaRequestStatus.DECLINED),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const request = requests.find(
|
|
||||||
(request) =>
|
|
||||||
// Check if the season is complete AND it contains the current season that was just marked available
|
|
||||||
request.seasons.every((season) =>
|
|
||||||
newAvailableSeasons.includes(season.seasonNumber)
|
|
||||||
) &&
|
|
||||||
request.seasons.some(
|
|
||||||
(season) => season.seasonNumber === changedSeasonNumber
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (request && !processedSeasons.includes(changedSeasonNumber)) {
|
|
||||||
processedSeasons.push(
|
|
||||||
...request.seasons.map((season) => season.seasonNumber)
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const tv = await tmdb.getTvShow({ tvId: entity.tmdbId });
|
|
||||||
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
|
|
||||||
event: `${is4k ? '4K ' : ''}Series Request Now Available`,
|
|
||||||
subject: `${tv.name}${
|
|
||||||
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
|
|
||||||
}`,
|
|
||||||
message: truncate(tv.overview, {
|
|
||||||
length: 500,
|
|
||||||
separator: /\s/,
|
|
||||||
omission: '…',
|
|
||||||
}),
|
|
||||||
notifyAdmin: false,
|
|
||||||
notifySystem: true,
|
|
||||||
notifyUser: request.requestedBy,
|
|
||||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
|
||||||
media: entity,
|
|
||||||
extra: [
|
|
||||||
{
|
|
||||||
name: 'Requested Seasons',
|
|
||||||
value: request.seasons
|
|
||||||
.map((season) => season.seasonNumber)
|
|
||||||
.join(', '),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
request,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Something went wrong sending media notification(s)', {
|
|
||||||
label: 'Notifications',
|
|
||||||
errorMessage: e.message,
|
|
||||||
mediaId: entity.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateChildRequestStatus(event: Media, is4k: boolean) {
|
private async updateChildRequestStatus(event: Media, is4k: boolean) {
|
||||||
const requestRepository = getRepository(MediaRequest);
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
|
||||||
@@ -197,52 +30,6 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
event.entity.mediaType === MediaType.MOVIE &&
|
|
||||||
event.entity.status === MediaStatus.AVAILABLE
|
|
||||||
) {
|
|
||||||
this.notifyAvailableMovie(
|
|
||||||
event.entity as Media,
|
|
||||||
event.databaseEntity,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
event.entity.mediaType === MediaType.MOVIE &&
|
|
||||||
event.entity.status4k === MediaStatus.AVAILABLE
|
|
||||||
) {
|
|
||||||
this.notifyAvailableMovie(
|
|
||||||
event.entity as Media,
|
|
||||||
event.databaseEntity,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
event.entity.mediaType === MediaType.TV &&
|
|
||||||
(event.entity.status === MediaStatus.AVAILABLE ||
|
|
||||||
event.entity.status === MediaStatus.PARTIALLY_AVAILABLE)
|
|
||||||
) {
|
|
||||||
this.notifyAvailableSeries(
|
|
||||||
event.entity as Media,
|
|
||||||
event.databaseEntity,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
event.entity.mediaType === MediaType.TV &&
|
|
||||||
(event.entity.status4k === MediaStatus.AVAILABLE ||
|
|
||||||
event.entity.status4k === MediaStatus.PARTIALLY_AVAILABLE)
|
|
||||||
) {
|
|
||||||
this.notifyAvailableSeries(
|
|
||||||
event.entity as Media,
|
|
||||||
event.databaseEntity,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event.entity.status === MediaStatus.AVAILABLE &&
|
event.entity.status === MediaStatus.AVAILABLE &&
|
||||||
event.databaseEntity.status === MediaStatus.PENDING
|
event.databaseEntity.status === MediaStatus.PENDING
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import Spinner from '@app/assets/spinner.svg';
|
import Spinner from '@app/assets/spinner.svg';
|
||||||
import { CheckCircleIcon } from '@heroicons/react/20/solid';
|
import { CheckCircleIcon } from '@heroicons/react/20/solid';
|
||||||
import { BellIcon, ClockIcon, MinusSmallIcon } from '@heroicons/react/24/solid';
|
import {
|
||||||
|
BellIcon,
|
||||||
|
ClockIcon,
|
||||||
|
MinusSmallIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from '@heroicons/react/24/solid';
|
||||||
import { MediaStatus } from '@server/constants/media';
|
import { MediaStatus } from '@server/constants/media';
|
||||||
|
|
||||||
interface StatusBadgeMiniProps {
|
interface StatusBadgeMiniProps {
|
||||||
@@ -50,6 +55,10 @@ const StatusBadgeMini = ({
|
|||||||
);
|
);
|
||||||
indicatorIcon = <MinusSmallIcon />;
|
indicatorIcon = <MinusSmallIcon />;
|
||||||
break;
|
break;
|
||||||
|
case MediaStatus.DELETED:
|
||||||
|
badgeStyle.push('bg-red-500 border-red-400 ring-red-400 text-red-100');
|
||||||
|
indicatorIcon = <TrashIcon />;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inProgress) {
|
if (inProgress) {
|
||||||
|
|||||||
@@ -204,6 +204,11 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
|||||||
{intl.formatMessage(globalMessages.failed)}
|
{intl.formatMessage(globalMessages.failed)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{request.status === MediaRequestStatus.COMPLETED && (
|
||||||
|
<Badge badgeType="success">
|
||||||
|
{intl.formatMessage(globalMessages.completed)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-center text-sm leading-5 sm:mt-0">
|
<div className="mt-2 flex items-center text-sm leading-5 sm:mt-0">
|
||||||
|
|||||||
@@ -264,7 +264,9 @@ const RequestButton = ({
|
|||||||
|
|
||||||
// Standard request button
|
// Standard request button
|
||||||
if (
|
if (
|
||||||
(!media || media.status === MediaStatus.UNKNOWN) &&
|
(!media ||
|
||||||
|
media.status === MediaStatus.UNKNOWN ||
|
||||||
|
(media.status === MediaStatus.DELETED && !activeRequest)) &&
|
||||||
hasPermission(
|
hasPermission(
|
||||||
[
|
[
|
||||||
Permission.REQUEST,
|
Permission.REQUEST,
|
||||||
@@ -307,7 +309,9 @@ const RequestButton = ({
|
|||||||
|
|
||||||
// 4K request button
|
// 4K request button
|
||||||
if (
|
if (
|
||||||
(!media || media.status4k === MediaStatus.UNKNOWN) &&
|
(!media ||
|
||||||
|
media.status4k === MediaStatus.UNKNOWN ||
|
||||||
|
(media.status4k === MediaStatus.DELETED && !active4kRequest)) &&
|
||||||
hasPermission(
|
hasPermission(
|
||||||
[
|
[
|
||||||
Permission.REQUEST_4K,
|
Permission.REQUEST_4K,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import { MediaRequestStatus } from '@server/constants/media';
|
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type { MovieDetails } from '@server/models/Movie';
|
import type { MovieDetails } from '@server/models/Movie';
|
||||||
import type { TvDetails } from '@server/models/Tv';
|
import type { TvDetails } from '@server/models/Tv';
|
||||||
@@ -411,6 +411,15 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
|||||||
>
|
>
|
||||||
{intl.formatMessage(globalMessages.failed)}
|
{intl.formatMessage(globalMessages.failed)}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
) : requestData.status === MediaRequestStatus.PENDING &&
|
||||||
|
requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||||
|
MediaStatus.DELETED ? (
|
||||||
|
<Badge
|
||||||
|
badgeType="warning"
|
||||||
|
href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(globalMessages.pending)}
|
||||||
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
status={
|
status={
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import { MediaRequestStatus } from '@server/constants/media';
|
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type { MovieDetails } from '@server/models/Movie';
|
import type { MovieDetails } from '@server/models/Movie';
|
||||||
import type { TvDetails } from '@server/models/Tv';
|
import type { TvDetails } from '@server/models/Tv';
|
||||||
@@ -474,6 +474,15 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
>
|
>
|
||||||
{intl.formatMessage(globalMessages.failed)}
|
{intl.formatMessage(globalMessages.failed)}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
) : requestData.status === MediaRequestStatus.PENDING &&
|
||||||
|
requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||||
|
MediaStatus.DELETED ? (
|
||||||
|
<Badge
|
||||||
|
badgeType="warning"
|
||||||
|
href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(globalMessages.pending)}
|
||||||
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
status={
|
status={
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ enum Filter {
|
|||||||
AVAILABLE = 'available',
|
AVAILABLE = 'available',
|
||||||
UNAVAILABLE = 'unavailable',
|
UNAVAILABLE = 'unavailable',
|
||||||
FAILED = 'failed',
|
FAILED = 'failed',
|
||||||
|
DELETED = 'deleted',
|
||||||
}
|
}
|
||||||
|
|
||||||
type Sort = 'added' | 'modified';
|
type Sort = 'added' | 'modified';
|
||||||
@@ -177,6 +178,9 @@ const RequestList = () => {
|
|||||||
<option value="unavailable">
|
<option value="unavailable">
|
||||||
{intl.formatMessage(globalMessages.unavailable)}
|
{intl.formatMessage(globalMessages.unavailable)}
|
||||||
</option>
|
</option>
|
||||||
|
<option value="deleted">
|
||||||
|
{intl.formatMessage(globalMessages.deleted)}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2 flex flex-grow sm:mb-0 lg:flex-grow-0">
|
<div className="mb-2 flex flex-grow sm:mb-0 lg:flex-grow-0">
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ const CollectionRequestModal = ({
|
|||||||
.filter(
|
.filter(
|
||||||
(request) =>
|
(request) =>
|
||||||
request.is4k === is4k &&
|
request.is4k === is4k &&
|
||||||
request.status !== MediaRequestStatus.DECLINED
|
request.status !== MediaRequestStatus.DECLINED &&
|
||||||
|
request.status !== MediaRequestStatus.COMPLETED
|
||||||
)
|
)
|
||||||
.map((part) => part.id),
|
.map((part) => part.id),
|
||||||
];
|
];
|
||||||
@@ -167,7 +168,9 @@ const CollectionRequestModal = ({
|
|||||||
|
|
||||||
return (part?.mediaInfo?.requests ?? []).find(
|
return (part?.mediaInfo?.requests ?? []).find(
|
||||||
(request) =>
|
(request) =>
|
||||||
request.is4k === is4k && request.status !== MediaRequestStatus.DECLINED
|
request.is4k === is4k &&
|
||||||
|
request.status !== MediaRequestStatus.DECLINED &&
|
||||||
|
request.status !== MediaRequestStatus.COMPLETED
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -342,7 +345,9 @@ const CollectionRequestModal = ({
|
|||||||
const partMedia =
|
const partMedia =
|
||||||
part.mediaInfo &&
|
part.mediaInfo &&
|
||||||
part.mediaInfo[is4k ? 'status4k' : 'status'] !==
|
part.mediaInfo[is4k ? 'status4k' : 'status'] !==
|
||||||
MediaStatus.UNKNOWN
|
MediaStatus.UNKNOWN &&
|
||||||
|
part.mediaInfo[is4k ? 'status4k' : 'status'] !==
|
||||||
|
MediaStatus.DELETED
|
||||||
? part.mediaInfo
|
? part.mediaInfo
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -243,7 +243,8 @@ const TvRequestModal = ({
|
|||||||
.filter(
|
.filter(
|
||||||
(request) =>
|
(request) =>
|
||||||
request.is4k === is4k &&
|
request.is4k === is4k &&
|
||||||
request.status !== MediaRequestStatus.DECLINED
|
request.status !== MediaRequestStatus.DECLINED &&
|
||||||
|
request.status !== MediaRequestStatus.COMPLETED
|
||||||
)
|
)
|
||||||
.reduce((requestedSeasons, request) => {
|
.reduce((requestedSeasons, request) => {
|
||||||
return [
|
return [
|
||||||
@@ -341,7 +342,8 @@ const TvRequestModal = ({
|
|||||||
(data.mediaInfo.requests || []).filter(
|
(data.mediaInfo.requests || []).filter(
|
||||||
(request) =>
|
(request) =>
|
||||||
request.is4k === is4k &&
|
request.is4k === is4k &&
|
||||||
request.status !== MediaRequestStatus.DECLINED
|
request.status !== MediaRequestStatus.DECLINED &&
|
||||||
|
request.status !== MediaRequestStatus.COMPLETED
|
||||||
).length > 0
|
).length > 0
|
||||||
) {
|
) {
|
||||||
data.mediaInfo.requests
|
data.mediaInfo.requests
|
||||||
@@ -349,7 +351,9 @@ const TvRequestModal = ({
|
|||||||
.forEach((request) => {
|
.forEach((request) => {
|
||||||
if (!seasonRequest) {
|
if (!seasonRequest) {
|
||||||
seasonRequest = request.seasons.find(
|
seasonRequest = request.seasons.find(
|
||||||
(season) => season.seasonNumber === seasonNumber
|
(season) =>
|
||||||
|
season.seasonNumber === seasonNumber &&
|
||||||
|
season.status !== MediaRequestStatus.COMPLETED
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -569,7 +573,9 @@ const TvRequestModal = ({
|
|||||||
(sn) =>
|
(sn) =>
|
||||||
sn.seasonNumber === season.seasonNumber &&
|
sn.seasonNumber === season.seasonNumber &&
|
||||||
sn[is4k ? 'status4k' : 'status'] !==
|
sn[is4k ? 'status4k' : 'status'] !==
|
||||||
MediaStatus.UNKNOWN
|
MediaStatus.UNKNOWN &&
|
||||||
|
sn[is4k ? 'status4k' : 'status'] !==
|
||||||
|
MediaStatus.DELETED
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<tr key={`season-${season.id}`}>
|
<tr key={`season-${season.id}`}>
|
||||||
|
|||||||
@@ -299,6 +299,17 @@ const StatusBadge = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case MediaStatus.DELETED:
|
||||||
|
return (
|
||||||
|
<Tooltip content={mediaLinkDescription}>
|
||||||
|
<Badge badgeType="danger">
|
||||||
|
{intl.formatMessage(is4k ? messages.status4k : messages.status, {
|
||||||
|
status: intl.formatMessage(globalMessages.deleted),
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,7 +207,9 @@ const TitleCard = ({
|
|||||||
<div
|
<div
|
||||||
className={`px-2 text-white ${
|
className={`px-2 text-white ${
|
||||||
!showRequestButton ||
|
!showRequestButton ||
|
||||||
(currentStatus && currentStatus !== MediaStatus.UNKNOWN)
|
(currentStatus &&
|
||||||
|
currentStatus !== MediaStatus.UNKNOWN &&
|
||||||
|
currentStatus !== MediaStatus.DELETED)
|
||||||
? 'pb-2'
|
? 'pb-2'
|
||||||
: 'pb-11'
|
: 'pb-11'
|
||||||
}`}
|
}`}
|
||||||
@@ -235,7 +237,8 @@ const TitleCard = ({
|
|||||||
WebkitLineClamp:
|
WebkitLineClamp:
|
||||||
!showRequestButton ||
|
!showRequestButton ||
|
||||||
(currentStatus &&
|
(currentStatus &&
|
||||||
currentStatus !== MediaStatus.UNKNOWN)
|
currentStatus !== MediaStatus.UNKNOWN &&
|
||||||
|
currentStatus !== MediaStatus.DELETED)
|
||||||
? 5
|
? 5
|
||||||
: 3,
|
: 3,
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
@@ -253,7 +256,9 @@ const TitleCard = ({
|
|||||||
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-2 py-2">
|
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-2 py-2">
|
||||||
{showRequestButton &&
|
{showRequestButton &&
|
||||||
(!currentStatus || currentStatus === MediaStatus.UNKNOWN) && (
|
(!currentStatus ||
|
||||||
|
currentStatus === MediaStatus.UNKNOWN ||
|
||||||
|
currentStatus === MediaStatus.DELETED) && (
|
||||||
<Button
|
<Button
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
buttonSize="sm"
|
buttonSize="sm"
|
||||||
|
|||||||
@@ -234,7 +234,8 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
.filter(
|
.filter(
|
||||||
(request) =>
|
(request) =>
|
||||||
request.is4k === is4k &&
|
request.is4k === is4k &&
|
||||||
request.status !== MediaRequestStatus.DECLINED
|
request.status !== MediaRequestStatus.DECLINED &&
|
||||||
|
request.status !== MediaRequestStatus.COMPLETED
|
||||||
)
|
)
|
||||||
.reduce((requestedSeasons, request) => {
|
.reduce((requestedSeasons, request) => {
|
||||||
return [
|
return [
|
||||||
@@ -647,6 +648,18 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{mSeason?.status === MediaStatus.DELETED && (
|
||||||
|
<>
|
||||||
|
<div className="hidden md:flex">
|
||||||
|
<Badge badgeType="danger">
|
||||||
|
{intl.formatMessage(globalMessages.deleted)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex md:hidden">
|
||||||
|
<StatusBadgeMini status={MediaStatus.DELETED} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{((!mSeason4k &&
|
{((!mSeason4k &&
|
||||||
request4k?.status ===
|
request4k?.status ===
|
||||||
MediaRequestStatus.APPROVED) ||
|
MediaRequestStatus.APPROVED) ||
|
||||||
@@ -733,6 +746,26 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{mSeason4k?.status4k === MediaStatus.DELETED &&
|
||||||
|
show4k && (
|
||||||
|
<>
|
||||||
|
<div className="hidden md:flex">
|
||||||
|
<Badge badgeType="danger">
|
||||||
|
{intl.formatMessage(messages.status4k, {
|
||||||
|
status: intl.formatMessage(
|
||||||
|
globalMessages.deleted
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex md:hidden">
|
||||||
|
<StatusBadgeMini
|
||||||
|
status={MediaStatus.DELETED}
|
||||||
|
is4k={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={`${
|
className={`${
|
||||||
open ? 'rotate-180' : ''
|
open ? 'rotate-180' : ''
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { defineMessages } from 'react-intl';
|
|||||||
const globalMessages = defineMessages({
|
const globalMessages = defineMessages({
|
||||||
available: 'Available',
|
available: 'Available',
|
||||||
partiallyavailable: 'Partially Available',
|
partiallyavailable: 'Partially Available',
|
||||||
|
deleted: 'Deleted',
|
||||||
processing: 'Processing',
|
processing: 'Processing',
|
||||||
unavailable: 'Unavailable',
|
unavailable: 'Unavailable',
|
||||||
notrequested: 'Not Requested',
|
notrequested: 'Not Requested',
|
||||||
@@ -14,6 +15,7 @@ const globalMessages = defineMessages({
|
|||||||
pending: 'Pending',
|
pending: 'Pending',
|
||||||
declined: 'Declined',
|
declined: 'Declined',
|
||||||
approved: 'Approved',
|
approved: 'Approved',
|
||||||
|
completed: 'Completed',
|
||||||
movie: 'Movie',
|
movie: 'Movie',
|
||||||
movies: 'Movies',
|
movies: 'Movies',
|
||||||
collection: 'Collection',
|
collection: 'Collection',
|
||||||
|
|||||||
@@ -1190,9 +1190,11 @@
|
|||||||
"i18n.canceling": "Canceling…",
|
"i18n.canceling": "Canceling…",
|
||||||
"i18n.close": "Close",
|
"i18n.close": "Close",
|
||||||
"i18n.collection": "Collection",
|
"i18n.collection": "Collection",
|
||||||
|
"i18n.completed": "Completed",
|
||||||
"i18n.decline": "Decline",
|
"i18n.decline": "Decline",
|
||||||
"i18n.declined": "Declined",
|
"i18n.declined": "Declined",
|
||||||
"i18n.delete": "Delete",
|
"i18n.delete": "Delete",
|
||||||
|
"i18n.deleted": "Deleted",
|
||||||
"i18n.deleting": "Deleting…",
|
"i18n.deleting": "Deleting…",
|
||||||
"i18n.delimitedlist": "{a}, {b}",
|
"i18n.delimitedlist": "{a}, {b}",
|
||||||
"i18n.edit": "Edit",
|
"i18n.edit": "Edit",
|
||||||
|
|||||||
Reference in New Issue
Block a user