Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot]
b4898bdad9 build(docker): update node.js to 7aa86fa 2026-02-12 14:01:43 +00:00
9 changed files with 241 additions and 393 deletions

View File

@@ -1,4 +1,4 @@
FROM node:22.22.0-alpine3.22@sha256:0c49915657c1c77c64c8af4d91d2f13fe96853bbd957993ed00dd592cbecc284 AS base
FROM node:22.22.0-alpine3.22@sha256:7aa86fa052f6e4b101557ccb56717cb4311be1334381f526fe013418fe157384 AS base
ARG SOURCE_DATE_EPOCH
ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
@@ -33,7 +33,7 @@ RUN pnpm build
RUN rm -rf .next/cache
FROM node:22.22.0-alpine3.22@sha256:0c49915657c1c77c64c8af4d91d2f13fe96853bbd957993ed00dd592cbecc284
FROM node:22.22.0-alpine3.22@sha256:7aa86fa052f6e4b101557ccb56717cb4311be1334381f526fe013418fe157384
ARG SOURCE_DATE_EPOCH
ARG COMMIT_TAG
ENV NODE_ENV=production

View File

@@ -1,4 +1,4 @@
FROM node:22.22.0-alpine3.22@sha256:0c49915657c1c77c64c8af4d91d2f13fe96853bbd957993ed00dd592cbecc284
FROM node:22.22.0-alpine3.22@sha256:7aa86fa052f6e4b101557ccb56717cb4311be1334381f526fe013418fe157384
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

View File

@@ -385,6 +385,26 @@ class BaseScanner<T> {
}
}
// We want to skip specials when checking if a show is available
const isAllStandardSeasons =
seasons.length &&
seasons
.filter((season) => season.seasonNumber !== 0)
.every(
(season) =>
season.episodes === season.totalEpisodes && season.episodes > 0
);
const isAll4kSeasons =
seasons.length &&
seasons
.filter((season) => season.seasonNumber !== 0)
.every(
(season) =>
season.episodes4k === season.totalEpisodes &&
season.episodes4k > 0
);
if (media) {
media.seasons = [...media.seasons, ...newSeasons];
@@ -444,38 +464,43 @@ class BaseScanner<T> {
externalServiceSlug;
}
const nonSpecialSeasons = media.seasons.filter(
(s) => s.seasonNumber !== 0
);
// Check the actual season objects instead scanner input
// to determine overall availability status
const isAllStandardSeasonsAvailable =
nonSpecialSeasons.length > 0 &&
nonSpecialSeasons.every((s) => s.status === MediaStatus.AVAILABLE);
const isAll4kSeasonsAvailable =
nonSpecialSeasons.length > 0 &&
nonSpecialSeasons.every((s) => s.status4k === MediaStatus.AVAILABLE);
media.status = isAllStandardSeasonsAvailable
? MediaStatus.AVAILABLE
: media.seasons.some(
(season) =>
season.status === MediaStatus.PARTIALLY_AVAILABLE ||
season.status === MediaStatus.AVAILABLE
)
? MediaStatus.PARTIALLY_AVAILABLE
: (!seasons.length && media.status !== MediaStatus.DELETED) ||
media.seasons.some(
(season) => season.status === MediaStatus.PROCESSING
// If the show is already available, and there are no new seasons, dont adjust
// the status. Skip specials when performing availability check
const shouldStayAvailable =
media.status === MediaStatus.AVAILABLE &&
newSeasons.filter(
(season) =>
season.status !== MediaStatus.UNKNOWN &&
season.status !== MediaStatus.DELETED &&
season.seasonNumber !== 0
).length === 0;
const shouldStayAvailable4k =
media.status4k === MediaStatus.AVAILABLE &&
newSeasons.filter(
(season) =>
season.status4k !== MediaStatus.UNKNOWN &&
season.status4k !== MediaStatus.DELETED &&
season.seasonNumber !== 0
).length === 0;
media.status =
isAllStandardSeasons || shouldStayAvailable
? MediaStatus.AVAILABLE
: media.seasons.some(
(season) =>
season.status === MediaStatus.PARTIALLY_AVAILABLE ||
season.status === MediaStatus.AVAILABLE
)
? MediaStatus.PROCESSING
: media.status === MediaStatus.DELETED
? MediaStatus.DELETED
: MediaStatus.UNKNOWN;
? MediaStatus.PARTIALLY_AVAILABLE
: (!seasons.length && media.status !== MediaStatus.DELETED) ||
media.seasons.some(
(season) => season.status === MediaStatus.PROCESSING
)
? MediaStatus.PROCESSING
: media.status === MediaStatus.DELETED
? MediaStatus.DELETED
: MediaStatus.UNKNOWN;
media.status4k =
isAll4kSeasonsAvailable && this.enable4kShow
(isAll4kSeasons || shouldStayAvailable4k) && this.enable4kShow
? MediaStatus.AVAILABLE
: this.enable4kShow &&
media.seasons.some(
@@ -495,22 +520,6 @@ class BaseScanner<T> {
await mediaRepository.save(media);
this.log(`Updating existing title: ${title}`);
} else {
// For new media, check actual newSeasons objects instead of scanner
// input to determine overall availability status
const nonSpecialNewSeasons = newSeasons.filter(
(s) => s.seasonNumber !== 0
);
const isAllStandardSeasonsAvailable =
nonSpecialNewSeasons.length > 0 &&
nonSpecialNewSeasons.every((s) => s.status === MediaStatus.AVAILABLE);
const isAll4kSeasonsAvailable =
nonSpecialNewSeasons.length > 0 &&
nonSpecialNewSeasons.every(
(s) => s.status4k === MediaStatus.AVAILABLE
);
const newMedia = new Media({
mediaType: MediaType.TV,
seasons: newSeasons,
@@ -555,7 +564,7 @@ class BaseScanner<T> {
)
? jellyfinMediaId
: undefined,
status: isAllStandardSeasonsAvailable
status: isAllStandardSeasons
? MediaStatus.AVAILABLE
: newSeasons.some(
(season) =>
@@ -569,7 +578,7 @@ class BaseScanner<T> {
? MediaStatus.PROCESSING
: MediaStatus.UNKNOWN,
status4k:
isAll4kSeasonsAvailable && this.enable4kShow
isAll4kSeasons && this.enable4kShow
? MediaStatus.AVAILABLE
: this.enable4kShow &&
newSeasons.some(

View File

@@ -15,7 +15,6 @@ import {
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import { MediaRequest } from '@server/entity/MediaRequest';
import Season from '@server/entity/Season';
import SeasonRequest from '@server/entity/SeasonRequest';
import notificationManager, { Notification } from '@server/lib/notifications';
import { getSettings } from '@server/lib/settings';
@@ -28,7 +27,7 @@ import type {
RemoveEvent,
UpdateEvent,
} from 'typeorm';
import { EventSubscriber, Not } from 'typeorm';
import { EventSubscriber } from 'typeorm';
const sanitizeDisplayName = (displayName: string): string => {
return displayName
@@ -398,23 +397,10 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
await mediaRepository.save(media);
})
.catch(async () => {
try {
const requestRepository = getRepository(MediaRequest);
const requestRepository = getRepository(MediaRequest);
if (entity.status !== MediaRequestStatus.FAILED) {
entity.status = MediaRequestStatus.FAILED;
await requestRepository.save(entity);
}
} catch (saveError) {
logger.error('Failed to mark request as FAILED', {
label: 'Media Request',
requestId: entity.id,
errorMessage:
saveError instanceof Error
? saveError.message
: String(saveError),
});
}
entity.status = MediaRequestStatus.FAILED;
requestRepository.save(entity);
logger.warn(
'Something went wrong sending movie request to Radarr, marking status as FAILED',
@@ -517,6 +503,7 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
const media = await mediaRepository.findOne({
where: { id: entity.media.id },
relations: { requests: true },
});
if (!media) {
@@ -703,6 +690,7 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
// We grab media again here to make sure we have the latest version of it
const media = await mediaRepository.findOne({
where: { id: entity.media.id },
relations: { requests: true },
});
if (!media) {
@@ -719,23 +707,10 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
await mediaRepository.save(media);
})
.catch(async () => {
try {
const requestRepository = getRepository(MediaRequest);
const requestRepository = getRepository(MediaRequest);
if (entity.status !== MediaRequestStatus.FAILED) {
entity.status = MediaRequestStatus.FAILED;
await requestRepository.save(entity);
}
} catch (saveError) {
logger.error('Failed to mark request as FAILED', {
label: 'Media Request',
requestId: entity.id,
errorMessage:
saveError instanceof Error
? saveError.message
: String(saveError),
});
}
entity.status = MediaRequestStatus.FAILED;
requestRepository.save(entity);
logger.warn(
'Something went wrong sending series request to Sonarr, marking status as FAILED',
@@ -783,6 +758,7 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOne({
where: { id: entity.media.id },
relations: { requests: true },
});
if (!media) {
logger.error('Media data not found', {
@@ -792,29 +768,26 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
});
return;
}
const statusKey = entity.is4k ? 'status4k' : 'status';
const seasonRequestRepository = getRepository(SeasonRequest);
const requestRepository = getRepository(MediaRequest);
if (
entity.status === MediaRequestStatus.APPROVED &&
// Do not update the status if the item is already partially available or available
media[statusKey] !== MediaStatus.AVAILABLE &&
media[statusKey] !== MediaStatus.PARTIALLY_AVAILABLE &&
media[statusKey] !== MediaStatus.PROCESSING
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
media[entity.is4k ? 'status4k' : 'status'] !==
MediaStatus.PARTIALLY_AVAILABLE &&
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.PROCESSING
) {
media[statusKey] = MediaStatus.PROCESSING;
await mediaRepository.save(media);
media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
mediaRepository.save(media);
}
if (
media.mediaType === MediaType.MOVIE &&
entity.status === MediaRequestStatus.DECLINED &&
media[statusKey] !== MediaStatus.DELETED
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
) {
media[statusKey] = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
}
/**
@@ -826,71 +799,14 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
if (
media.mediaType === MediaType.TV &&
entity.status === MediaRequestStatus.DECLINED &&
media[statusKey] === MediaStatus.PENDING
media.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING
).length === 0 &&
media[entity.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING &&
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
) {
const pendingCount = await requestRepository.count({
where: {
media: { id: media.id },
status: MediaRequestStatus.PENDING,
is4k: entity.is4k,
id: Not(entity.id),
},
});
if (pendingCount === 0) {
// Re-fetch media without requests to avoid cascade issues
const freshMedia = await mediaRepository.findOne({
where: { id: media.id },
});
if (freshMedia) {
freshMedia[statusKey] = MediaStatus.UNKNOWN;
await mediaRepository.save(freshMedia);
}
}
}
// Reset season statuses when a TV request is declined
if (
media.mediaType === MediaType.TV &&
entity.status === MediaRequestStatus.DECLINED
) {
const seasonRepository = getRepository(Season);
const actualSeasons = await seasonRepository.find({
where: { media: { id: media.id } },
});
for (const seasonRequest of entity.seasons) {
seasonRequest.status = MediaRequestStatus.DECLINED;
await seasonRequestRepository.save(seasonRequest);
const season = actualSeasons.find(
(s) => s.seasonNumber === seasonRequest.seasonNumber
);
if (season && season[statusKey] === MediaStatus.PENDING) {
const otherActiveRequests = await requestRepository
.createQueryBuilder('request')
.leftJoinAndSelect('request.seasons', 'season')
.where('request.mediaId = :mediaId', { mediaId: media.id })
.andWhere('request.id != :requestId', { requestId: entity.id })
.andWhere('request.is4k = :is4k', { is4k: entity.is4k })
.andWhere('request.status NOT IN (:...statuses)', {
statuses: [
MediaRequestStatus.DECLINED,
MediaRequestStatus.COMPLETED,
],
})
.andWhere('season.seasonNumber = :seasonNumber', {
seasonNumber: season.seasonNumber,
})
.getCount();
if (otherActiveRequests === 0) {
season[statusKey] = MediaStatus.UNKNOWN;
await seasonRepository.save(season);
}
}
}
media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
}
// Approve child seasons if parent is approved
@@ -914,74 +830,54 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
relations: { requests: true },
});
const needsStatusUpdate =
if (!fullMedia) return;
if (
!fullMedia.requests.some((request) => !request.is4k) &&
fullMedia.status !== MediaStatus.AVAILABLE;
fullMedia.status !== MediaStatus.AVAILABLE
) {
fullMedia.status = MediaStatus.UNKNOWN;
}
const needs4kStatusUpdate =
if (
!fullMedia.requests.some((request) => request.is4k) &&
fullMedia.status4k !== MediaStatus.AVAILABLE;
if (needsStatusUpdate || needs4kStatusUpdate) {
// Re-fetch WITHOUT requests to avoid cascade issues on save
const cleanMedia = await manager.findOneOrFail(Media, {
where: { id: entity.media.id },
});
if (needsStatusUpdate) {
cleanMedia.status = MediaStatus.UNKNOWN;
}
if (needs4kStatusUpdate) {
cleanMedia.status4k = MediaStatus.UNKNOWN;
}
await manager.save(cleanMedia);
fullMedia.status4k !== MediaStatus.AVAILABLE
) {
fullMedia.status4k = MediaStatus.UNKNOWN;
}
await manager.save(fullMedia);
}
public async afterUpdate(event: UpdateEvent<MediaRequest>): Promise<void> {
public afterUpdate(event: UpdateEvent<MediaRequest>): void {
if (!event.entity) {
return;
}
try {
await this.sendToRadarr(event.entity as MediaRequest);
await this.sendToSonarr(event.entity as MediaRequest);
await this.updateParentStatus(event.entity as MediaRequest);
this.sendToRadarr(event.entity as MediaRequest);
this.sendToSonarr(event.entity as MediaRequest);
if (event.entity.status === MediaRequestStatus.COMPLETED) {
if (event.entity.media.mediaType === MediaType.MOVIE) {
await this.notifyAvailableMovie(event.entity as MediaRequest, event);
}
if (event.entity.media.mediaType === MediaType.TV) {
await this.notifyAvailableSeries(event.entity as MediaRequest, event);
}
this.updateParentStatus(event.entity as MediaRequest);
if (event.entity.status === MediaRequestStatus.COMPLETED) {
if (event.entity.media.mediaType === MediaType.MOVIE) {
this.notifyAvailableMovie(event.entity as MediaRequest, event);
}
if (event.entity.media.mediaType === MediaType.TV) {
this.notifyAvailableSeries(event.entity as MediaRequest, event);
}
} catch (e) {
logger.error('Error in afterUpdate subscriber', {
label: 'Media Request',
requestId: (event.entity as MediaRequest).id,
errorMessage: e instanceof Error ? e.message : String(e),
});
}
}
public async afterInsert(event: InsertEvent<MediaRequest>): Promise<void> {
public afterInsert(event: InsertEvent<MediaRequest>): void {
if (!event.entity) {
return;
}
try {
await this.sendToRadarr(event.entity as MediaRequest);
await this.sendToSonarr(event.entity as MediaRequest);
await this.updateParentStatus(event.entity as MediaRequest);
} catch (e) {
logger.error('Error in afterInsert subscriber', {
label: 'Media Request',
requestId: (event.entity as MediaRequest).id,
errorMessage: e instanceof Error ? e.message : String(e),
});
}
this.sendToRadarr(event.entity as MediaRequest);
this.sendToSonarr(event.entity as MediaRequest);
this.updateParentStatus(event.entity as MediaRequest);
}
public async afterRemove(event: RemoveEvent<MediaRequest>): Promise<void> {

View File

@@ -360,12 +360,7 @@ const TvRequestModal = ({
).length > 0
) {
data.mediaInfo.requests
.filter(
(request) =>
request.is4k === is4k &&
request.status !== MediaRequestStatus.DECLINED &&
request.status !== MediaRequestStatus.COMPLETED
)
.filter((request) => request.is4k === is4k)
.forEach((request) => {
if (!seasonRequest) {
seasonRequest = request.seasons.find(

View File

@@ -134,6 +134,7 @@ const OverrideRuleTiles = ({
const users: User[] = response.data.results;
setUsers(users);
}
setUsers(users);
})();
}, [rules, users]);

View File

@@ -69,7 +69,6 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
dnsCacheGlobalStats: 'Global DNS Cache Stats',
dnsCacheGlobalStatsDescription:
'These stats are aggregated across all DNS cache entries.',
dnsNoCacheEntries: 'No DNS lookups have been cached yet.',
size: 'Size',
hits: 'Hits',
misses: 'Misses',
@@ -612,133 +611,91 @@ const SettingsJobs = () => {
</Table.TBody>
</Table>
</div>
{cacheData?.dnsCache != null && (
<>
<div>
<h3 className="heading">{intl.formatMessage(messages.dnsCache)}</h3>
<p className="description">
{intl.formatMessage(messages.dnsCacheDescription)}
</p>
</div>
<div className="section">
<Table>
<thead>
<tr>
<Table.TH>
{intl.formatMessage(messages.dnscachename)}
</Table.TH>
<Table.TH>
{intl.formatMessage(messages.dnscacheactiveaddress)}
</Table.TH>
<Table.TH>
{intl.formatMessage(messages.dnscachehits)}
</Table.TH>
<Table.TH>
{intl.formatMessage(messages.dnscachemisses)}
</Table.TH>
<Table.TH>
{intl.formatMessage(messages.dnscacheage)}
</Table.TH>
<Table.TH></Table.TH>
<div>
<h3 className="heading">{intl.formatMessage(messages.dnsCache)}</h3>
<p className="description">
{intl.formatMessage(messages.dnsCacheDescription)}
</p>
</div>
<div className="section">
<Table>
<thead>
<tr>
<Table.TH>{intl.formatMessage(messages.dnscachename)}</Table.TH>
<Table.TH>
{intl.formatMessage(messages.dnscacheactiveaddress)}
</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscachehits)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscachemisses)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscacheage)}</Table.TH>
<Table.TH></Table.TH>
</tr>
</thead>
<Table.TBody>
{Object.entries(cacheData?.dnsCache.entries || {}).map(
([hostname, data]) => (
<tr key={`cache-list-${hostname}`}>
<Table.TD>{hostname}</Table.TD>
<Table.TD>{data.activeAddress}</Table.TD>
<Table.TD>{intl.formatNumber(data.hits)}</Table.TD>
<Table.TD>{intl.formatNumber(data.misses)}</Table.TD>
<Table.TD>{formatAge(data.age)}</Table.TD>
<Table.TD alignText="right">
<Button
buttonType="danger"
onClick={() => flushDnsCache(hostname)}
>
<TrashIcon />
<span>{intl.formatMessage(messages.flushdnscache)}</span>
</Button>
</Table.TD>
</tr>
</thead>
<Table.TBody>
{(() => {
if (!cacheData) {
return (
<tr>
<Table.TD colSpan={6} alignText="center">
<LoadingSpinner />
</Table.TD>
</tr>
);
}
const entries = Object.entries(
cacheData.dnsCache?.entries ?? {}
);
if (entries.length === 0) {
return (
<tr>
<Table.TD colSpan={6} alignText="center">
{intl.formatMessage(messages.dnsNoCacheEntries)}
</Table.TD>
</tr>
);
}
return entries.map(([hostname, data]) => (
<tr key={`cache-list-${hostname}`}>
<Table.TD>{hostname}</Table.TD>
<Table.TD>{data.activeAddress}</Table.TD>
<Table.TD>{intl.formatNumber(data.hits)}</Table.TD>
<Table.TD>{intl.formatNumber(data.misses)}</Table.TD>
<Table.TD>{formatAge(data.age)}</Table.TD>
<Table.TD alignText="right">
<Button
buttonType="danger"
onClick={() => flushDnsCache(hostname)}
>
<TrashIcon />
<span>
{intl.formatMessage(messages.flushdnscache)}
</span>
</Button>
</Table.TD>
</tr>
));
})()}
</Table.TBody>
</Table>
</div>
<div>
<h3 className="heading">
{intl.formatMessage(messages.dnsCacheGlobalStats)}
</h3>
<p className="description">
{intl.formatMessage(messages.dnsCacheGlobalStatsDescription)}
</p>
</div>
<div className="section">
{!cacheData ? (
<LoadingSpinner />
) : (
<Table>
<thead>
<tr>
{Object.entries(cacheData.dnsCache?.stats ?? {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName]) => (
<Table.TH key={`dns-stat-header-${statName}`}>
{messages[statName]
? intl.formatMessage(messages[statName])
: statName}
</Table.TH>
))}
</tr>
</thead>
<Table.TBody>
<tr>
{Object.entries(cacheData.dnsCache?.stats ?? {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName, statValue]) => (
<Table.TD key={`dns-stat-${statName}`}>
{statName === 'hitRate'
? intl.formatNumber(statValue, {
style: 'percent',
maximumFractionDigits: 2,
})
: intl.formatNumber(statValue)}
</Table.TD>
))}
</tr>
</Table.TBody>
</Table>
)
)}
</div>
</>
)}
</Table.TBody>
</Table>
</div>
<div>
<h3 className="heading">
{intl.formatMessage(messages.dnsCacheGlobalStats)}
</h3>
<p className="description">
{intl.formatMessage(messages.dnsCacheGlobalStatsDescription)}
</p>
</div>
<div className="section">
<Table>
<thead>
<tr>
{Object.entries(cacheData?.dnsCache.stats || {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName]) => (
<Table.TH key={`dns-stat-header-${statName}`}>
{messages[statName]
? intl.formatMessage(messages[statName])
: statName}
</Table.TH>
))}
</tr>
</thead>
<Table.TBody>
<tr>
{Object.entries(cacheData?.dnsCache.stats || {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName, statValue]) => (
<Table.TD key={`dns-stat-${statName}`}>
{statName === 'hitRate'
? intl.formatNumber(statValue, {
style: 'percent',
maximumFractionDigits: 2,
})
: intl.formatNumber(statValue)}
</Table.TD>
))}
</tr>
</Table.TBody>
</Table>
</div>
<div className="break-words">
<h3 className="heading">{intl.formatMessage(messages.imagecache)}</h3>
<p className="description">

View File

@@ -29,8 +29,6 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
trustProxyTip:
'Allow Seerr to correctly register client IP addresses behind a proxy',
proxyEnabled: 'HTTP(S) Proxy',
proxyEnabledTip:
'Send ALL outgoing HTTP/HTTPS requests through a proxy server (host/port). Does NOT enable HTTPS, SSL, or certificate configuration.',
proxyHostname: 'Proxy Hostname',
proxyPort: 'Proxy Port',
proxySsl: 'Use SSL For Proxy',
@@ -80,16 +78,13 @@ const SettingsNetwork = () => {
then: Yup.number()
.typeError(intl.formatMessage(messages.validationDnsCacheMaxTtl))
.required(intl.formatMessage(messages.validationDnsCacheMaxTtl))
.min(-1),
.min(0),
}),
proxyPort: Yup.number().when('proxyEnabled', {
is: (proxyEnabled: boolean) => proxyEnabled,
then: Yup.number()
.typeError(intl.formatMessage(messages.validationProxyPort))
.integer(intl.formatMessage(messages.validationProxyPort))
.min(1, intl.formatMessage(messages.validationProxyPort))
.max(65535, intl.formatMessage(messages.validationProxyPort))
.required(intl.formatMessage(messages.validationProxyPort)),
then: Yup.number().required(
intl.formatMessage(messages.validationProxyPort)
),
}),
});
@@ -293,50 +288,50 @@ const SettingsNetwork = () => {
<div className="form-row">
<label
htmlFor="dnsCacheForceMinTtl"
className="text-label"
className="checkbox-label"
>
{intl.formatMessage(messages.dnsCacheForceMinTtl)}
</label>
<div className="form-input-area">
<Field
id="dnsCacheForceMinTtl"
name="dnsCacheForceMinTtl"
type="text"
inputMode="numeric"
className="short"
/>
<div className="form-input-field">
<Field
id="dnsCacheForceMinTtl"
name="dnsCacheForceMinTtl"
type="number"
/>
</div>
{errors.dnsCacheForceMinTtl &&
touched.dnsCacheForceMinTtl &&
typeof errors.dnsCacheForceMinTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMinTtl}
</div>
)}
</div>
{errors.dnsCacheForceMinTtl &&
touched.dnsCacheForceMinTtl &&
typeof errors.dnsCacheForceMinTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMinTtl}
</div>
)}
</div>
<div className="form-row">
<label
htmlFor="dnsCacheForceMaxTtl"
className="text-label"
className="checkbox-label"
>
{intl.formatMessage(messages.dnsCacheForceMaxTtl)}
</label>
<div className="form-input-area">
<Field
id="dnsCacheForceMaxTtl"
name="dnsCacheForceMaxTtl"
type="text"
inputMode="text"
className="short"
/>
<div className="form-input-field">
<Field
id="dnsCacheForceMaxTtl"
name="dnsCacheForceMaxTtl"
type="number"
/>
</div>
{errors.dnsCacheForceMaxTtl &&
touched.dnsCacheForceMaxTtl &&
typeof errors.dnsCacheForceMaxTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMaxTtl}
</div>
)}
</div>
{errors.dnsCacheForceMaxTtl &&
touched.dnsCacheForceMaxTtl &&
typeof errors.dnsCacheForceMaxTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMaxTtl}
</div>
)}
</div>
</div>
</>
@@ -348,9 +343,6 @@ const SettingsNetwork = () => {
</span>
<SettingsBadge badgeType="advanced" className="mr-2" />
<SettingsBadge badgeType="restartRequired" />
<span className="label-tip">
{intl.formatMessage(messages.proxyEnabledTip)}
</span>
</label>
<div className="form-input-area">
<Field
@@ -395,13 +387,13 @@ const SettingsNetwork = () => {
{intl.formatMessage(messages.proxyPort)}
</label>
<div className="form-input-area">
<Field
id="proxyPort"
name="proxyPort"
type="text"
inputMode="numeric"
className="short"
/>
<div className="form-input-field">
<Field
id="proxyPort"
name="proxyPort"
type="number"
/>
</div>
{errors.proxyPort &&
touched.proxyPort &&
typeof errors.proxyPort === 'string' && (

View File

@@ -890,7 +890,6 @@
"components.Settings.SettingsJobsCache.dnsCacheDescription": "Seerr caches DNS lookups to optimize performance and avoid making unnecessary API calls.",
"components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "Global DNS Cache Stats",
"components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "These stats are aggregated across all DNS cache entries.",
"components.Settings.SettingsJobsCache.dnsNoCacheEntries": "No DNS lookups have been cached yet.",
"components.Settings.SettingsJobsCache.dnscacheactiveaddress": "Active Address",
"components.Settings.SettingsJobsCache.dnscacheage": "Age",
"components.Settings.SettingsJobsCache.dnscacheflushed": "{hostname} dns cache flushed.",
@@ -1016,7 +1015,6 @@
"components.Settings.SettingsNetwork.proxyBypassFilterTip": "Use ',' as a separator, and '*.' as a wildcard for subdomains",
"components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "Bypass Proxy for Local Addresses",
"components.Settings.SettingsNetwork.proxyEnabled": "HTTP(S) Proxy",
"components.Settings.SettingsNetwork.proxyEnabledTip": "Send ALL outgoing HTTP/HTTPS requests through a proxy server (host/port). Does NOT enable HTTPS, SSL, or certificate configuration.",
"components.Settings.SettingsNetwork.proxyHostname": "Proxy Hostname",
"components.Settings.SettingsNetwork.proxyPassword": "Proxy Password",
"components.Settings.SettingsNetwork.proxyPort": "Proxy Port",