Compare commits

..

2 Commits

Author SHA1 Message Date
sct
b4c29acc9e build: try to fix sqlite for arm 2023-11-06 10:04:14 +00:00
sct
e3c2c1c2d5 build: add global node-gyp for arm 2023-11-06 09:56:33 +00:00
30 changed files with 434 additions and 606 deletions

View File

@@ -35,7 +35,7 @@ jobs:
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
build-args: |
COMMIT_TAG=${{ github.sha }}

View File

@@ -1053,7 +1053,7 @@ components:
status:
type: number
example: 0
description: Availability of the media. 1 = `UNKNOWN`, 2 = `PENDING`, 3 = `PROCESSING`, 4 = `PARTIALLY_AVAILABLE`, 5 = `AVAILABLE`, 6 = `DELETED`
description: Availability of the media. 1 = `UNKNOWN`, 2 = `PENDING`, 3 = `PROCESSING`, 4 = `PARTIALLY_AVAILABLE`, 5 = `AVAILABLE`
requests:
type: array
readOnly: true
@@ -4955,7 +4955,6 @@ paths:
processing,
unavailable,
failed,
deleted,
]
- in: query
name: sort
@@ -5695,16 +5694,7 @@ paths:
schema:
type: string
nullable: true
enum:
[
all,
available,
partial,
allavailable,
processing,
pending,
deleted,
]
enum: [all, available, partial, allavailable, processing, pending]
- in: query
name: sort
schema:
@@ -5763,7 +5753,7 @@ paths:
example: available
schema:
type: string
enum: [available, partial, processing, pending, unknown, deleted]
enum: [available, partial, processing, pending, unknown]
requestBody:
content:
application/json:

View File

@@ -86,7 +86,7 @@
"reflect-metadata": "0.1.13",
"secure-random-password": "0.2.3",
"semver": "7.3.8",
"sqlite3": "5.1.4",
"sqlite3": "5.1.6",
"swagger-ui-express": "4.6.2",
"swr": "2.0.4",
"typeorm": "0.3.12",
@@ -166,7 +166,6 @@
"typescript": "4.9.5"
},
"resolutions": {
"sqlite3/node-gyp": "8.4.1",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/express-session": "1.17.6"

View File

@@ -3,7 +3,6 @@ export enum MediaRequestStatus {
APPROVED,
DECLINED,
FAILED,
COMPLETED,
}
export enum MediaType {
@@ -17,5 +16,4 @@ export enum MediaStatus {
PROCESSING,
PARTIALLY_AVAILABLE,
AVAILABLE,
DELETED,
}

View File

@@ -1,10 +1,6 @@
import RadarrAPI from '@server/api/servarr/radarr';
import SonarrAPI from '@server/api/servarr/sonarr';
import {
MediaRequestStatus,
MediaStatus,
MediaType,
} from '@server/constants/media';
import { MediaStatus, MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import type { DownloadingItem } from '@server/lib/downloadtracker';
import downloadTracker from '@server/lib/downloadtracker';
@@ -12,7 +8,6 @@ import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import {
AfterLoad,
AfterUpdate,
Column,
CreateDateColumn,
Entity,
@@ -314,37 +309,6 @@ 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;

View File

@@ -167,8 +167,7 @@ export class MediaRequest {
// If there is an existing movie request that isn't declined, don't allow a new one.
if (
requestBody.mediaType === MediaType.MOVIE &&
existing[0].status !== MediaRequestStatus.DECLINED &&
existing[0].status !== MediaRequestStatus.COMPLETED
existing[0].status !== MediaRequestStatus.DECLINED
) {
logger.warn('Duplicate request for media blocked', {
tmdbId: tmdbMedia.id,
@@ -261,8 +260,7 @@ export class MediaRequest {
.filter(
(request) =>
request.is4k === requestBody.is4k &&
request.status !== MediaRequestStatus.DECLINED &&
request.status !== MediaRequestStatus.COMPLETED
request.status !== MediaRequestStatus.DECLINED
)
.reduce((seasons, request) => {
const combinedSeasons = request.seasons.map(
@@ -281,9 +279,7 @@ export class MediaRequest {
.filter(
(season) =>
season[requestBody.is4k ? 'status4k' : 'status'] !==
MediaStatus.UNKNOWN &&
season[requestBody.is4k ? 'status4k' : 'status'] !==
MediaStatus.DELETED
MediaStatus.UNKNOWN
)
.map((season) => season.seasonNumber),
];
@@ -587,8 +583,7 @@ export class MediaRequest {
if (
media.mediaType === MediaType.MOVIE &&
this.status === MediaRequestStatus.DECLINED &&
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
this.status === MediaRequestStatus.DECLINED
) {
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
@@ -606,8 +601,7 @@ export class MediaRequest {
media.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING
).length === 0 &&
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING &&
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING
) {
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);

View File

@@ -1,8 +1,5 @@
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import SeasonRequest from '@server/entity/SeasonRequest';
import { MediaStatus } from '@server/constants/media';
import {
AfterUpdate,
Column,
CreateDateColumn,
Entity,
@@ -38,37 +35,6 @@ class Season {
constructor(init?: Partial<Season>) {
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;

View File

@@ -1,7 +1,7 @@
import { MediaRequestStatus } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import {
AfterUpdate,
AfterRemove,
Column,
CreateDateColumn,
Entity,
@@ -37,25 +37,15 @@ class SeasonRequest {
Object.assign(this, init);
}
@AfterUpdate()
public async updateMediaRequests(): Promise<void> {
const requestRepository = getRepository(MediaRequest);
const relatedRequest = await requestRepository.findOne({
@AfterRemove()
public async handleRemoveParent(): Promise<void> {
const mediaRequestRepository = getRepository(MediaRequest);
const requestToBeDeleted = await mediaRequestRepository.findOneOrFail({
where: { id: this.request.id },
});
// Check the parent of the season request and
// 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);
if (requestToBeDeleted.seasons.length === 0) {
await mediaRequestRepository.delete({ id: this.request.id });
}
}
}

View File

@@ -8,6 +8,7 @@ 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';
@@ -242,66 +243,105 @@ 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 {
// If media type is tv, check if a season is processing
//to see if we need to keep the external metadata
let isMediaProcessing = false;
// 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.mediaType === 'tv') {
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;
}
mediaStatus = this.findMediaStatus(requests, is4k);
}
// 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 ? 'status4k' : 'status'] = mediaStatus;
media[is4k ? 'serviceId4k' : 'serviceId'] =
mediaStatus === MediaStatus.PROCESSING
? media[is4k ? 'serviceId4k' : 'serviceId']
: null;
media[is4k ? 'externalServiceId4k' : 'externalServiceId'] =
isMediaProcessing
mediaStatus === MediaStatus.PROCESSING
? media[is4k ? 'externalServiceId4k' : 'externalServiceId']
: null;
media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
isMediaProcessing
mediaStatus === MediaStatus.PROCESSING
? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']
: null;
media[is4k ? 'ratingKey4k' : 'ratingKey'] = isMediaProcessing
? media[is4k ? 'ratingKey4k' : 'ratingKey']
: null;
media[is4k ? 'ratingKey4k' : 'ratingKey'] =
mediaStatus === MediaStatus.PROCESSING
? 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 deleted.`,
} and Plex instance. Status will be changed to unknown.`,
{ label: 'AvailabilitySync' }
);
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'} ${
@@ -321,21 +361,35 @@ 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.DELETED;
mediaSeason[is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
}
}
@@ -357,12 +411,16 @@ class AvailabilitySync {
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 deleted.`,
} and Plex instance. Status will be changed to unknown.`,
{ label: 'AvailabilitySync' }
);
} catch (ex) {
@@ -387,43 +445,41 @@ 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) {
if (server.is4k === is4k) {
const radarrAPI = new RadarrAPI({
apiKey: server.apiKey,
url: RadarrAPI.buildUrl(server, '/api/v3'),
});
const radarrAPI = new RadarrAPI({
apiKey: server.apiKey,
url: RadarrAPI.buildUrl(server, '/api/v3'),
});
try {
let radarr: RadarrMovie | undefined;
try {
let radarr: RadarrMovie | undefined;
if (media.externalServiceId && !is4k) {
radarr = await radarrAPI.getMovie({
id: media.externalServiceId,
});
}
if (!server.is4k && media.externalServiceId && !is4k) {
radarr = await radarrAPI.getMovie({
id: media.externalServiceId,
});
}
if (media.externalServiceId4k && is4k) {
radarr = await radarrAPI.getMovie({
id: media.externalServiceId4k,
});
}
if (server.is4k && media.externalServiceId4k && is4k) {
radarr = await radarrAPI.getMovie({
id: media.externalServiceId4k,
});
}
if (radarr && radarr.hasFile) {
existsInRadarr = true;
}
} catch (ex) {
if (!ex.message.includes('404')) {
existsInRadarr = true;
logger.debug(
`Failure retrieving the ${
is4k ? '4K' : 'non-4K'
} movie [TMDB ID ${media.tmdbId}] from Radarr.`,
{
errorMessage: ex.message,
label: 'AvailabilitySync',
}
);
}
if (radarr && radarr.hasFile) {
existsInRadarr = true;
}
} catch (ex) {
if (!ex.message.includes('404')) {
existsInRadarr = true;
logger.debug(
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
media.tmdbId
}] from Radarr.`,
{
errorMessage: ex.message,
label: 'AvailabilitySync',
}
);
}
}
}
@@ -441,45 +497,42 @@ 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) {
if (server.is4k === is4k) {
const sonarrAPI = new SonarrAPI({
apiKey: server.apiKey,
url: SonarrAPI.buildUrl(server, '/api/v3'),
});
const sonarrAPI = new SonarrAPI({
apiKey: server.apiKey,
url: SonarrAPI.buildUrl(server, '/api/v3'),
});
try {
let sonarr: SonarrSeries | undefined;
try {
let sonarr: SonarrSeries | undefined;
if (media.externalServiceId && !is4k) {
sonarr = await sonarrAPI.getSeriesById(media.externalServiceId);
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
sonarr.seasons;
}
if (!server.is4k && media.externalServiceId && !is4k) {
sonarr = await sonarrAPI.getSeriesById(media.externalServiceId);
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
sonarr.seasons;
}
if (media.externalServiceId4k && is4k) {
sonarr = await sonarrAPI.getSeriesById(media.externalServiceId4k);
this.sonarrSeasonsCache[
`${server.id}-${media.externalServiceId4k}`
] = sonarr.seasons;
}
if (server.is4k && media.externalServiceId4k && is4k) {
sonarr = await sonarrAPI.getSeriesById(media.externalServiceId4k);
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] =
sonarr.seasons;
}
if (sonarr && sonarr.statistics.episodeFileCount > 0) {
existsInSonarr = true;
}
} catch (ex) {
if (!ex.message.includes('404')) {
existsInSonarr = true;
preventSeasonSearch = true;
logger.debug(
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} show [TMDB ID ${
media.tmdbId
}] from Sonarr.`,
{
errorMessage: ex.message,
label: 'AvailabilitySync',
}
);
}
if (sonarr && sonarr.statistics.episodeFileCount > 0) {
existsInSonarr = true;
}
} catch (ex) {
if (!ex.message.includes('404')) {
existsInSonarr = true;
preventSeasonSearch = true;
logger.debug(
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} show [TMDB ID ${
media.tmdbId
}] from Sonarr.`,
{
errorMessage: ex.message,
label: 'AvailabilitySync',
}
);
}
}
}
@@ -669,5 +722,4 @@ class AvailabilitySync {
}
const availabilitySync = new AvailabilitySync();
export default availabilitySync;

View File

@@ -14,12 +14,7 @@ import {
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
interface PushoverImagePayload {
attachment_base64: string;
attachment_type: string;
}
interface PushoverPayload extends PushoverImagePayload {
interface PushoverPayload {
token: string;
user: string;
title: string;
@@ -48,36 +43,10 @@ class PushoverAgent
return true;
}
private async getImagePayload(
imageUrl: string
): Promise<Partial<PushoverImagePayload>> {
try {
const response = await axios.get(imageUrl, {
responseType: 'arraybuffer',
});
const base64 = Buffer.from(response.data, 'binary').toString('base64');
const contentType = (
response.headers['Content-Type'] || response.headers['content-type']
)?.toString();
return {
attachment_base64: base64,
attachment_type: contentType,
};
} catch (e) {
logger.error('Error getting image payload', {
label: 'Notifications',
errorMessage: e.message,
response: e.response?.data,
});
return {};
}
}
private async getNotificationPayload(
private getNotificationPayload(
type: Notification,
payload: NotificationPayload
): Promise<Partial<PushoverPayload>> {
): Partial<PushoverPayload> {
const { applicationUrl, applicationTitle } = getSettings().main;
const title = payload.event ?? payload.subject;
@@ -153,16 +122,6 @@ class PushoverAgent
? `View ${payload.issue ? 'Issue' : 'Media'} in ${applicationTitle}`
: undefined;
let attachment_base64;
let attachment_type;
if (payload.image) {
const imagePayload = await this.getImagePayload(payload.image);
if (imagePayload.attachment_base64 && imagePayload.attachment_type) {
attachment_base64 = imagePayload.attachment_base64;
attachment_type = imagePayload.attachment_type;
}
}
return {
title,
message,
@@ -170,8 +129,6 @@ class PushoverAgent
url_title,
priority,
html: 1,
attachment_base64,
attachment_type,
};
}
@@ -181,10 +138,7 @@ class PushoverAgent
): Promise<boolean> {
const settings = this.getSettings();
const endpoint = 'https://api.pushover.net/1/messages.json';
const notificationPayload = await this.getNotificationPayload(
type,
payload
);
const notificationPayload = this.getNotificationPayload(type, payload);
// Send system notification
if (

View File

@@ -281,9 +281,7 @@ class BaseScanner<T> {
? MediaStatus.AVAILABLE
: season.episodes > 0
? MediaStatus.PARTIALLY_AVAILABLE
: !season.is4kOverride &&
season.processing &&
existingSeason.status !== MediaStatus.DELETED
: !season.is4kOverride && season.processing
? MediaStatus.PROCESSING
: existingSeason.status;
@@ -296,9 +294,7 @@ class BaseScanner<T> {
? MediaStatus.AVAILABLE
: this.enable4kShow && season.episodes4k > 0
? MediaStatus.PARTIALLY_AVAILABLE
: season.is4kOverride &&
season.processing &&
existingSeason.status4k !== MediaStatus.DELETED
: season.is4kOverride && season.processing
? MediaStatus.PROCESSING
: existingSeason.status4k;
} else {
@@ -405,18 +401,13 @@ class BaseScanner<T> {
// the status
const shouldStayAvailable =
media.status === MediaStatus.AVAILABLE &&
newSeasons.filter(
(season) =>
season.status !== MediaStatus.UNKNOWN &&
season.status !== MediaStatus.DELETED
).length === 0;
newSeasons.filter((season) => season.status !== MediaStatus.UNKNOWN)
.length === 0;
const shouldStayAvailable4k =
media.status4k === MediaStatus.AVAILABLE &&
newSeasons.filter(
(season) =>
season.status4k !== MediaStatus.UNKNOWN &&
season.status4k !== MediaStatus.DELETED
).length === 0;
newSeasons.filter((season) => season.status4k !== MediaStatus.UNKNOWN)
.length === 0;
media.status =
isAllStandardSeasons || shouldStayAvailable
? MediaStatus.AVAILABLE
@@ -426,13 +417,11 @@ class BaseScanner<T> {
season.status === MediaStatus.AVAILABLE
)
? MediaStatus.PARTIALLY_AVAILABLE
: (!seasons.length && media.status !== MediaStatus.DELETED) ||
: !seasons.length ||
media.seasons.some(
(season) => season.status === MediaStatus.PROCESSING
)
? MediaStatus.PROCESSING
: media.status === MediaStatus.DELETED
? MediaStatus.DELETED
: MediaStatus.UNKNOWN;
media.status4k =
(isAll4kSeasons || shouldStayAvailable4k) && this.enable4kShow
@@ -444,13 +433,11 @@ class BaseScanner<T> {
season.status4k === MediaStatus.AVAILABLE
)
? MediaStatus.PARTIALLY_AVAILABLE
: (!seasons.length && media.status4k !== MediaStatus.DELETED) ||
: !seasons.length ||
media.seasons.some(
(season) => season.status4k === MediaStatus.PROCESSING
)
? MediaStatus.PROCESSING
: media.status4k === MediaStatus.DELETED
? MediaStatus.DELETED
: MediaStatus.UNKNOWN;
await mediaRepository.save(media);
this.log(`Updating existing title: ${title}`);

View File

@@ -1,13 +1,7 @@
import TautulliAPI from '@server/api/tautulli';
import {
MediaRequestStatus,
MediaStatus,
MediaType,
} from '@server/constants/media';
import { MediaStatus, MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
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 type {
MediaResultsResponse,
@@ -104,8 +98,6 @@ mediaRoutes.post<
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => {
const mediaRepository = getRepository(Media);
const requestRepository = getRepository(MediaRequest);
const seasonRequestRepository = getRepository(SeasonRequest);
const media = await mediaRepository.findOne({
where: { id: Number(req.params.id) },
@@ -146,35 +138,6 @@ mediaRoutes.post<
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);
return res.status(200).json(media);

View File

@@ -40,6 +40,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
switch (req.query.filter) {
case 'approved':
case 'processing':
case 'available':
statusFilter = [MediaRequestStatus.APPROVED];
break;
case 'pending':
@@ -54,18 +55,12 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
case 'failed':
statusFilter = [MediaRequestStatus.FAILED];
break;
case 'completed':
case 'available':
case 'deleted':
statusFilter = [MediaRequestStatus.COMPLETED];
break;
default:
statusFilter = [
MediaRequestStatus.PENDING,
MediaRequestStatus.APPROVED,
MediaRequestStatus.DECLINED,
MediaRequestStatus.FAILED,
MediaRequestStatus.COMPLETED,
];
}
@@ -84,9 +79,6 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
MediaStatus.PARTIALLY_AVAILABLE,
];
break;
case 'deleted':
mediaStatusFilter = [MediaStatus.DELETED];
break;
default:
mediaStatusFilter = [
MediaStatus.UNKNOWN,
@@ -94,7 +86,6 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
MediaStatus.PROCESSING,
MediaStatus.PARTIALLY_AVAILABLE,
MediaStatus.AVAILABLE,
MediaStatus.DELETED,
];
}
@@ -400,8 +391,7 @@ requestRoutes.put<{ requestId: string }>(
(r) =>
r.is4k === request.is4k &&
r.id !== request.id &&
r.status !== MediaRequestStatus.DECLINED &&
r.status !== MediaRequestStatus.COMPLETED
r.status !== MediaRequestStatus.DECLINED
)
.reduce((seasons, r) => {
const combinedSeasons = r.seasons.map(

View File

@@ -1,128 +0,0 @@
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;
}
}

View File

@@ -1,12 +1,179 @@
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import TheMovieDb from '@server/api/themoviedb';
import {
MediaRequestStatus,
MediaStatus,
MediaType,
} from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
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 { EventSubscriber } from 'typeorm';
import { EventSubscriber, In, Not } from 'typeorm';
@EventSubscriber()
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) {
const requestRepository = getRepository(MediaRequest);
@@ -30,6 +197,52 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
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 (
event.entity.status === MediaStatus.AVAILABLE &&
event.databaseEntity.status === MediaStatus.PENDING

View File

@@ -16,7 +16,7 @@ architectures:
parts:
overseerr:
plugin: nodejs
nodejs-version: '18.18.2'
nodejs-version: '18.8.2'
nodejs-package-manager: 'yarn'
nodejs-yarn-version: v1.22.19
build-packages:

View File

@@ -1,11 +1,6 @@
import Spinner from '@app/assets/spinner.svg';
import { CheckCircleIcon } from '@heroicons/react/20/solid';
import {
BellIcon,
ClockIcon,
MinusSmallIcon,
TrashIcon,
} from '@heroicons/react/24/solid';
import { BellIcon, ClockIcon, MinusSmallIcon } from '@heroicons/react/24/solid';
import { MediaStatus } from '@server/constants/media';
interface StatusBadgeMiniProps {
@@ -55,10 +50,6 @@ const StatusBadgeMini = ({
);
indicatorIcon = <MinusSmallIcon />;
break;
case MediaStatus.DELETED:
badgeStyle.push('bg-red-500 border-red-400 ring-red-400 text-red-100');
indicatorIcon = <TrashIcon />;
break;
}
if (inProgress) {

View File

@@ -204,11 +204,6 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
{intl.formatMessage(globalMessages.failed)}
</Badge>
)}
{request.status === MediaRequestStatus.COMPLETED && (
<Badge badgeType="success">
{intl.formatMessage(globalMessages.completed)}
</Badge>
)}
</div>
</div>
<div className="mt-2 flex items-center text-sm leading-5 sm:mt-0">

View File

@@ -264,9 +264,7 @@ const RequestButton = ({
// Standard request button
if (
(!media ||
media.status === MediaStatus.UNKNOWN ||
(media.status === MediaStatus.DELETED && !activeRequest)) &&
(!media || media.status === MediaStatus.UNKNOWN) &&
hasPermission(
[
Permission.REQUEST,
@@ -309,9 +307,7 @@ const RequestButton = ({
// 4K request button
if (
(!media ||
media.status4k === MediaStatus.UNKNOWN ||
(media.status4k === MediaStatus.DELETED && !active4kRequest)) &&
(!media || media.status4k === MediaStatus.UNKNOWN) &&
hasPermission(
[
Permission.REQUEST_4K,

View File

@@ -16,7 +16,7 @@ import {
TrashIcon,
XMarkIcon,
} from '@heroicons/react/24/solid';
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import { MediaRequestStatus } from '@server/constants/media';
import type { MediaRequest } from '@server/entity/MediaRequest';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
@@ -411,15 +411,6 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
>
{intl.formatMessage(globalMessages.failed)}
</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
status={

View File

@@ -15,7 +15,7 @@ import {
TrashIcon,
XMarkIcon,
} from '@heroicons/react/24/solid';
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import { MediaRequestStatus } from '@server/constants/media';
import type { MediaRequest } from '@server/entity/MediaRequest';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
@@ -474,15 +474,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
>
{intl.formatMessage(globalMessages.failed)}
</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
status={

View File

@@ -34,7 +34,6 @@ enum Filter {
AVAILABLE = 'available',
UNAVAILABLE = 'unavailable',
FAILED = 'failed',
DELETED = 'deleted',
}
type Sort = 'added' | 'modified';
@@ -178,9 +177,6 @@ const RequestList = () => {
<option value="unavailable">
{intl.formatMessage(globalMessages.unavailable)}
</option>
<option value="deleted">
{intl.formatMessage(globalMessages.deleted)}
</option>
</select>
</div>
<div className="mb-2 flex flex-grow sm:mb-0 lg:flex-grow-0">

View File

@@ -78,8 +78,7 @@ const CollectionRequestModal = ({
.filter(
(request) =>
request.is4k === is4k &&
request.status !== MediaRequestStatus.DECLINED &&
request.status !== MediaRequestStatus.COMPLETED
request.status !== MediaRequestStatus.DECLINED
)
.map((part) => part.id),
];
@@ -168,9 +167,7 @@ const CollectionRequestModal = ({
return (part?.mediaInfo?.requests ?? []).find(
(request) =>
request.is4k === is4k &&
request.status !== MediaRequestStatus.DECLINED &&
request.status !== MediaRequestStatus.COMPLETED
request.is4k === is4k && request.status !== MediaRequestStatus.DECLINED
);
};
@@ -345,9 +342,7 @@ const CollectionRequestModal = ({
const partMedia =
part.mediaInfo &&
part.mediaInfo[is4k ? 'status4k' : 'status'] !==
MediaStatus.UNKNOWN &&
part.mediaInfo[is4k ? 'status4k' : 'status'] !==
MediaStatus.DELETED
MediaStatus.UNKNOWN
? part.mediaInfo
: undefined;

View File

@@ -243,8 +243,7 @@ const TvRequestModal = ({
.filter(
(request) =>
request.is4k === is4k &&
request.status !== MediaRequestStatus.DECLINED &&
request.status !== MediaRequestStatus.COMPLETED
request.status !== MediaRequestStatus.DECLINED
)
.reduce((requestedSeasons, request) => {
return [
@@ -342,8 +341,7 @@ const TvRequestModal = ({
(data.mediaInfo.requests || []).filter(
(request) =>
request.is4k === is4k &&
request.status !== MediaRequestStatus.DECLINED &&
request.status !== MediaRequestStatus.COMPLETED
request.status !== MediaRequestStatus.DECLINED
).length > 0
) {
data.mediaInfo.requests
@@ -351,9 +349,7 @@ const TvRequestModal = ({
.forEach((request) => {
if (!seasonRequest) {
seasonRequest = request.seasons.find(
(season) =>
season.seasonNumber === seasonNumber &&
season.status !== MediaRequestStatus.COMPLETED
(season) => season.seasonNumber === seasonNumber
);
}
});
@@ -573,9 +569,7 @@ const TvRequestModal = ({
(sn) =>
sn.seasonNumber === season.seasonNumber &&
sn[is4k ? 'status4k' : 'status'] !==
MediaStatus.UNKNOWN &&
sn[is4k ? 'status4k' : 'status'] !==
MediaStatus.DELETED
MediaStatus.UNKNOWN
);
return (
<tr key={`season-${season.id}`}>

View File

@@ -299,17 +299,6 @@ const StatusBadge = ({
</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:
return null;
}

View File

@@ -207,9 +207,7 @@ const TitleCard = ({
<div
className={`px-2 text-white ${
!showRequestButton ||
(currentStatus &&
currentStatus !== MediaStatus.UNKNOWN &&
currentStatus !== MediaStatus.DELETED)
(currentStatus && currentStatus !== MediaStatus.UNKNOWN)
? 'pb-2'
: 'pb-11'
}`}
@@ -237,8 +235,7 @@ const TitleCard = ({
WebkitLineClamp:
!showRequestButton ||
(currentStatus &&
currentStatus !== MediaStatus.UNKNOWN &&
currentStatus !== MediaStatus.DELETED)
currentStatus !== MediaStatus.UNKNOWN)
? 5
: 3,
display: '-webkit-box',
@@ -256,9 +253,7 @@ const TitleCard = ({
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-2 py-2">
{showRequestButton &&
(!currentStatus ||
currentStatus === MediaStatus.UNKNOWN ||
currentStatus === MediaStatus.DELETED) && (
(!currentStatus || currentStatus === MediaStatus.UNKNOWN) && (
<Button
buttonType="primary"
buttonSize="sm"

View File

@@ -234,8 +234,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
.filter(
(request) =>
request.is4k === is4k &&
request.status !== MediaRequestStatus.DECLINED &&
request.status !== MediaRequestStatus.COMPLETED
request.status !== MediaRequestStatus.DECLINED
)
.reduce((requestedSeasons, request) => {
return [
@@ -648,18 +647,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
</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 &&
request4k?.status ===
MediaRequestStatus.APPROVED) ||
@@ -746,26 +733,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
</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
className={`${
open ? 'rotate-180' : ''

View File

@@ -3,7 +3,6 @@ import { defineMessages } from 'react-intl';
const globalMessages = defineMessages({
available: 'Available',
partiallyavailable: 'Partially Available',
deleted: 'Deleted',
processing: 'Processing',
unavailable: 'Unavailable',
notrequested: 'Not Requested',
@@ -15,7 +14,6 @@ const globalMessages = defineMessages({
pending: 'Pending',
declined: 'Declined',
approved: 'Approved',
completed: 'Completed',
movie: 'Movie',
movies: 'Movies',
collection: 'Collection',

View File

@@ -1190,11 +1190,9 @@
"i18n.canceling": "Canceling…",
"i18n.close": "Close",
"i18n.collection": "Collection",
"i18n.completed": "Completed",
"i18n.decline": "Decline",
"i18n.declined": "Declined",
"i18n.delete": "Delete",
"i18n.deleted": "Deleted",
"i18n.deleting": "Deleting…",
"i18n.delimitedlist": "{a}, {b}",
"i18n.edit": "Edit",

View File

@@ -10179,7 +10179,7 @@ node-fetch@^2.6.0, node-fetch@^2.6.7:
dependencies:
whatwg-url "^5.0.0"
node-gyp@8.4.1, node-gyp@8.x:
node-gyp@8.x:
version "8.4.1"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937"
integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==
@@ -12603,10 +12603,10 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
sqlite3@5.1.4:
version "5.1.4"
resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.4.tgz#35f83d368963168b324ad2f0fffce09f3b8723a7"
integrity sha512-i0UlWAzPlzX3B5XP2cYuhWQJsTtlMD6obOa1PgeEQ4DHEXUuyJkgv50I3isqZAP5oFc2T8OFvakmDh2W6I+YpA==
sqlite3@5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.6.tgz#1d4fbc90fe4fbd51e952e0a90fd8f6c2b9098e97"
integrity sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.0"
node-addon-api "^4.2.0"