Compare commits

..

11 Commits

Author SHA1 Message Date
0xsysr3ll
ed3a9b1c8d fix(availability-sync): added some diagnostic logging
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
2026-01-20 23:10:24 +01:00
0xsysr3ll
8431ef3f3b fix(availability-sync): remove incorrect existence flags for media in Radarr, Sonarr, and Plex
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
2026-01-20 20:07:16 +01:00
fallenbagel
88b2e7843f fix(sonarr): re-monitor episodes when re-requesting deleted but monitored seasons (#2312) 2026-01-20 18:34:21 +01:00
fallenbagel
dbd5935ade fix(proxy): configure proxy agent connection limits and IPv4 support (#2303)
* fix: configure axios proxy agent socket limits to prevent connection leaks

Add socket pool configuration to HttpProxyAgent and HttpsProxyAgent to
prevent connection leaks.

fix #2297

* fix(proxy): pass forceIpv4First option to custom proxy agent

* fix(proxy): add connection limits and IPv4 support to undici agents
2026-01-20 12:37:41 +01:00
fallenbagel
bb2120c14d fix(base-scanner): fix PROCESSING status persisting for unmonitored seasons (#2311)
BaseScanner's fallthrough logic was preventing unmonitored seasons from
resetting to UNKNOWN status.

fix #2310
2026-01-18 22:32:57 +05:00
fallenbagel
c9037f77e6 fix(network-settings): convert DNS cache TTL values to numbers (#2299)
This PR ensures DNS cache TTL values are properly converted to numbers before being sent to the
backend.

fix #2294
2026-01-17 13:46:05 +01:00
Brandon Cohen
48631db989 fix: preserve deleted status when processing movies (#2066)
* fix: prevent the delete status from changing unless a new request is made"

refactor: remove parent remove change until later date

refactor: remove console log

* fix: add download progress for deleted badge

fix: check if not processing first for movies

* fix: add season pack change
2026-01-17 06:48:14 +05:00
fallenbagel
ac7c2983d3 fix(pushover): prevent notifications when agent is disabled or unconfigured (#2304) 2026-01-16 22:39:15 +01:00
fallenbagel
767dc529e8 fix(ui): correct season pluralization in RequestItem (#2307)
Fixes incorrect "Seasons" label when only one season is requested.

fix #2263
2026-01-16 22:12:21 +01:00
fallenbagel
448a25e2a4 fix(availability-sync): prevent incorrect season deletion when media server is unreachable (#2302) 2026-01-16 10:47:47 +01:00
fallenbagel
3f35b8c886 fix(ui): correct season pluralisation in RequestCard (#2305)
Fixes incorrect "seasons" label when only one season is requested. The plural form was being used
regardless of the actual count

fix #2263
2026-01-16 09:19:13 +01:00
10 changed files with 331 additions and 145 deletions

View File

@@ -209,6 +209,34 @@ class SonarrAPI extends ServarrBase<{
series: newSeriesResponse.data,
});
try {
const episodes = await this.getEpisodes(newSeriesResponse.data.id);
const episodeIdsToMonitor = episodes
.filter(
(ep) =>
options.seasons.includes(ep.seasonNumber) && !ep.monitored
)
.map((ep) => ep.id);
if (episodeIdsToMonitor.length > 0) {
logger.debug(
'Re-monitoring unmonitored episodes for requested seasons.',
{
label: 'Sonarr',
seriesId: newSeriesResponse.data.id,
episodeCount: episodeIdsToMonitor.length,
}
);
await this.monitorEpisodes(episodeIdsToMonitor);
}
} catch (e) {
logger.warn('Failed to re-monitor episodes', {
label: 'Sonarr',
errorMessage: e.message,
seriesId: newSeriesResponse.data.id,
});
}
if (options.searchNow) {
this.searchSeries(newSeriesResponse.data.id);
}
@@ -318,6 +346,38 @@ class SonarrAPI extends ServarrBase<{
}
}
public async getEpisodes(seriesId: number): Promise<EpisodeResult[]> {
try {
const response = await this.axios.get<EpisodeResult[]>('/episode', {
params: { seriesId },
});
return response.data;
} catch (e) {
logger.error('Failed to retrieve episodes', {
label: 'Sonarr API',
errorMessage: e.message,
seriesId,
});
throw new Error('Failed to get episodes');
}
}
public async monitorEpisodes(episodeIds: number[]): Promise<void> {
try {
await this.axios.put('/episode/monitor', {
episodeIds,
monitored: true,
});
} catch (e) {
logger.error('Failed to monitor episodes', {
label: 'Sonarr API',
errorMessage: e.message,
episodeIds,
});
throw new Error('Failed to monitor episodes');
}
}
private buildSeasonList(
seasons: number[],
existingSeasons?: SonarrSeason[]

View File

@@ -143,7 +143,9 @@ class AvailabilitySync {
const { existsInPlex: existsInPlex4k } =
await this.mediaExistsInPlex(media, true);
if (existsInPlex || existsInRadarr) {
// Media must exist in Plex to be considered available
// If it exists in Radarr but not in Plex, it should be marked as deleted
if (existsInPlex) {
movieExists = true;
logger.info(
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
@@ -153,7 +155,7 @@ class AvailabilitySync {
);
}
if (existsInPlex4k || existsInRadarr4k) {
if (existsInPlex4k) {
movieExists4k = true;
logger.info(
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
@@ -240,7 +242,9 @@ class AvailabilitySync {
//plex
if (mediaServerType === MediaServerType.PLEX) {
if (existsInPlex || existsInSonarr) {
// Media must exist in Plex to be considered available
// If it exists in Sonarr but not in Plex, it should be marked as deleted
if (existsInPlex) {
showExists = true;
logger.info(
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
@@ -252,7 +256,7 @@ class AvailabilitySync {
}
if (mediaServerType === MediaServerType.PLEX) {
if (existsInPlex4k || existsInSonarr4k) {
if (existsInPlex4k) {
showExists4k = true;
logger.info(
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
@@ -300,7 +304,6 @@ class AvailabilitySync {
// Sonarr finds that season, we will change the final seasons value
// to true.
const filteredSeasonsMap: Map<number, boolean> = new Map();
media.seasons
.filter(
(season) =>
@@ -311,48 +314,7 @@ class AvailabilitySync {
filteredSeasonsMap.set(season.seasonNumber, false)
);
// non-4k
const finalSeasons: Map<number, boolean> = new Map();
if (mediaServerType === MediaServerType.PLEX) {
plexSeasonsMap.forEach((value, key) => {
finalSeasons.set(key, value);
});
filteredSeasonsMap.forEach((value, key) => {
if (!finalSeasons.has(key)) {
finalSeasons.set(key, value);
}
});
sonarrSeasonsMap.forEach((value, key) => {
if (!finalSeasons.has(key)) {
finalSeasons.set(key, value);
}
});
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
jellyfinSeasonsMap.forEach((value, key) => {
finalSeasons.set(key, value);
});
filteredSeasonsMap.forEach((value, key) => {
if (!finalSeasons.has(key)) {
finalSeasons.set(key, value);
}
});
sonarrSeasonsMap.forEach((value, key) => {
if (!finalSeasons.has(key)) {
finalSeasons.set(key, value);
}
});
}
const filteredSeasonsMap4k: Map<number, boolean> = new Map();
media.seasons
.filter(
(season) =>
@@ -363,44 +325,32 @@ class AvailabilitySync {
filteredSeasonsMap4k.set(season.seasonNumber, false)
);
// 4k
const finalSeasons4k: Map<number, boolean> = new Map();
let finalSeasons: Map<number, boolean>;
let finalSeasons4k: Map<number, boolean>;
if (mediaServerType === MediaServerType.PLEX) {
plexSeasonsMap4k.forEach((value, key) => {
finalSeasons4k.set(key, value);
});
filteredSeasonsMap4k.forEach((value, key) => {
if (!finalSeasons4k.has(key)) {
finalSeasons4k.set(key, value);
}
});
sonarrSeasonsMap4k.forEach((value, key) => {
if (!finalSeasons4k.has(key)) {
finalSeasons4k.set(key, value);
}
});
} else if (
mediaServerType === MediaServerType.JELLYFIN ||
mediaServerType === MediaServerType.EMBY
) {
jellyfinSeasonsMap4k.forEach((value, key) => {
finalSeasons4k.set(key, value);
});
filteredSeasonsMap4k.forEach((value, key) => {
if (!finalSeasons4k.has(key)) {
finalSeasons4k.set(key, value);
}
});
sonarrSeasonsMap4k.forEach((value, key) => {
if (!finalSeasons4k.has(key)) {
finalSeasons4k.set(key, value);
}
});
finalSeasons = new Map([
...filteredSeasonsMap,
...plexSeasonsMap,
...sonarrSeasonsMap,
]);
finalSeasons4k = new Map([
...filteredSeasonsMap4k,
...plexSeasonsMap4k,
...sonarrSeasonsMap4k,
]);
} else {
// Jellyfin/Emby
finalSeasons = new Map([
...filteredSeasonsMap,
...jellyfinSeasonsMap,
...sonarrSeasonsMap,
]);
finalSeasons4k = new Map([
...filteredSeasonsMap4k,
...jellyfinSeasonsMap4k,
...sonarrSeasonsMap4k,
]);
}
if (
@@ -665,6 +615,21 @@ class AvailabilitySync {
is4k: boolean
): Promise<boolean> {
let existsInRadarr = false;
const externalServiceId = is4k
? media.externalServiceId4k
: media.externalServiceId;
if (!externalServiceId) {
logger.debug(
`Skipping Radarr check for ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
media.tmdbId
}] - no externalServiceId available`,
{
label: 'Availability Sync',
}
);
return false;
}
// Check for availability in all of the available radarr servers
// If any find the media, we will assume the media exists
@@ -691,22 +656,63 @@ class AvailabilitySync {
});
}
if (radarr && radarr.hasFile) {
const resolution =
radarr?.movieFile?.mediaInfo?.resolution?.split('x');
const is4kMovie =
resolution?.length === 2 && Number(resolution[0]) >= 2000;
existsInRadarr = is4k ? is4kMovie : !is4kMovie;
if (radarr) {
if (radarr.hasFile) {
const resolution =
radarr?.movieFile?.mediaInfo?.resolution?.split('x');
const is4kMovie =
resolution?.length === 2 && Number(resolution[0]) >= 2000;
const matches4k = is4k ? is4kMovie : !is4kMovie;
if (matches4k) {
existsInRadarr = true;
logger.debug(
`Found ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
media.tmdbId
}] in Radarr`,
{
radarrId: radarr.id,
radarrTitle: radarr.title,
hasFile: radarr.hasFile,
externalServiceId: externalServiceId,
label: 'Availability Sync',
}
);
} else {
logger.debug(
`Movie [TMDB ID ${media.tmdbId}] found in Radarr but resolution doesn't match (is4k: ${is4k}, movie resolution: ${radarr?.movieFile?.mediaInfo?.resolution})`,
{
label: 'Availability Sync',
}
);
}
} else {
logger.debug(
`Movie [TMDB ID ${media.tmdbId}] found in Radarr but has no file`,
{
radarrId: radarr.id,
radarrTitle: radarr.title,
externalServiceId: externalServiceId,
label: 'Availability Sync',
}
);
}
}
} catch (ex) {
if (!ex.message.includes('404')) {
existsInRadarr = true;
if (ex.message.includes('404')) {
logger.debug(
`Movie [TMDB ID ${media.tmdbId}] not found in Radarr (404) - externalServiceId may be stale: ${externalServiceId}`,
{
label: 'Availability Sync',
}
);
} else {
logger.debug(
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
media.tmdbId
}] from Radarr.`,
{
errorMessage: ex.message,
externalServiceId: externalServiceId,
label: 'Availability Sync',
}
);
@@ -754,7 +760,6 @@ class AvailabilitySync {
}
} catch (ex) {
if (!ex.message.includes('404')) {
existsInSonarr = true;
preventSeasonSearch = true;
logger.debug(
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} show [TMDB ID ${
@@ -853,31 +858,66 @@ class AvailabilitySync {
// We can use the cache we built when we fetched the series with mediaExistsInPlex
try {
let plexMedia: PlexMetadata | undefined;
const currentRatingKey = is4k ? ratingKey4k : ratingKey;
if (ratingKey && !is4k) {
plexMedia = await this.plexClient?.getMetadata(ratingKey);
if (!currentRatingKey) {
logger.debug(
`Skipping Plex check for ${is4k ? '4K' : 'non-4K'} ${
media.mediaType === 'tv' ? 'show' : 'movie'
} [TMDB ID ${media.tmdbId}] - no ratingKey available`,
{
label: 'Availability Sync',
}
);
} else {
if (ratingKey && !is4k) {
plexMedia = await this.plexClient?.getMetadata(ratingKey);
if (media.mediaType === 'tv') {
this.plexSeasonsCache[ratingKey] =
await this.plexClient?.getChildrenMetadata(ratingKey);
if (media.mediaType === 'tv') {
this.plexSeasonsCache[ratingKey] =
await this.plexClient?.getChildrenMetadata(ratingKey);
}
}
}
if (ratingKey4k && is4k) {
plexMedia = await this.plexClient?.getMetadata(ratingKey4k);
if (ratingKey4k && is4k) {
plexMedia = await this.plexClient?.getMetadata(ratingKey4k);
if (media.mediaType === 'tv') {
this.plexSeasonsCache[ratingKey4k] =
await this.plexClient?.getChildrenMetadata(ratingKey4k);
if (media.mediaType === 'tv') {
this.plexSeasonsCache[ratingKey4k] =
await this.plexClient?.getChildrenMetadata(ratingKey4k);
}
}
}
if (plexMedia) {
existsInPlex = true;
if (plexMedia) {
existsInPlex = true;
logger.debug(
`Found ${is4k ? '4K' : 'non-4K'} ${
media.mediaType === 'tv' ? 'show' : 'movie'
} [TMDB ID ${media.tmdbId}] in Plex`,
{
ratingKey: is4k ? ratingKey4k : ratingKey,
plexTitle: plexMedia.title,
plexRatingKey: plexMedia.ratingKey,
plexGuid: plexMedia.guid,
label: 'Availability Sync',
}
);
}
}
} catch (ex) {
if (!ex.message.includes('404')) {
existsInPlex = true;
if (ex.message.includes('404')) {
logger.debug(
`Media ${is4k ? '4K' : 'non-4K'} ${
media.mediaType === 'tv' ? 'show' : 'movie'
} [TMDB ID ${
media.tmdbId
}] not found in Plex (404) - ratingKey may be stale`,
{
ratingKey: is4k ? ratingKey4k : ratingKey,
label: 'Availability Sync',
}
);
} else {
preventSeasonSearch = true;
logger.debug(
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${
@@ -993,8 +1033,8 @@ class AvailabilitySync {
existsInJellyfin = true;
}
} catch (ex) {
if (!ex.message.includes('404' || '500')) {
existsInJellyfin = false;
if (!ex.message.includes('404') && !ex.message.includes('500')) {
existsInJellyfin = true;
preventSeasonSearch = true;
logger.debug(
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${

View File

@@ -45,7 +45,17 @@ class PushoverAgent
}
public shouldSend(): boolean {
return true;
const settings = this.getSettings();
if (
settings.enabled &&
settings.options.accessToken &&
settings.options.userToken
) {
return true;
}
return false;
}
private async getImagePayload(

View File

@@ -115,9 +115,11 @@ class BaseScanner<T> {
let changedExisting = false;
if (existing[is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE) {
existing[is4k ? 'status4k' : 'status'] = processing
? MediaStatus.PROCESSING
: MediaStatus.AVAILABLE;
existing[is4k ? 'status4k' : 'status'] = !processing
? MediaStatus.AVAILABLE
: existing[is4k ? 'status4k' : 'status'] === MediaStatus.DELETED
? MediaStatus.DELETED
: MediaStatus.PROCESSING;
if (mediaAddedAt) {
existing.mediaAddedAt = mediaAddedAt;
}
@@ -330,6 +332,11 @@ class BaseScanner<T> {
season.processing &&
existingSeason.status !== MediaStatus.DELETED
? MediaStatus.PROCESSING
: !season.is4kOverride &&
!season.processing &&
season.episodes === 0 &&
existingSeason.status === MediaStatus.PROCESSING
? MediaStatus.UNKNOWN
: existingSeason.status;
// Same thing here, except we only do updates if 4k is enabled
@@ -345,6 +352,11 @@ class BaseScanner<T> {
season.processing &&
existingSeason.status4k !== MediaStatus.DELETED
? MediaStatus.PROCESSING
: season.is4kOverride &&
!season.processing &&
season.episodes4k === 0 &&
existingSeason.status4k === MediaStatus.PROCESSING
? MediaStatus.UNKNOWN
: existingSeason.status4k;
} else {
newSeasons.push(

View File

@@ -5,7 +5,6 @@ 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';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
@@ -219,7 +218,6 @@ interface RequestCardProps {
}
const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
const settings = useSettings();
const { ref, inView } = useInView({
triggerOnce: true,
});
@@ -402,14 +400,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
<div className="my-0.5 hidden items-center text-sm sm:my-1 sm:flex">
<span className="mr-2 font-bold ">
{intl.formatMessage(messages.seasons, {
seasonCount:
(settings.currentSettings.enableSpecialEpisodes
? title.seasons.length
: title.seasons.filter(
(season) => season.seasonNumber !== 0
).length) === request.seasons.length
? 0
: request.seasons.length,
seasonCount: request.seasons.length,
})}
</span>
<div className="hide-scrollbar overflow-x-scroll">

View File

@@ -5,7 +5,6 @@ import ConfirmButton from '@app/components/Common/ConfirmButton';
import RequestModal from '@app/components/RequestModal';
import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
@@ -295,7 +294,6 @@ interface RequestItemProps {
}
const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
const settings = useSettings();
const { ref, inView } = useInView({
triggerOnce: true,
});
@@ -470,14 +468,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
<div className="card-field">
<span className="card-field-name">
{intl.formatMessage(messages.seasons, {
seasonCount:
(settings.currentSettings.enableSpecialEpisodes
? title.seasons.length
: title.seasons.filter(
(season) => season.seasonNumber !== 0
).length) === request.seasons.length
? 0
: request.seasons.length,
seasonCount: request.seasons.length,
})}
</span>
<div className="hide-scrollbar flex flex-nowrap overflow-x-scroll">

View File

@@ -49,7 +49,12 @@ const NotificationsPushover = () => {
const { data: soundsData } = useSWR<PushoverSound[]>(
data?.options.accessToken
? `/api/v1/settings/notifications/pushover/sounds?token=${data.options.accessToken}`
: null
: null,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
shouldRetryOnError: false,
}
);
const NotificationsPushoverSchema = Yup.object().shape({

View File

@@ -38,6 +38,8 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
proxyBypassFilterTip:
"Use ',' as a separator, and '*.' as a wildcard for subdomains",
proxyBypassLocalAddresses: 'Bypass Proxy for Local Addresses',
validationDnsCacheMinTtl: 'You must provide a valid minimum TTL',
validationDnsCacheMaxTtl: 'You must provide a valid maximum TTL',
validationProxyPort: 'You must provide a valid port',
networkDisclaimer:
'Network parameters from your container/system should be used instead of these settings. See the {docs} for more information.',
@@ -64,6 +66,20 @@ const SettingsNetwork = () => {
} = useSWR<NetworkSettings>('/api/v1/settings/network');
const NetworkSettingsSchema = Yup.object().shape({
dnsCacheForceMinTtl: Yup.number().when('dnsCacheEnabled', {
is: true,
then: Yup.number()
.typeError(intl.formatMessage(messages.validationDnsCacheMinTtl))
.required(intl.formatMessage(messages.validationDnsCacheMinTtl))
.min(0),
}),
dnsCacheForceMaxTtl: Yup.number().when('dnsCacheEnabled', {
is: true,
then: Yup.number()
.typeError(intl.formatMessage(messages.validationDnsCacheMaxTtl))
.required(intl.formatMessage(messages.validationDnsCacheMaxTtl))
.min(0),
}),
proxyPort: Yup.number().when('proxyEnabled', {
is: (proxyEnabled: boolean) => proxyEnabled,
then: Yup.number().required(
@@ -120,8 +136,8 @@ const SettingsNetwork = () => {
trustProxy: values.trustProxy,
dnsCache: {
enabled: values.dnsCacheEnabled,
forceMinTtl: values.dnsCacheForceMinTtl,
forceMaxTtl: values.dnsCacheForceMaxTtl,
forceMinTtl: Number(values.dnsCacheForceMinTtl),
forceMaxTtl: Number(values.dnsCacheForceMaxTtl),
},
proxy: {
enabled: values.proxyEnabled,
@@ -281,7 +297,7 @@ const SettingsNetwork = () => {
<Field
id="dnsCacheForceMinTtl"
name="dnsCacheForceMinTtl"
type="text"
type="number"
/>
</div>
{errors.dnsCacheForceMinTtl &&
@@ -305,7 +321,7 @@ const SettingsNetwork = () => {
<Field
id="dnsCacheForceMaxTtl"
name="dnsCacheForceMaxTtl"
type="text"
type="number"
/>
</div>
{errors.dnsCacheForceMaxTtl &&
@@ -375,7 +391,7 @@ const SettingsNetwork = () => {
<Field
id="proxyPort"
name="proxyPort"
type="text"
type="number"
/>
</div>
{errors.proxyPort &&

View File

@@ -139,7 +139,11 @@ const StatusBadge = ({
<div
className={`
absolute top-0 left-0 z-10 flex h-full bg-opacity-80 ${
status === MediaStatus.PROCESSING ? 'bg-indigo-500' : 'bg-green-500'
status === MediaStatus.DELETED
? 'bg-red-600'
: status === MediaStatus.PROCESSING
? 'bg-indigo-500'
: 'bg-green-500'
} transition-all duration-200 ease-in-out
`}
style={{
@@ -373,11 +377,66 @@ const StatusBadge = ({
case MediaStatus.DELETED:
return (
<Tooltip content={mediaLinkDescription}>
<Badge badgeType="danger">
{intl.formatMessage(is4k ? messages.status4k : messages.status, {
status: intl.formatMessage(globalMessages.deleted),
})}
<Tooltip
content={inProgress ? tooltipContent : mediaLinkDescription}
className={`${
inProgress && 'hidden max-h-96 w-96 overflow-y-auto sm:block'
}`}
tooltipConfig={{
...(inProgress && { interactive: true, delayHide: 100 }),
}}
>
<Badge
badgeType="danger"
href={mediaLink}
className={`${
inProgress &&
'relative !bg-gray-700 !bg-opacity-80 !px-0 hover:!bg-gray-700'
} overflow-hidden`}
>
{inProgress && badgeDownloadProgress}
<div
className={`relative z-20 flex items-center ${
inProgress && 'px-2'
}`}
>
<span>
{intl.formatMessage(
is4k ? messages.status4k : messages.status,
{
status: inProgress
? intl.formatMessage(globalMessages.processing)
: intl.formatMessage(globalMessages.deleted),
}
)}
</span>
{inProgress && (
<>
{mediaType === 'tv' &&
downloadItem[0].episode &&
(downloadItem.length > 1 &&
downloadItem.every(
(item) =>
item.downloadId &&
item.downloadId === downloadItem[0].downloadId
) ? (
<span className="ml-1">
{intl.formatMessage(messages.seasonnumber, {
seasonNumber: downloadItem[0].episode.seasonNumber,
})}
</span>
) : (
<span className="ml-1">
{intl.formatMessage(messages.seasonepisodenumber, {
seasonNumber: downloadItem[0].episode.seasonNumber,
episodeNumber: downloadItem[0].episode.episodeNumber,
})}
</span>
))}
<Spinner className="ml-1 h-3 w-3" />
</>
)}
</div>
</Badge>
</Tooltip>
);

View File

@@ -1022,6 +1022,8 @@
"components.Settings.SettingsNetwork.toastSettingsSuccess": "Settings saved successfully!",
"components.Settings.SettingsNetwork.trustProxy": "Enable Proxy Support",
"components.Settings.SettingsNetwork.trustProxyTip": "Allow Seerr to correctly register client IP addresses behind a proxy",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "You must provide a valid maximum TTL",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "You must provide a valid minimum TTL",
"components.Settings.SettingsNetwork.validationProxyPort": "You must provide a valid port",
"components.Settings.SettingsUsers.atLeastOneAuth": "At least one authentication method must be selected.",
"components.Settings.SettingsUsers.defaultPermissions": "Default Permissions",