Compare commits
3 Commits
fix/0xsysr
...
fallenbage
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b92a26bfcb | ||
|
|
0753d7f48e | ||
|
|
6f9988085b |
@@ -24,6 +24,10 @@ Set this to the username and password for your ntfy.sh server.
|
||||
|
||||
Set this to the token for your ntfy.sh server.
|
||||
|
||||
### Priority (optional)
|
||||
|
||||
Set the priority level for notifications. Options range from Minimum (1) to Urgent (5), with Default (3) being the standard level. Higher priority notifications may bypass Do Not Disturb settings on some devices.
|
||||
|
||||
:::info
|
||||
Please refer to the [ntfy.sh API documentation](https://docs.ntfy.sh/) for more details on configuring these notifications.
|
||||
:::
|
||||
|
||||
@@ -209,34 +209,6 @@ 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);
|
||||
}
|
||||
@@ -346,38 +318,6 @@ 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[]
|
||||
|
||||
@@ -97,10 +97,7 @@ app
|
||||
|
||||
// Register HTTP proxy
|
||||
if (settings.network.proxy.enabled) {
|
||||
await createCustomProxyAgent(
|
||||
settings.network.proxy,
|
||||
settings.network.forceIpv4First
|
||||
);
|
||||
await createCustomProxyAgent(settings.network.proxy);
|
||||
}
|
||||
|
||||
// Migrate library types
|
||||
|
||||
@@ -143,9 +143,7 @@ class AvailabilitySync {
|
||||
const { existsInPlex: existsInPlex4k } =
|
||||
await this.mediaExistsInPlex(media, true);
|
||||
|
||||
// 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) {
|
||||
if (existsInPlex || existsInRadarr) {
|
||||
movieExists = true;
|
||||
logger.info(
|
||||
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
@@ -155,7 +153,7 @@ class AvailabilitySync {
|
||||
);
|
||||
}
|
||||
|
||||
if (existsInPlex4k) {
|
||||
if (existsInPlex4k || existsInRadarr4k) {
|
||||
movieExists4k = true;
|
||||
logger.info(
|
||||
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
@@ -242,9 +240,7 @@ class AvailabilitySync {
|
||||
|
||||
//plex
|
||||
if (mediaServerType === MediaServerType.PLEX) {
|
||||
// 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) {
|
||||
if (existsInPlex || existsInSonarr) {
|
||||
showExists = true;
|
||||
logger.info(
|
||||
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
@@ -256,7 +252,7 @@ class AvailabilitySync {
|
||||
}
|
||||
|
||||
if (mediaServerType === MediaServerType.PLEX) {
|
||||
if (existsInPlex4k) {
|
||||
if (existsInPlex4k || existsInSonarr4k) {
|
||||
showExists4k = true;
|
||||
logger.info(
|
||||
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||
@@ -304,6 +300,7 @@ 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) =>
|
||||
@@ -314,7 +311,48 @@ 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) =>
|
||||
@@ -325,32 +363,44 @@ class AvailabilitySync {
|
||||
filteredSeasonsMap4k.set(season.seasonNumber, false)
|
||||
);
|
||||
|
||||
let finalSeasons: Map<number, boolean>;
|
||||
let finalSeasons4k: Map<number, boolean>;
|
||||
// 4k
|
||||
const finalSeasons4k: Map<number, boolean> = new Map();
|
||||
|
||||
if (mediaServerType === MediaServerType.PLEX) {
|
||||
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,
|
||||
]);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -615,21 +665,6 @@ 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
|
||||
@@ -656,63 +691,22 @@ class AvailabilitySync {
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
} catch (ex) {
|
||||
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 {
|
||||
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,
|
||||
externalServiceId: externalServiceId,
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
@@ -760,6 +754,7 @@ 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 ${
|
||||
@@ -858,66 +853,31 @@ 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 (!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 (ratingKey && !is4k) {
|
||||
plexMedia = await this.plexClient?.getMetadata(ratingKey);
|
||||
|
||||
if (media.mediaType === 'tv') {
|
||||
this.plexSeasonsCache[ratingKey] =
|
||||
await this.plexClient?.getChildrenMetadata(ratingKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (ratingKey4k && is4k) {
|
||||
plexMedia = await this.plexClient?.getMetadata(ratingKey4k);
|
||||
|
||||
if (media.mediaType === 'tv') {
|
||||
this.plexSeasonsCache[ratingKey4k] =
|
||||
await this.plexClient?.getChildrenMetadata(ratingKey4k);
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
);
|
||||
if (media.mediaType === 'tv') {
|
||||
this.plexSeasonsCache[ratingKey] =
|
||||
await this.plexClient?.getChildrenMetadata(ratingKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (ratingKey4k && is4k) {
|
||||
plexMedia = await this.plexClient?.getMetadata(ratingKey4k);
|
||||
|
||||
if (media.mediaType === 'tv') {
|
||||
this.plexSeasonsCache[ratingKey4k] =
|
||||
await this.plexClient?.getChildrenMetadata(ratingKey4k);
|
||||
}
|
||||
}
|
||||
|
||||
if (plexMedia) {
|
||||
existsInPlex = true;
|
||||
}
|
||||
} catch (ex) {
|
||||
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 {
|
||||
if (!ex.message.includes('404')) {
|
||||
existsInPlex = true;
|
||||
preventSeasonSearch = true;
|
||||
logger.debug(
|
||||
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${
|
||||
@@ -1033,8 +993,8 @@ class AvailabilitySync {
|
||||
existsInJellyfin = true;
|
||||
}
|
||||
} catch (ex) {
|
||||
if (!ex.message.includes('404') && !ex.message.includes('500')) {
|
||||
existsInJellyfin = true;
|
||||
if (!ex.message.includes('404' || '500')) {
|
||||
existsInJellyfin = false;
|
||||
preventSeasonSearch = true;
|
||||
logger.debug(
|
||||
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${
|
||||
|
||||
@@ -27,7 +27,7 @@ class NtfyAgent
|
||||
const { embedPoster } = settings.notifications.agents.ntfy;
|
||||
|
||||
const topic = this.getSettings().options.topic;
|
||||
const priority = 3;
|
||||
const priority = this.getSettings().options.priority ?? 3;
|
||||
|
||||
const title = payload.event
|
||||
? `${payload.event} - ${payload.subject}`
|
||||
|
||||
@@ -45,17 +45,7 @@ class PushoverAgent
|
||||
}
|
||||
|
||||
public shouldSend(): boolean {
|
||||
const settings = this.getSettings();
|
||||
|
||||
if (
|
||||
settings.enabled &&
|
||||
settings.options.accessToken &&
|
||||
settings.options.userToken
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private async getImagePayload(
|
||||
|
||||
@@ -115,11 +115,9 @@ class BaseScanner<T> {
|
||||
let changedExisting = false;
|
||||
|
||||
if (existing[is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE) {
|
||||
existing[is4k ? 'status4k' : 'status'] = !processing
|
||||
? MediaStatus.AVAILABLE
|
||||
: existing[is4k ? 'status4k' : 'status'] === MediaStatus.DELETED
|
||||
? MediaStatus.DELETED
|
||||
: MediaStatus.PROCESSING;
|
||||
existing[is4k ? 'status4k' : 'status'] = processing
|
||||
? MediaStatus.PROCESSING
|
||||
: MediaStatus.AVAILABLE;
|
||||
if (mediaAddedAt) {
|
||||
existing.mediaAddedAt = mediaAddedAt;
|
||||
}
|
||||
@@ -332,11 +330,6 @@ 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
|
||||
@@ -352,11 +345,6 @@ 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(
|
||||
|
||||
@@ -296,6 +296,7 @@ export interface NotificationAgentNtfy extends NotificationAgentConfig {
|
||||
password?: string;
|
||||
authMethodToken?: boolean;
|
||||
token?: string;
|
||||
priority?: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -529,6 +530,7 @@ class Settings {
|
||||
options: {
|
||||
url: '',
|
||||
topic: '',
|
||||
priority: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,14 +11,9 @@ export let requestInterceptorFunction: (
|
||||
) => InternalAxiosRequestConfig;
|
||||
|
||||
export default async function createCustomProxyAgent(
|
||||
proxySettings: ProxySettings,
|
||||
forceIpv4First?: boolean
|
||||
proxySettings: ProxySettings
|
||||
) {
|
||||
const defaultAgent = new Agent({
|
||||
keepAliveTimeout: 5000,
|
||||
connections: 50,
|
||||
connect: forceIpv4First ? { family: 4 } : undefined,
|
||||
});
|
||||
const defaultAgent = new Agent({ keepAliveTimeout: 5000 });
|
||||
|
||||
const skipUrl = (url: string | URL) => {
|
||||
const hostname =
|
||||
@@ -72,23 +67,16 @@ export default async function createCustomProxyAgent(
|
||||
uri: proxyUrl,
|
||||
token,
|
||||
keepAliveTimeout: 5000,
|
||||
connections: 50,
|
||||
connect: forceIpv4First ? { family: 4 } : undefined,
|
||||
});
|
||||
|
||||
setGlobalDispatcher(proxyAgent.compose(noProxyInterceptor));
|
||||
|
||||
const agentOptions = {
|
||||
axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl, {
|
||||
headers: token ? { 'proxy-authorization': token } : undefined,
|
||||
keepAlive: true,
|
||||
maxSockets: 50,
|
||||
maxFreeSockets: 10,
|
||||
timeout: 5000,
|
||||
scheduling: 'lifo' as const,
|
||||
family: forceIpv4First ? 4 : undefined,
|
||||
};
|
||||
axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl, agentOptions);
|
||||
axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl, agentOptions);
|
||||
});
|
||||
axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl, {
|
||||
headers: token ? { 'proxy-authorization': token } : undefined,
|
||||
});
|
||||
|
||||
requestInterceptorFunction = (config) => {
|
||||
const url = config.baseURL
|
||||
|
||||
@@ -5,6 +5,7 @@ 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';
|
||||
@@ -218,6 +219,7 @@ interface RequestCardProps {
|
||||
}
|
||||
|
||||
const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
const settings = useSettings();
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true,
|
||||
});
|
||||
@@ -400,7 +402,14 @@ 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: request.seasons.length,
|
||||
seasonCount:
|
||||
(settings.currentSettings.enableSpecialEpisodes
|
||||
? title.seasons.length
|
||||
: title.seasons.filter(
|
||||
(season) => season.seasonNumber !== 0
|
||||
).length) === request.seasons.length
|
||||
? 0
|
||||
: request.seasons.length,
|
||||
})}
|
||||
</span>
|
||||
<div className="hide-scrollbar overflow-x-scroll">
|
||||
|
||||
@@ -5,6 +5,7 @@ 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';
|
||||
@@ -294,6 +295,7 @@ interface RequestItemProps {
|
||||
}
|
||||
|
||||
const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
const settings = useSettings();
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true,
|
||||
});
|
||||
@@ -468,7 +470,14 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
<div className="card-field">
|
||||
<span className="card-field-name">
|
||||
{intl.formatMessage(messages.seasons, {
|
||||
seasonCount: request.seasons.length,
|
||||
seasonCount:
|
||||
(settings.currentSettings.enableSpecialEpisodes
|
||||
? title.seasons.length
|
||||
: title.seasons.filter(
|
||||
(season) => season.seasonNumber !== 0
|
||||
).length) === request.seasons.length
|
||||
? 0
|
||||
: request.seasons.length,
|
||||
})}
|
||||
</span>
|
||||
<div className="hide-scrollbar flex flex-nowrap overflow-x-scroll">
|
||||
|
||||
@@ -27,6 +27,7 @@ const messages = defineMessages(
|
||||
password: 'Password',
|
||||
tokenAuth: 'Token authentication',
|
||||
token: 'Token',
|
||||
priority: 'Priority',
|
||||
ntfysettingssaved: 'Ntfy notification settings saved successfully!',
|
||||
ntfysettingsfailed: 'Ntfy notification settings failed to save.',
|
||||
toastNtfyTestSending: 'Sending ntfy test notification…',
|
||||
@@ -34,6 +35,7 @@ const messages = defineMessages(
|
||||
toastNtfyTestFailed: 'Ntfy test notification failed to send.',
|
||||
validationNtfyUrl: 'You must provide a valid URL',
|
||||
validationNtfyTopic: 'You must provide a topic',
|
||||
validationPriorityRequired: 'You must provide a priority between 1 and 5',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
}
|
||||
);
|
||||
@@ -71,6 +73,14 @@ const NotificationsNtfy = () => {
|
||||
otherwise: Yup.string().nullable(),
|
||||
})
|
||||
.defined(intl.formatMessage(messages.validationNtfyTopic)),
|
||||
priority: Yup.number().when('enabled', {
|
||||
is: true,
|
||||
then: Yup.number()
|
||||
.min(1)
|
||||
.max(5)
|
||||
.required(intl.formatMessage(messages.validationPriorityRequired)),
|
||||
otherwise: Yup.number().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -90,6 +100,7 @@ const NotificationsNtfy = () => {
|
||||
password: data?.options.password,
|
||||
authMethodToken: data?.options.authMethodToken,
|
||||
token: data?.options.token,
|
||||
priority: data?.options.priority,
|
||||
}}
|
||||
validationSchema={NotificationsNtfySchema}
|
||||
onSubmit={async (values) => {
|
||||
@@ -106,6 +117,7 @@ const NotificationsNtfy = () => {
|
||||
password: values.password,
|
||||
authMethodToken: values.authMethodToken,
|
||||
token: values.token,
|
||||
priority: values.priority,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -157,6 +169,7 @@ const NotificationsNtfy = () => {
|
||||
password: values.password,
|
||||
authMethodToken: values.authMethodToken,
|
||||
token: values.token,
|
||||
priority: values.priority,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -313,6 +326,22 @@ const NotificationsNtfy = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-row">
|
||||
<label htmlFor="priority" className="text-label">
|
||||
{intl.formatMessage(messages.priority)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field as="select" id="priority" name="priority">
|
||||
<option value={1}>Minimum</option>
|
||||
<option value={2}>Low</option>
|
||||
<option value={3}>Default</option>
|
||||
<option value={4}>High</option>
|
||||
<option value={5}>Urgent</option>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
currentTypes={values.enabled ? values.types || 0 : 0}
|
||||
onUpdate={(newTypes) => {
|
||||
|
||||
@@ -49,12 +49,7 @@ const NotificationsPushover = () => {
|
||||
const { data: soundsData } = useSWR<PushoverSound[]>(
|
||||
data?.options.accessToken
|
||||
? `/api/v1/settings/notifications/pushover/sounds?token=${data.options.accessToken}`
|
||||
: null,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
shouldRetryOnError: false,
|
||||
}
|
||||
: null
|
||||
);
|
||||
|
||||
const NotificationsPushoverSchema = Yup.object().shape({
|
||||
|
||||
@@ -38,8 +38,6 @@ 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.',
|
||||
@@ -66,20 +64,6 @@ 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(
|
||||
@@ -136,8 +120,8 @@ const SettingsNetwork = () => {
|
||||
trustProxy: values.trustProxy,
|
||||
dnsCache: {
|
||||
enabled: values.dnsCacheEnabled,
|
||||
forceMinTtl: Number(values.dnsCacheForceMinTtl),
|
||||
forceMaxTtl: Number(values.dnsCacheForceMaxTtl),
|
||||
forceMinTtl: values.dnsCacheForceMinTtl,
|
||||
forceMaxTtl: values.dnsCacheForceMaxTtl,
|
||||
},
|
||||
proxy: {
|
||||
enabled: values.proxyEnabled,
|
||||
@@ -297,7 +281,7 @@ const SettingsNetwork = () => {
|
||||
<Field
|
||||
id="dnsCacheForceMinTtl"
|
||||
name="dnsCacheForceMinTtl"
|
||||
type="number"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.dnsCacheForceMinTtl &&
|
||||
@@ -321,7 +305,7 @@ const SettingsNetwork = () => {
|
||||
<Field
|
||||
id="dnsCacheForceMaxTtl"
|
||||
name="dnsCacheForceMaxTtl"
|
||||
type="number"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.dnsCacheForceMaxTtl &&
|
||||
@@ -391,7 +375,7 @@ const SettingsNetwork = () => {
|
||||
<Field
|
||||
id="proxyPort"
|
||||
name="proxyPort"
|
||||
type="number"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.proxyPort &&
|
||||
|
||||
@@ -139,11 +139,7 @@ const StatusBadge = ({
|
||||
<div
|
||||
className={`
|
||||
absolute top-0 left-0 z-10 flex h-full bg-opacity-80 ${
|
||||
status === MediaStatus.DELETED
|
||||
? 'bg-red-600'
|
||||
: status === MediaStatus.PROCESSING
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-green-500'
|
||||
status === MediaStatus.PROCESSING ? 'bg-indigo-500' : 'bg-green-500'
|
||||
} transition-all duration-200 ease-in-out
|
||||
`}
|
||||
style={{
|
||||
@@ -377,66 +373,11 @@ const StatusBadge = ({
|
||||
|
||||
case MediaStatus.DELETED:
|
||||
return (
|
||||
<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>
|
||||
<Tooltip content={mediaLinkDescription}>
|
||||
<Badge badgeType="danger">
|
||||
{intl.formatMessage(is4k ? messages.status4k : messages.status, {
|
||||
status: intl.formatMessage(globalMessages.deleted),
|
||||
})}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -629,6 +629,7 @@
|
||||
"components.Settings.Notifications.NotificationsNtfy.ntfysettingsfailed": "Ntfy notification settings failed to save.",
|
||||
"components.Settings.Notifications.NotificationsNtfy.ntfysettingssaved": "Ntfy notification settings saved successfully!",
|
||||
"components.Settings.Notifications.NotificationsNtfy.password": "Password",
|
||||
"components.Settings.Notifications.NotificationsNtfy.priority": "Priority",
|
||||
"components.Settings.Notifications.NotificationsNtfy.toastNtfyTestFailed": "Ntfy test notification failed to send.",
|
||||
"components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSending": "Sending ntfy test notification…",
|
||||
"components.Settings.Notifications.NotificationsNtfy.toastNtfyTestSuccess": "Ntfy test notification sent!",
|
||||
@@ -640,6 +641,7 @@
|
||||
"components.Settings.Notifications.NotificationsNtfy.usernamePasswordAuth": "Username + Password authentication",
|
||||
"components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "You must provide a topic",
|
||||
"components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "You must provide a valid URL",
|
||||
"components.Settings.Notifications.NotificationsNtfy.validationPriorityRequired": "You must provide a priority between 1 and 5",
|
||||
"components.Settings.Notifications.NotificationsNtfy.validationTypes": "You must select at least one notification type",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.accessToken": "Access Token",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "Create a token from your <PushbulletSettingsLink>Account Settings</PushbulletSettingsLink>",
|
||||
@@ -1022,8 +1024,6 @@
|
||||
"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",
|
||||
|
||||
Reference in New Issue
Block a user