mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat: simple failed request handling (#474)
When a movie or series is added with radarr or sonarr, if it fails, this changes the media state to unknown and sends a notification to admins. Client side this will look like a failed state along with a retry button that will delete the request and re-queue it.
This commit is contained in:
@@ -2200,6 +2200,30 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: Succesfully removed request
|
description: Succesfully removed request
|
||||||
|
/request/{requestId}/retry:
|
||||||
|
post:
|
||||||
|
summary: Retry a failed request
|
||||||
|
description: |
|
||||||
|
Retries a request by resending requests to Sonarr or Radarr
|
||||||
|
|
||||||
|
Requires the `MANAGE_REQUESTS` permission or `ADMIN`
|
||||||
|
tags:
|
||||||
|
- request
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: requestId
|
||||||
|
description: Request ID
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: 1
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Retry triggered
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/MediaRequest'
|
||||||
/request/{requestId}/{status}:
|
/request/{requestId}/{status}:
|
||||||
get:
|
get:
|
||||||
summary: Update a requests status
|
summary: Update a requests status
|
||||||
|
@@ -76,7 +76,7 @@ class RadarrAPI {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public addMovie = async (options: RadarrMovieOptions): Promise<void> => {
|
public addMovie = async (options: RadarrMovieOptions): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.post<RadarrMovie>(`/movie`, {
|
const response = await this.axios.post<RadarrMovie>(`/movie`, {
|
||||||
title: options.title,
|
title: options.title,
|
||||||
@@ -104,7 +104,9 @@ class RadarrAPI {
|
|||||||
label: 'Radarr',
|
label: 'Radarr',
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
|
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
|
||||||
@@ -112,8 +114,13 @@ class RadarrAPI {
|
|||||||
label: 'Radarr',
|
label: 'Radarr',
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
options,
|
options,
|
||||||
|
response: e?.response?.data,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
if (e?.response?.data?.[0]?.errorCode === 'MovieExistsValidator') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -116,7 +116,7 @@ class SonarrAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addSeries(options: AddSeriesOptions): Promise<SonarrSeries> {
|
public async addSeries(options: AddSeriesOptions): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const series = await this.getSeriesByTvdbId(options.tvdbid);
|
const series = await this.getSeriesByTvdbId(options.tvdbid);
|
||||||
|
|
||||||
@@ -147,9 +147,10 @@ class SonarrAPI {
|
|||||||
label: 'Sonarr',
|
label: 'Sonarr',
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newSeriesResponse.data;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdSeriesResponse = await this.axios.post<SonarrSeries>(
|
const createdSeriesResponse = await this.axios.post<SonarrSeries>(
|
||||||
@@ -188,16 +189,18 @@ class SonarrAPI {
|
|||||||
label: 'Sonarr',
|
label: 'Sonarr',
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdSeriesResponse.data;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Something went wrong adding a series to Sonarr', {
|
logger.error('Something went wrong adding a series to Sonarr', {
|
||||||
label: 'Sonarr API',
|
label: 'Sonarr API',
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
error: e,
|
error: e,
|
||||||
|
response: e?.response?.data,
|
||||||
});
|
});
|
||||||
throw new Error('Failed to add series');
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -69,6 +69,12 @@ export class MediaRequest {
|
|||||||
Object.assign(this, init);
|
Object.assign(this, init);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterUpdate()
|
||||||
|
@AfterInsert()
|
||||||
|
public async sendMedia(): Promise<void> {
|
||||||
|
await Promise.all([this._sendToRadarr(), this._sendToSonarr()]);
|
||||||
|
}
|
||||||
|
|
||||||
@AfterInsert()
|
@AfterInsert()
|
||||||
private async _notifyNewRequest() {
|
private async _notifyNewRequest() {
|
||||||
if (this.status === MediaRequestStatus.PENDING) {
|
if (this.status === MediaRequestStatus.PENDING) {
|
||||||
@@ -163,7 +169,7 @@ export class MediaRequest {
|
|||||||
|
|
||||||
@AfterUpdate()
|
@AfterUpdate()
|
||||||
@AfterInsert()
|
@AfterInsert()
|
||||||
private async _updateParentStatus() {
|
public async updateParentStatus(): Promise<void> {
|
||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
const media = await mediaRepository.findOne({
|
const media = await mediaRepository.findOne({
|
||||||
where: { id: this.media.id },
|
where: { id: this.media.id },
|
||||||
@@ -229,14 +235,13 @@ export class MediaRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterUpdate()
|
|
||||||
@AfterInsert()
|
|
||||||
private async _sendToRadarr() {
|
private async _sendToRadarr() {
|
||||||
if (
|
if (
|
||||||
this.status === MediaRequestStatus.APPROVED &&
|
this.status === MediaRequestStatus.APPROVED &&
|
||||||
this.type === MediaType.MOVIE
|
this.type === MediaType.MOVIE
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const mediaRepository = getRepository(Media);
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
if (settings.radarr.length === 0 && !settings.radarr[0]) {
|
if (settings.radarr.length === 0 && !settings.radarr[0]) {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -268,7 +273,8 @@ export class MediaRequest {
|
|||||||
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
|
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
|
||||||
|
|
||||||
// Run this asynchronously so we don't wait for it on the UI side
|
// Run this asynchronously so we don't wait for it on the UI side
|
||||||
radarr.addMovie({
|
radarr
|
||||||
|
.addMovie({
|
||||||
profileId: radarrSettings.activeProfileId,
|
profileId: radarrSettings.activeProfileId,
|
||||||
qualityProfileId: radarrSettings.activeProfileId,
|
qualityProfileId: radarrSettings.activeProfileId,
|
||||||
rootFolderPath: radarrSettings.activeDirectory,
|
rootFolderPath: radarrSettings.activeDirectory,
|
||||||
@@ -278,6 +284,37 @@ export class MediaRequest {
|
|||||||
year: Number(movie.release_date.slice(0, 4)),
|
year: Number(movie.release_date.slice(0, 4)),
|
||||||
monitored: true,
|
monitored: true,
|
||||||
searchNow: true,
|
searchNow: true,
|
||||||
|
})
|
||||||
|
.then(async (success) => {
|
||||||
|
if (!success) {
|
||||||
|
const media = await mediaRepository.findOne({
|
||||||
|
where: { id: this.media.id },
|
||||||
|
});
|
||||||
|
if (!media) {
|
||||||
|
logger.error('Media not present');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
media.status = MediaStatus.UNKNOWN;
|
||||||
|
await mediaRepository.save(media);
|
||||||
|
logger.warn(
|
||||||
|
'Newly added movie request failed to add to Radarr, marking as unknown',
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
const admin = await userRepository.findOneOrFail({
|
||||||
|
select: ['id', 'plexToken'],
|
||||||
|
order: { id: 'ASC' },
|
||||||
|
});
|
||||||
|
notificationManager.sendNotification(Notification.MEDIA_FAILED, {
|
||||||
|
subject: movie.title,
|
||||||
|
message: 'Movie failed to add to Radarr',
|
||||||
|
notifyUser: admin,
|
||||||
|
media,
|
||||||
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
logger.info('Sent request to Radarr', { label: 'Media Request' });
|
logger.info('Sent request to Radarr', { label: 'Media Request' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -288,8 +325,6 @@ export class MediaRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterUpdate()
|
|
||||||
@AfterInsert()
|
|
||||||
private async _sendToSonarr() {
|
private async _sendToSonarr() {
|
||||||
if (
|
if (
|
||||||
this.status === MediaRequestStatus.APPROVED &&
|
this.status === MediaRequestStatus.APPROVED &&
|
||||||
@@ -352,7 +387,8 @@ export class MediaRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run this asynchronously so we don't wait for it on the UI side
|
// Run this asynchronously so we don't wait for it on the UI side
|
||||||
sonarr.addSeries({
|
sonarr
|
||||||
|
.addSeries({
|
||||||
profileId:
|
profileId:
|
||||||
seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
|
seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
|
||||||
? sonarrSettings.activeAnimeProfileId
|
? sonarrSettings.activeAnimeProfileId
|
||||||
@@ -368,6 +404,37 @@ export class MediaRequest {
|
|||||||
seriesType,
|
seriesType,
|
||||||
monitored: true,
|
monitored: true,
|
||||||
searchNow: true,
|
searchNow: true,
|
||||||
|
})
|
||||||
|
.then(async (success) => {
|
||||||
|
if (!success) {
|
||||||
|
media.status = MediaStatus.UNKNOWN;
|
||||||
|
await mediaRepository.save(media);
|
||||||
|
logger.warn(
|
||||||
|
'Newly added series request failed to add to Sonarr, marking as unknown',
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
const admin = await userRepository.findOneOrFail({
|
||||||
|
order: { id: 'ASC' },
|
||||||
|
});
|
||||||
|
notificationManager.sendNotification(Notification.MEDIA_FAILED, {
|
||||||
|
subject: series.name,
|
||||||
|
message: 'Series failed to add to Sonarr',
|
||||||
|
notifyUser: admin,
|
||||||
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`,
|
||||||
|
media,
|
||||||
|
extra: [
|
||||||
|
{
|
||||||
|
name: 'Seasons',
|
||||||
|
value: this.seasons
|
||||||
|
.map((season) => season.seasonNumber)
|
||||||
|
.join(', '),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
logger.info('Sent request to Sonarr', { label: 'Media Request' });
|
logger.info('Sent request to Sonarr', { label: 'Media Request' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@@ -158,6 +158,15 @@ class DiscordAgent
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (settings.main.applicationUrl) {
|
||||||
|
fields.push({
|
||||||
|
name: 'View Media',
|
||||||
|
value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Notification.MEDIA_FAILED:
|
||||||
|
color = EmbedColors.RED;
|
||||||
if (settings.main.applicationUrl) {
|
if (settings.main.applicationUrl) {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: 'View Media',
|
name: 'View Media',
|
||||||
|
@@ -112,6 +112,52 @@ class EmailAgent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async sendMediaFailedEmail(payload: NotificationPayload) {
|
||||||
|
// This is getting main settings for the whole app
|
||||||
|
const applicationUrl = getSettings().main.applicationUrl;
|
||||||
|
try {
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
const users = await userRepository.find();
|
||||||
|
|
||||||
|
// Send to all users with the manage requests permission (or admins)
|
||||||
|
users
|
||||||
|
.filter((user) => user.hasPermission(Permission.MANAGE_REQUESTS))
|
||||||
|
.forEach((user) => {
|
||||||
|
const email = this.getNewEmail();
|
||||||
|
|
||||||
|
email.send({
|
||||||
|
template: path.join(
|
||||||
|
__dirname,
|
||||||
|
'../../../templates/email/media-request'
|
||||||
|
),
|
||||||
|
message: {
|
||||||
|
to: user.email,
|
||||||
|
},
|
||||||
|
locals: {
|
||||||
|
body:
|
||||||
|
"A user's new request has failed to add to Sonarr or Radarr",
|
||||||
|
mediaName: payload.subject,
|
||||||
|
imageUrl: payload.image,
|
||||||
|
timestamp: new Date().toTimeString(),
|
||||||
|
requestedBy: payload.notifyUser.username,
|
||||||
|
actionUrl: applicationUrl
|
||||||
|
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
||||||
|
: undefined,
|
||||||
|
applicationUrl,
|
||||||
|
requestType: 'Failed Request',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Mail notification failed to send', {
|
||||||
|
label: 'Notifications',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async sendMediaApprovedEmail(payload: NotificationPayload) {
|
private async sendMediaApprovedEmail(payload: NotificationPayload) {
|
||||||
// This is getting main settings for the whole app
|
// This is getting main settings for the whole app
|
||||||
const applicationUrl = getSettings().main.applicationUrl;
|
const applicationUrl = getSettings().main.applicationUrl;
|
||||||
@@ -228,6 +274,9 @@ class EmailAgent
|
|||||||
case Notification.MEDIA_AVAILABLE:
|
case Notification.MEDIA_AVAILABLE:
|
||||||
this.sendMediaAvailableEmail(payload);
|
this.sendMediaAvailableEmail(payload);
|
||||||
break;
|
break;
|
||||||
|
case Notification.MEDIA_FAILED:
|
||||||
|
this.sendMediaFailedEmail(payload);
|
||||||
|
break;
|
||||||
case Notification.TEST_NOTIFICATION:
|
case Notification.TEST_NOTIFICATION:
|
||||||
this.sendTestEmail(payload);
|
this.sendTestEmail(payload);
|
||||||
break;
|
break;
|
||||||
|
@@ -5,7 +5,8 @@ export enum Notification {
|
|||||||
MEDIA_PENDING = 2,
|
MEDIA_PENDING = 2,
|
||||||
MEDIA_APPROVED = 4,
|
MEDIA_APPROVED = 4,
|
||||||
MEDIA_AVAILABLE = 8,
|
MEDIA_AVAILABLE = 8,
|
||||||
TEST_NOTIFICATION = 16,
|
MEDIA_FAILED = 16,
|
||||||
|
TEST_NOTIFICATION = 32,
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationManager {
|
class NotificationManager {
|
||||||
|
@@ -244,6 +244,32 @@ requestRoutes.delete('/:requestId', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
requestRoutes.post<{
|
||||||
|
requestId: string;
|
||||||
|
}>(
|
||||||
|
'/:requestId/retry',
|
||||||
|
isAuthenticated(Permission.MANAGE_REQUESTS),
|
||||||
|
async (req, res, next) => {
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = await requestRepository.findOneOrFail({
|
||||||
|
where: { id: Number(req.params.requestId) },
|
||||||
|
relations: ['requestedBy', 'modifiedBy'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await request.updateParentStatus();
|
||||||
|
await request.sendMedia();
|
||||||
|
return res.status(200).json(request);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Error processing request retry', {
|
||||||
|
label: 'Media Request',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
next({ status: 404, message: 'Request not found' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
requestRoutes.get<{
|
requestRoutes.get<{
|
||||||
requestId: string;
|
requestId: string;
|
||||||
status: 'pending' | 'approve' | 'decline';
|
status: 'pending' | 'approve' | 'decline';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
import type { MediaRequest } from '../../../../server/entity/MediaRequest';
|
import type { MediaRequest } from '../../../../server/entity/MediaRequest';
|
||||||
import {
|
import {
|
||||||
@@ -15,16 +15,21 @@ import useSWR from 'swr';
|
|||||||
import Badge from '../../Common/Badge';
|
import Badge from '../../Common/Badge';
|
||||||
import StatusBadge from '../../StatusBadge';
|
import StatusBadge from '../../StatusBadge';
|
||||||
import Table from '../../Common/Table';
|
import Table from '../../Common/Table';
|
||||||
import { MediaRequestStatus } from '../../../../server/constants/media';
|
import {
|
||||||
|
MediaRequestStatus,
|
||||||
|
MediaStatus,
|
||||||
|
} from '../../../../server/constants/media';
|
||||||
import Button from '../../Common/Button';
|
import Button from '../../Common/Button';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import globalMessages from '../../../i18n/globalMessages';
|
import globalMessages from '../../../i18n/globalMessages';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
requestedby: 'Requested by {username}',
|
requestedby: 'Requested by {username}',
|
||||||
seasons: 'Seasons',
|
seasons: 'Seasons',
|
||||||
notavailable: 'N/A',
|
notavailable: 'N/A',
|
||||||
|
failedretry: 'Something went wrong retrying the request',
|
||||||
});
|
});
|
||||||
|
|
||||||
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||||
@@ -33,13 +38,17 @@ const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
|||||||
|
|
||||||
interface RequestItemProps {
|
interface RequestItemProps {
|
||||||
request: MediaRequest;
|
request: MediaRequest;
|
||||||
onDelete: () => void;
|
revalidateList: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RequestItem: React.FC<RequestItemProps> = ({ request, onDelete }) => {
|
const RequestItem: React.FC<RequestItemProps> = ({
|
||||||
|
request,
|
||||||
|
revalidateList,
|
||||||
|
}) => {
|
||||||
const { ref, inView } = useInView({
|
const { ref, inView } = useInView({
|
||||||
triggerOnce: true,
|
triggerOnce: true,
|
||||||
});
|
});
|
||||||
|
const { addToast } = useToasts();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { hasPermission } = useUser();
|
const { hasPermission } = useUser();
|
||||||
const { locale } = useContext(LanguageContext);
|
const { locale } = useContext(LanguageContext);
|
||||||
@@ -50,13 +59,15 @@ const RequestItem: React.FC<RequestItemProps> = ({ request, onDelete }) => {
|
|||||||
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
|
||||||
inView ? `${url}?language=${locale}` : null
|
inView ? `${url}?language=${locale}` : null
|
||||||
);
|
);
|
||||||
const { data: requestData, revalidate } = useSWR<MediaRequest>(
|
const { data: requestData, revalidate, mutate } = useSWR<MediaRequest>(
|
||||||
`/api/v1/request/${request.id}`,
|
`/api/v1/request/${request.id}`,
|
||||||
{
|
{
|
||||||
initialData: request,
|
initialData: request,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isRetrying, setRetrying] = useState(false);
|
||||||
|
|
||||||
const modifyRequest = async (type: 'approve' | 'decline') => {
|
const modifyRequest = async (type: 'approve' | 'decline') => {
|
||||||
const response = await axios.get(`/api/v1/request/${request.id}/${type}`);
|
const response = await axios.get(`/api/v1/request/${request.id}/${type}`);
|
||||||
|
|
||||||
@@ -68,7 +79,23 @@ const RequestItem: React.FC<RequestItemProps> = ({ request, onDelete }) => {
|
|||||||
const deleteRequest = async () => {
|
const deleteRequest = async () => {
|
||||||
await axios.delete(`/api/v1/request/${request.id}`);
|
await axios.delete(`/api/v1/request/${request.id}`);
|
||||||
|
|
||||||
onDelete();
|
revalidateList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const retryRequest = async () => {
|
||||||
|
setRetrying(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await axios.post(`/api/v1/request/${request.id}/retry`);
|
||||||
|
mutate(result.data);
|
||||||
|
} catch (e) {
|
||||||
|
addToast(intl.formatMessage(messages.failedretry), {
|
||||||
|
autoDismiss: true,
|
||||||
|
appearance: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setRetrying(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!title && !error) {
|
if (!title && !error) {
|
||||||
@@ -138,7 +165,13 @@ const RequestItem: React.FC<RequestItemProps> = ({ request, onDelete }) => {
|
|||||||
)}
|
)}
|
||||||
</Table.TD>
|
</Table.TD>
|
||||||
<Table.TD>
|
<Table.TD>
|
||||||
|
{requestData.media.status === MediaStatus.UNKNOWN ? (
|
||||||
|
<Badge badgeType="danger">
|
||||||
|
{intl.formatMessage(globalMessages.failed)}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
<StatusBadge status={requestData.media.status} />
|
<StatusBadge status={requestData.media.status} />
|
||||||
|
)}
|
||||||
</Table.TD>
|
</Table.TD>
|
||||||
<Table.TD>
|
<Table.TD>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -167,6 +200,31 @@ const RequestItem: React.FC<RequestItemProps> = ({ request, onDelete }) => {
|
|||||||
</div>
|
</div>
|
||||||
</Table.TD>
|
</Table.TD>
|
||||||
<Table.TD alignText="right">
|
<Table.TD alignText="right">
|
||||||
|
{requestData.media.status === MediaStatus.UNKNOWN &&
|
||||||
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
|
<Button
|
||||||
|
className="mr-2"
|
||||||
|
buttonType="primary"
|
||||||
|
buttonSize="sm"
|
||||||
|
disabled={isRetrying}
|
||||||
|
onClick={() => retryRequest()}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mr-0 sm:mr-1"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="18px"
|
||||||
|
height="18px"
|
||||||
|
>
|
||||||
|
<path d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z" />
|
||||||
|
</svg>
|
||||||
|
<span className="hidden sm:block">
|
||||||
|
{intl.formatMessage(globalMessages.retry)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{requestData.status !== MediaRequestStatus.PENDING &&
|
{requestData.status !== MediaRequestStatus.PENDING &&
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
<Button
|
<Button
|
||||||
|
@@ -56,7 +56,7 @@ const RequestList: React.FC = () => {
|
|||||||
<RequestItem
|
<RequestItem
|
||||||
request={request}
|
request={request}
|
||||||
key={`request-list-${request.id}`}
|
key={`request-list-${request.id}`}
|
||||||
onDelete={() => revalidate()}
|
revalidateList={() => revalidate()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@@ -6,6 +6,7 @@ const globalMessages = defineMessages({
|
|||||||
processing: 'Processing',
|
processing: 'Processing',
|
||||||
unavailable: 'Unavailable',
|
unavailable: 'Unavailable',
|
||||||
requested: 'Requested',
|
requested: 'Requested',
|
||||||
|
failed: 'Failed',
|
||||||
pending: 'Pending',
|
pending: 'Pending',
|
||||||
declined: 'Declined',
|
declined: 'Declined',
|
||||||
approved: 'Approved',
|
approved: 'Approved',
|
||||||
@@ -15,6 +16,7 @@ const globalMessages = defineMessages({
|
|||||||
approve: 'Approve',
|
approve: 'Approve',
|
||||||
decline: 'Decline',
|
decline: 'Decline',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
|
retry: 'Retry',
|
||||||
deleting: 'Deleting…',
|
deleting: 'Deleting…',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
});
|
});
|
||||||
|
@@ -71,6 +71,7 @@
|
|||||||
"components.RequestCard.all": "All",
|
"components.RequestCard.all": "All",
|
||||||
"components.RequestCard.requestedby": "Requested by {username}",
|
"components.RequestCard.requestedby": "Requested by {username}",
|
||||||
"components.RequestCard.seasons": "Seasons",
|
"components.RequestCard.seasons": "Seasons",
|
||||||
|
"components.RequestList.RequestItem.failedretry": "Something went wrong retrying the request",
|
||||||
"components.RequestList.RequestItem.notavailable": "N/A",
|
"components.RequestList.RequestItem.notavailable": "N/A",
|
||||||
"components.RequestList.RequestItem.requestedby": "Requested by {username}",
|
"components.RequestList.RequestItem.requestedby": "Requested by {username}",
|
||||||
"components.RequestList.RequestItem.seasons": "Seasons",
|
"components.RequestList.RequestItem.seasons": "Seasons",
|
||||||
@@ -376,11 +377,13 @@
|
|||||||
"i18n.declined": "Declined",
|
"i18n.declined": "Declined",
|
||||||
"i18n.delete": "Delete",
|
"i18n.delete": "Delete",
|
||||||
"i18n.deleting": "Deleting…",
|
"i18n.deleting": "Deleting…",
|
||||||
|
"i18n.failed": "Failed",
|
||||||
"i18n.movies": "Movies",
|
"i18n.movies": "Movies",
|
||||||
"i18n.partiallyavailable": "Partially Available",
|
"i18n.partiallyavailable": "Partially Available",
|
||||||
"i18n.pending": "Pending",
|
"i18n.pending": "Pending",
|
||||||
"i18n.processing": "Processing…",
|
"i18n.processing": "Processing…",
|
||||||
"i18n.requested": "Requested",
|
"i18n.requested": "Requested",
|
||||||
|
"i18n.retry": "Retry",
|
||||||
"i18n.tvshows": "Series",
|
"i18n.tvshows": "Series",
|
||||||
"i18n.unavailable": "Unavailable",
|
"i18n.unavailable": "Unavailable",
|
||||||
"pages.internalServerError": "{statusCode} - Internal Server Error",
|
"pages.internalServerError": "{statusCode} - Internal Server Error",
|
||||||
|
Reference in New Issue
Block a user