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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/components/RequestBlock/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import useRequestOverride from '@app/hooks/useRequestOverride';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { formatFailureReason } from '@app/utils/requestFailureHelpers';
import {
CalendarIcon,
CheckIcon,
Expand Down Expand Up @@ -225,9 +226,17 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
</Badge>
)}
{request.status === MediaRequestStatus.FAILED && (
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.failed)}
</Badge>
<Tooltip
content={formatFailureReason(
intl,
request.type,
request.failureReason
)}
>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.failed)}
</Badge>
</Tooltip>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)}
{request.status === MediaRequestStatus.COMPLETED && (
<Badge badgeType="success">
Expand Down
104 changes: 81 additions & 23 deletions src/components/RequestCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ import {
getRequestDownloadStatus,
refreshIntervalHelper,
} from '@app/utils/refreshIntervalHelper';
import {
canRetryRequest,
canSearchAnotherListing,
formatFailureReason,
} from '@app/utils/requestFailureHelpers';
import { withProperties } from '@app/utils/typeHelpers';
import {
ArrowPathIcon,
CheckIcon,
MagnifyingGlassIcon,
PencilIcon,
TrashIcon,
XMarkIcon,
Expand Down Expand Up @@ -149,11 +155,19 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
</span>
{requestData.status === MediaRequestStatus.DECLINED ||
requestData.status === MediaRequestStatus.FAILED ? (
<Badge badgeType="danger">
{requestData.status === MediaRequestStatus.DECLINED
? intl.formatMessage(globalMessages.declined)
: intl.formatMessage(globalMessages.failed)}
</Badge>
<Tooltip
content={formatFailureReason(
intl,
requestData.type,
requestData.failureReason
)}
>
<Badge badgeType="danger">
{requestData.status === MediaRequestStatus.DECLINED
? intl.formatMessage(globalMessages.declined)
: intl.formatMessage(globalMessages.failed)}
</Badge>
</Tooltip>
) : (
<StatusBadge
status={
Expand Down Expand Up @@ -442,12 +456,20 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
{intl.formatMessage(globalMessages.declined)}
</Badge>
) : requestData.status === MediaRequestStatus.FAILED ? (
<Badge
badgeType="danger"
href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
<Tooltip
content={formatFailureReason(
intl,
requestData.type,
requestData.failureReason
)}
>
{intl.formatMessage(globalMessages.failed)}
</Badge>
<Badge
badgeType="danger"
href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
>
{intl.formatMessage(globalMessages.failed)}
</Badge>
</Tooltip>
) : requestData.status === MediaRequestStatus.PENDING &&
requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.DELETED ? (
Expand Down Expand Up @@ -480,20 +502,56 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
<div className="flex flex-1 items-end space-x-2">
{requestData.status === MediaRequestStatus.FAILED &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
buttonType="primary"
buttonSize="sm"
disabled={isRetrying}
onClick={() => retryRequest()}
<Tooltip
content={
canRetryRequest(requestData.failureReason)
? undefined
: intl.formatMessage(globalMessages.retryunavailable)
}
>
<ArrowPathIcon
className={isRetrying ? 'animate-spin' : ''}
style={{ marginRight: '0', animationDirection: 'reverse' }}
/>
<span className="ml-1.5 hidden sm:block">
{intl.formatMessage(globalMessages.retry)}
</span>
</Button>
<Button
buttonType="primary"
buttonSize="sm"
disabled={
isRetrying || !canRetryRequest(requestData.failureReason)
}
onClick={() => retryRequest()}
>
<ArrowPathIcon
className={isRetrying ? 'animate-spin' : ''}
style={{
marginRight: '0',
animationDirection: 'reverse',
}}
/>
<span className="ml-1.5 hidden sm:block">
{intl.formatMessage(globalMessages.retry)}
</span>
</Button>
</Tooltip>
)}
{requestData.status === MediaRequestStatus.FAILED &&
canSearchAnotherListing(requestData.failureReason) &&
title && (
<Link
href={`/search?query=${encodeURIComponent(
isMovie(title) ? title.title : title.name
)}`}
passHref
legacyBehavior
>
<Button
as="a"
buttonType="default"
buttonSize="sm"
title={intl.formatMessage(globalMessages.findlisting)}
>
<MagnifyingGlassIcon style={{ marginRight: '0' }} />
<span className="ml-1.5 hidden sm:block">
{intl.formatMessage(globalMessages.findlisting)}
</span>
</Button>
</Link>
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
Expand Down
101 changes: 76 additions & 25 deletions src/components/RequestList/RequestItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import ConfirmButton from '@app/components/Common/ConfirmButton';
import Tooltip from '@app/components/Common/Tooltip';
import RequestModal from '@app/components/RequestModal';
import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
Expand All @@ -14,9 +15,15 @@ import {
getRequestDownloadStatus,
refreshIntervalHelper,
} from '@app/utils/refreshIntervalHelper';
import {
canRetryRequest,
canSearchAnotherListing,
formatFailureReason,
} from '@app/utils/requestFailureHelpers';
import {
ArrowPathIcon,
CheckIcon,
MagnifyingGlassIcon,
PencilIcon,
TrashIcon,
XMarkIcon,
Expand Down Expand Up @@ -139,11 +146,19 @@ const RequestItemError = ({
</span>
{requestData.status === MediaRequestStatus.DECLINED ||
requestData.status === MediaRequestStatus.FAILED ? (
<Badge badgeType="danger">
{requestData.status === MediaRequestStatus.DECLINED
? intl.formatMessage(globalMessages.declined)
: intl.formatMessage(globalMessages.failed)}
</Badge>
<Tooltip
content={formatFailureReason(
intl,
requestData.type,
requestData.failureReason
)}
>
<Badge badgeType="danger">
{requestData.status === MediaRequestStatus.DECLINED
? intl.formatMessage(globalMessages.declined)
: intl.formatMessage(globalMessages.failed)}
</Badge>
</Tooltip>
) : (
<StatusBadge
status={
Expand Down Expand Up @@ -527,12 +542,20 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
{intl.formatMessage(globalMessages.declined)}
</Badge>
) : requestData.status === MediaRequestStatus.FAILED ? (
<Badge
badgeType="danger"
href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
<Tooltip
content={formatFailureReason(
intl,
requestData.type,
requestData.failureReason
)}
>
{intl.formatMessage(globalMessages.failed)}
</Badge>
<Badge
badgeType="danger"
href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
>
{intl.formatMessage(globalMessages.failed)}
</Badge>
</Tooltip>
) : requestData.status === MediaRequestStatus.PENDING &&
requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
MediaStatus.DELETED ? (
Expand Down Expand Up @@ -683,22 +706,50 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
{requestData.status === MediaRequestStatus.FAILED &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
className="w-full"
buttonType="primary"
disabled={isRetrying}
onClick={() => retryRequest()}
<Tooltip
content={
canRetryRequest(requestData.failureReason)
? undefined
: intl.formatMessage(globalMessages.retryunavailable)
}
>
<ArrowPathIcon
className={isRetrying ? 'animate-spin' : ''}
style={{ animationDirection: 'reverse' }}
/>
<span>
{intl.formatMessage(
isRetrying ? globalMessages.retrying : globalMessages.retry
)}
</span>
</Button>
<Button
className="w-full"
buttonType="primary"
disabled={
isRetrying || !canRetryRequest(requestData.failureReason)
}
onClick={() => retryRequest()}
>
<ArrowPathIcon
className={isRetrying ? 'animate-spin' : ''}
style={{ animationDirection: 'reverse' }}
/>
<span>
{intl.formatMessage(
isRetrying
? globalMessages.retrying
: globalMessages.retry
)}
</span>
</Button>
</Tooltip>
)}
{requestData.status === MediaRequestStatus.FAILED &&
canSearchAnotherListing(requestData.failureReason) &&
title && (
<Link
href={`/search?query=${encodeURIComponent(
isMovie(title) ? title.title : title.name
)}`}
passHref
legacyBehavior
>
<Button as="a" className="w-full" buttonType="default">
<MagnifyingGlassIcon />
<span>{intl.formatMessage(globalMessages.findlisting)}</span>
</Button>
</Link>
)}
{requestData.status !== MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
Expand Down
9 changes: 9 additions & 0 deletions src/i18n/globalMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ const globalMessages = defineMessages('i18n', {
removefromBlocklist: 'Remove from Blocklist',
specials: 'Specials',
notificationLocale: 'Notification Language',
findlisting: 'Find Listing',
retryunavailable: 'Retrying cannot resolve this failure.',
failurereasonunreachable: 'Could not connect to {serviceName}.',
failurereasondispatch: '{serviceName} rejected the request.',
failurereasontvdbid: 'This series could not be identified on TheTVDB.',
failurereasonseasonunverified:
'The season numbering could not be checked against TheTVDB.',
failurereasonseasonmismatch:
'This listing numbers its seasons differently than TheTVDB.',
});

export default globalMessages;
7 changes: 7 additions & 0 deletions src/i18n/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1665,6 +1665,12 @@
"i18n.error": "Something went wrong. Please try again.",
"i18n.experimental": "Experimental",
"i18n.failed": "Failed",
"i18n.failurereasondispatch": "{serviceName} rejected the request.",
"i18n.failurereasonseasonmismatch": "This listing numbers its seasons differently than TheTVDB.",
"i18n.failurereasonseasonunverified": "The season numbering could not be checked against TheTVDB.",
"i18n.failurereasontvdbid": "This series could not be identified on TheTVDB.",
"i18n.failurereasonunreachable": "Could not connect to {serviceName}.",
"i18n.findlisting": "Find Listing",
"i18n.import": "Import",
"i18n.importing": "Importing…",
"i18n.loading": "Loading…",
Expand All @@ -1691,6 +1697,7 @@
"i18n.resultsperpage": "Display {pageSize} results per page",
"i18n.retry": "Retry",
"i18n.retrying": "Retrying…",
"i18n.retryunavailable": "Retrying cannot resolve this failure.",
"i18n.save": "Save Changes",
"i18n.saving": "Saving…",
"i18n.settings": "Settings",
Expand Down
42 changes: 42 additions & 0 deletions src/utils/requestFailureHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import globalMessages from '@app/i18n/globalMessages';
import { MediaRequestFailureReason, MediaType } from '@server/constants/media';
import type { IntlShape, MessageDescriptor } from 'react-intl';

const failureReasonMessages: Record<
MediaRequestFailureReason,
MessageDescriptor
> = {
[MediaRequestFailureReason.SERVICE_UNREACHABLE]:
globalMessages.failurereasonunreachable,
[MediaRequestFailureReason.DISPATCH_FAILED]:
globalMessages.failurereasondispatch,
[MediaRequestFailureReason.TVDB_ID_UNRESOLVED]:
globalMessages.failurereasontvdbid,
[MediaRequestFailureReason.SEASON_NUMBERING_UNVERIFIED]:
globalMessages.failurereasonseasonunverified,
[MediaRequestFailureReason.SEASON_NUMBERING_MISMATCH]:
globalMessages.failurereasonseasonmismatch,
};

export const formatFailureReason = (
intl: IntlShape,
type: MediaType,
reason?: MediaRequestFailureReason | null
): string | undefined => {
const message = reason ? failureReasonMessages[reason] : undefined;

return message
? intl.formatMessage(message, {
serviceName: type === MediaType.MOVIE ? 'Radarr' : 'Sonarr',
})
: undefined;
};

export const canSearchAnotherListing = (
reason?: MediaRequestFailureReason | null
): boolean => reason === MediaRequestFailureReason.SEASON_NUMBERING_MISMATCH;

// a numbering mismatch re-runs the same comparison on every attempt
export const canRetryRequest = (
reason?: MediaRequestFailureReason | null
): boolean => reason !== MediaRequestFailureReason.SEASON_NUMBERING_MISMATCH;
Loading