Compare commits
3 Commits
preview-av
...
preview-pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b46a7d8804 | ||
|
|
844f86d41d | ||
|
|
d9aceee3f6 |
@@ -209,34 +209,6 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
series: newSeriesResponse.data,
|
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) {
|
if (options.searchNow) {
|
||||||
this.searchSeries(newSeriesResponse.data.id);
|
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(
|
private buildSeasonList(
|
||||||
seasons: number[],
|
seasons: number[],
|
||||||
existingSeasons?: SonarrSeason[]
|
existingSeasons?: SonarrSeason[]
|
||||||
|
|||||||
@@ -97,10 +97,7 @@ app
|
|||||||
|
|
||||||
// Register HTTP proxy
|
// Register HTTP proxy
|
||||||
if (settings.network.proxy.enabled) {
|
if (settings.network.proxy.enabled) {
|
||||||
await createCustomProxyAgent(
|
await createCustomProxyAgent(settings.network.proxy);
|
||||||
settings.network.proxy,
|
|
||||||
settings.network.forceIpv4First
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate library types
|
// Migrate library types
|
||||||
|
|||||||
@@ -143,9 +143,7 @@ class AvailabilitySync {
|
|||||||
const { existsInPlex: existsInPlex4k } =
|
const { existsInPlex: existsInPlex4k } =
|
||||||
await this.mediaExistsInPlex(media, true);
|
await this.mediaExistsInPlex(media, true);
|
||||||
|
|
||||||
// Media must exist in Plex to be considered available
|
if (existsInPlex || existsInRadarr) {
|
||||||
// If it exists in Radarr but not in Plex, it should be marked as deleted
|
|
||||||
if (existsInPlex) {
|
|
||||||
movieExists = true;
|
movieExists = true;
|
||||||
logger.info(
|
logger.info(
|
||||||
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
`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;
|
movieExists4k = true;
|
||||||
logger.info(
|
logger.info(
|
||||||
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||||
@@ -242,9 +240,7 @@ class AvailabilitySync {
|
|||||||
|
|
||||||
//plex
|
//plex
|
||||||
if (mediaServerType === MediaServerType.PLEX) {
|
if (mediaServerType === MediaServerType.PLEX) {
|
||||||
// Media must exist in Plex to be considered available
|
if (existsInPlex || existsInSonarr) {
|
||||||
// If it exists in Sonarr but not in Plex, it should be marked as deleted
|
|
||||||
if (existsInPlex) {
|
|
||||||
showExists = true;
|
showExists = true;
|
||||||
logger.info(
|
logger.info(
|
||||||
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||||
@@ -256,7 +252,7 @@ class AvailabilitySync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mediaServerType === MediaServerType.PLEX) {
|
if (mediaServerType === MediaServerType.PLEX) {
|
||||||
if (existsInPlex4k) {
|
if (existsInPlex4k || existsInSonarr4k) {
|
||||||
showExists4k = true;
|
showExists4k = true;
|
||||||
logger.info(
|
logger.info(
|
||||||
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
`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
|
// Sonarr finds that season, we will change the final seasons value
|
||||||
// to true.
|
// to true.
|
||||||
const filteredSeasonsMap: Map<number, boolean> = new Map();
|
const filteredSeasonsMap: Map<number, boolean> = new Map();
|
||||||
|
|
||||||
media.seasons
|
media.seasons
|
||||||
.filter(
|
.filter(
|
||||||
(season) =>
|
(season) =>
|
||||||
@@ -314,7 +311,48 @@ class AvailabilitySync {
|
|||||||
filteredSeasonsMap.set(season.seasonNumber, false)
|
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();
|
const filteredSeasonsMap4k: Map<number, boolean> = new Map();
|
||||||
|
|
||||||
media.seasons
|
media.seasons
|
||||||
.filter(
|
.filter(
|
||||||
(season) =>
|
(season) =>
|
||||||
@@ -325,32 +363,44 @@ class AvailabilitySync {
|
|||||||
filteredSeasonsMap4k.set(season.seasonNumber, false)
|
filteredSeasonsMap4k.set(season.seasonNumber, false)
|
||||||
);
|
);
|
||||||
|
|
||||||
let finalSeasons: Map<number, boolean>;
|
// 4k
|
||||||
let finalSeasons4k: Map<number, boolean>;
|
const finalSeasons4k: Map<number, boolean> = new Map();
|
||||||
|
|
||||||
if (mediaServerType === MediaServerType.PLEX) {
|
if (mediaServerType === MediaServerType.PLEX) {
|
||||||
finalSeasons = new Map([
|
plexSeasonsMap4k.forEach((value, key) => {
|
||||||
...filteredSeasonsMap,
|
finalSeasons4k.set(key, value);
|
||||||
...plexSeasonsMap,
|
});
|
||||||
...sonarrSeasonsMap,
|
|
||||||
]);
|
filteredSeasonsMap4k.forEach((value, key) => {
|
||||||
finalSeasons4k = new Map([
|
if (!finalSeasons4k.has(key)) {
|
||||||
...filteredSeasonsMap4k,
|
finalSeasons4k.set(key, value);
|
||||||
...plexSeasonsMap4k,
|
}
|
||||||
...sonarrSeasonsMap4k,
|
});
|
||||||
]);
|
|
||||||
} else {
|
sonarrSeasonsMap4k.forEach((value, key) => {
|
||||||
// Jellyfin/Emby
|
if (!finalSeasons4k.has(key)) {
|
||||||
finalSeasons = new Map([
|
finalSeasons4k.set(key, value);
|
||||||
...filteredSeasonsMap,
|
}
|
||||||
...jellyfinSeasonsMap,
|
});
|
||||||
...sonarrSeasonsMap,
|
} else if (
|
||||||
]);
|
mediaServerType === MediaServerType.JELLYFIN ||
|
||||||
finalSeasons4k = new Map([
|
mediaServerType === MediaServerType.EMBY
|
||||||
...filteredSeasonsMap4k,
|
) {
|
||||||
...jellyfinSeasonsMap4k,
|
jellyfinSeasonsMap4k.forEach((value, key) => {
|
||||||
...sonarrSeasonsMap4k,
|
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 (
|
if (
|
||||||
@@ -615,20 +665,15 @@ class AvailabilitySync {
|
|||||||
is4k: boolean
|
is4k: boolean
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
let existsInRadarr = false;
|
let existsInRadarr = false;
|
||||||
const externalServiceId = is4k
|
|
||||||
? media.externalServiceId4k
|
|
||||||
: media.externalServiceId;
|
|
||||||
|
|
||||||
if (!externalServiceId) {
|
if (is4k && media.status4k === MediaStatus.AVAILABLE) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Skipping Radarr check for ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
|
`Checking if 4K movie [TMDB ID ${media.tmdbId}] exists in Radarr`,
|
||||||
media.tmdbId
|
|
||||||
}] - no externalServiceId available`,
|
|
||||||
{
|
{
|
||||||
label: 'Availability Sync',
|
label: 'AvailabilitySync',
|
||||||
|
externalServiceId4k: media.externalServiceId4k,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for availability in all of the available radarr servers
|
// Check for availability in all of the available radarr servers
|
||||||
@@ -656,63 +701,22 @@ class AvailabilitySync {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (radarr) {
|
if (radarr && radarr.hasFile) {
|
||||||
if (radarr.hasFile) {
|
const resolution =
|
||||||
const resolution =
|
radarr?.movieFile?.mediaInfo?.resolution?.split('x');
|
||||||
radarr?.movieFile?.mediaInfo?.resolution?.split('x');
|
const is4kMovie =
|
||||||
const is4kMovie =
|
resolution?.length === 2 && Number(resolution[0]) >= 2000;
|
||||||
resolution?.length === 2 && Number(resolution[0]) >= 2000;
|
existsInRadarr = is4k ? is4kMovie : !is4kMovie;
|
||||||
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) {
|
} catch (ex) {
|
||||||
if (ex.message.includes('404')) {
|
if (!ex.message.includes('404')) {
|
||||||
logger.debug(
|
existsInRadarr = true;
|
||||||
`Movie [TMDB ID ${media.tmdbId}] not found in Radarr (404) - externalServiceId may be stale: ${externalServiceId}`,
|
|
||||||
{
|
|
||||||
label: 'Availability Sync',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
|
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
|
||||||
media.tmdbId
|
media.tmdbId
|
||||||
}] from Radarr.`,
|
}] from Radarr.`,
|
||||||
{
|
{
|
||||||
errorMessage: ex.message,
|
errorMessage: ex.message,
|
||||||
externalServiceId: externalServiceId,
|
|
||||||
label: 'Availability Sync',
|
label: 'Availability Sync',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -760,6 +764,7 @@ class AvailabilitySync {
|
|||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (!ex.message.includes('404')) {
|
if (!ex.message.includes('404')) {
|
||||||
|
existsInSonarr = true;
|
||||||
preventSeasonSearch = true;
|
preventSeasonSearch = true;
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} show [TMDB ID ${
|
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} show [TMDB ID ${
|
||||||
@@ -858,66 +863,57 @@ class AvailabilitySync {
|
|||||||
// We can use the cache we built when we fetched the series with mediaExistsInPlex
|
// We can use the cache we built when we fetched the series with mediaExistsInPlex
|
||||||
try {
|
try {
|
||||||
let plexMedia: PlexMetadata | undefined;
|
let plexMedia: PlexMetadata | undefined;
|
||||||
const currentRatingKey = is4k ? ratingKey4k : ratingKey;
|
|
||||||
|
|
||||||
if (!currentRatingKey) {
|
if (ratingKey && !is4k) {
|
||||||
logger.debug(
|
plexMedia = await this.plexClient?.getMetadata(ratingKey);
|
||||||
`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') {
|
if (media.mediaType === 'tv') {
|
||||||
this.plexSeasonsCache[ratingKey] =
|
this.plexSeasonsCache[ratingKey] =
|
||||||
await this.plexClient?.getChildrenMetadata(ratingKey);
|
await this.plexClient?.getChildrenMetadata(ratingKey);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ratingKey4k && is4k) {
|
if (ratingKey4k && is4k) {
|
||||||
plexMedia = await this.plexClient?.getMetadata(ratingKey4k);
|
plexMedia = await this.plexClient?.getMetadata(ratingKey4k);
|
||||||
|
|
||||||
if (media.mediaType === 'tv') {
|
if (media.mediaType === 'tv') {
|
||||||
this.plexSeasonsCache[ratingKey4k] =
|
this.plexSeasonsCache[ratingKey4k] =
|
||||||
await this.plexClient?.getChildrenMetadata(ratingKey4k);
|
await this.plexClient?.getChildrenMetadata(ratingKey4k);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plexMedia) {
|
if (plexMedia) {
|
||||||
existsInPlex = true;
|
if (media.mediaType === 'movie') {
|
||||||
logger.debug(
|
const has4kByWidth = plexMedia.Media?.some(
|
||||||
`Found ${is4k ? '4K' : 'non-4K'} ${
|
(mediaItem) => (mediaItem.width ?? 0) >= 2000
|
||||||
media.mediaType === 'tv' ? 'show' : 'movie'
|
);
|
||||||
} [TMDB ID ${media.tmdbId}] in Plex`,
|
|
||||||
{
|
if (is4k) {
|
||||||
ratingKey: is4k ? ratingKey4k : ratingKey,
|
if (ratingKey === ratingKey4k || !has4kByWidth) {
|
||||||
plexTitle: plexMedia.title,
|
plexMedia = undefined;
|
||||||
plexRatingKey: plexMedia.ratingKey,
|
}
|
||||||
plexGuid: plexMedia.guid,
|
} else {
|
||||||
label: 'Availability Sync',
|
const hasNon4kByWidth = plexMedia.Media?.some(
|
||||||
|
(mediaItem) =>
|
||||||
|
(mediaItem.width ?? 0) < 2000 && (mediaItem.width ?? 0) > 0
|
||||||
|
);
|
||||||
|
if (!hasNon4kByWidth && has4kByWidth) {
|
||||||
|
plexMedia = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
} else if (media.mediaType === 'tv' && is4k) {
|
||||||
|
if (ratingKey === ratingKey4k) {
|
||||||
|
plexMedia = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (plexMedia) {
|
||||||
|
existsInPlex = true;
|
||||||
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (ex.message.includes('404')) {
|
if (!ex.message.includes('404')) {
|
||||||
logger.debug(
|
existsInPlex = true;
|
||||||
`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;
|
preventSeasonSearch = true;
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${
|
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${
|
||||||
@@ -1033,8 +1029,8 @@ class AvailabilitySync {
|
|||||||
existsInJellyfin = true;
|
existsInJellyfin = true;
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (!ex.message.includes('404') && !ex.message.includes('500')) {
|
if (!ex.message.includes('404' || '500')) {
|
||||||
existsInJellyfin = true;
|
existsInJellyfin = false;
|
||||||
preventSeasonSearch = true;
|
preventSeasonSearch = true;
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${
|
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${
|
||||||
|
|||||||
@@ -45,17 +45,7 @@ class PushoverAgent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public shouldSend(): boolean {
|
public shouldSend(): boolean {
|
||||||
const settings = this.getSettings();
|
return true;
|
||||||
|
|
||||||
if (
|
|
||||||
settings.enabled &&
|
|
||||||
settings.options.accessToken &&
|
|
||||||
settings.options.userToken
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getImagePayload(
|
private async getImagePayload(
|
||||||
|
|||||||
@@ -115,11 +115,9 @@ class BaseScanner<T> {
|
|||||||
let changedExisting = false;
|
let changedExisting = false;
|
||||||
|
|
||||||
if (existing[is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE) {
|
if (existing[is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE) {
|
||||||
existing[is4k ? 'status4k' : 'status'] = !processing
|
existing[is4k ? 'status4k' : 'status'] = processing
|
||||||
? MediaStatus.AVAILABLE
|
? MediaStatus.PROCESSING
|
||||||
: existing[is4k ? 'status4k' : 'status'] === MediaStatus.DELETED
|
: MediaStatus.AVAILABLE;
|
||||||
? MediaStatus.DELETED
|
|
||||||
: MediaStatus.PROCESSING;
|
|
||||||
if (mediaAddedAt) {
|
if (mediaAddedAt) {
|
||||||
existing.mediaAddedAt = mediaAddedAt;
|
existing.mediaAddedAt = mediaAddedAt;
|
||||||
}
|
}
|
||||||
@@ -332,11 +330,6 @@ class BaseScanner<T> {
|
|||||||
season.processing &&
|
season.processing &&
|
||||||
existingSeason.status !== MediaStatus.DELETED
|
existingSeason.status !== MediaStatus.DELETED
|
||||||
? MediaStatus.PROCESSING
|
? MediaStatus.PROCESSING
|
||||||
: !season.is4kOverride &&
|
|
||||||
!season.processing &&
|
|
||||||
season.episodes === 0 &&
|
|
||||||
existingSeason.status === MediaStatus.PROCESSING
|
|
||||||
? MediaStatus.UNKNOWN
|
|
||||||
: existingSeason.status;
|
: existingSeason.status;
|
||||||
|
|
||||||
// Same thing here, except we only do updates if 4k is enabled
|
// Same thing here, except we only do updates if 4k is enabled
|
||||||
@@ -352,11 +345,6 @@ class BaseScanner<T> {
|
|||||||
season.processing &&
|
season.processing &&
|
||||||
existingSeason.status4k !== MediaStatus.DELETED
|
existingSeason.status4k !== MediaStatus.DELETED
|
||||||
? MediaStatus.PROCESSING
|
? MediaStatus.PROCESSING
|
||||||
: season.is4kOverride &&
|
|
||||||
!season.processing &&
|
|
||||||
season.episodes4k === 0 &&
|
|
||||||
existingSeason.status4k === MediaStatus.PROCESSING
|
|
||||||
? MediaStatus.UNKNOWN
|
|
||||||
: existingSeason.status4k;
|
: existingSeason.status4k;
|
||||||
} else {
|
} else {
|
||||||
newSeasons.push(
|
newSeasons.push(
|
||||||
|
|||||||
@@ -11,14 +11,9 @@ export let requestInterceptorFunction: (
|
|||||||
) => InternalAxiosRequestConfig;
|
) => InternalAxiosRequestConfig;
|
||||||
|
|
||||||
export default async function createCustomProxyAgent(
|
export default async function createCustomProxyAgent(
|
||||||
proxySettings: ProxySettings,
|
proxySettings: ProxySettings
|
||||||
forceIpv4First?: boolean
|
|
||||||
) {
|
) {
|
||||||
const defaultAgent = new Agent({
|
const defaultAgent = new Agent({ keepAliveTimeout: 5000 });
|
||||||
keepAliveTimeout: 5000,
|
|
||||||
connections: 50,
|
|
||||||
connect: forceIpv4First ? { family: 4 } : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const skipUrl = (url: string | URL) => {
|
const skipUrl = (url: string | URL) => {
|
||||||
const hostname =
|
const hostname =
|
||||||
@@ -72,23 +67,16 @@ export default async function createCustomProxyAgent(
|
|||||||
uri: proxyUrl,
|
uri: proxyUrl,
|
||||||
token,
|
token,
|
||||||
keepAliveTimeout: 5000,
|
keepAliveTimeout: 5000,
|
||||||
connections: 50,
|
|
||||||
connect: forceIpv4First ? { family: 4 } : undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setGlobalDispatcher(proxyAgent.compose(noProxyInterceptor));
|
setGlobalDispatcher(proxyAgent.compose(noProxyInterceptor));
|
||||||
|
|
||||||
const agentOptions = {
|
axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl, {
|
||||||
headers: token ? { 'proxy-authorization': token } : undefined,
|
headers: token ? { 'proxy-authorization': token } : undefined,
|
||||||
keepAlive: true,
|
});
|
||||||
maxSockets: 50,
|
axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl, {
|
||||||
maxFreeSockets: 10,
|
headers: token ? { 'proxy-authorization': token } : undefined,
|
||||||
timeout: 5000,
|
});
|
||||||
scheduling: 'lifo' as const,
|
|
||||||
family: forceIpv4First ? 4 : undefined,
|
|
||||||
};
|
|
||||||
axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl, agentOptions);
|
|
||||||
axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl, agentOptions);
|
|
||||||
|
|
||||||
requestInterceptorFunction = (config) => {
|
requestInterceptorFunction = (config) => {
|
||||||
const url = config.baseURL
|
const url = config.baseURL
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Tooltip from '@app/components/Common/Tooltip';
|
|||||||
import RequestModal from '@app/components/RequestModal';
|
import RequestModal from '@app/components/RequestModal';
|
||||||
import StatusBadge from '@app/components/StatusBadge';
|
import StatusBadge from '@app/components/StatusBadge';
|
||||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||||
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import { Permission, useUser } from '@app/hooks/useUser';
|
import { Permission, useUser } from '@app/hooks/useUser';
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
@@ -218,6 +219,7 @@ interface RequestCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||||
|
const settings = useSettings();
|
||||||
const { ref, inView } = useInView({
|
const { ref, inView } = useInView({
|
||||||
triggerOnce: true,
|
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">
|
<div className="my-0.5 hidden items-center text-sm sm:my-1 sm:flex">
|
||||||
<span className="mr-2 font-bold ">
|
<span className="mr-2 font-bold ">
|
||||||
{intl.formatMessage(messages.seasons, {
|
{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>
|
</span>
|
||||||
<div className="hide-scrollbar overflow-x-scroll">
|
<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 RequestModal from '@app/components/RequestModal';
|
||||||
import StatusBadge from '@app/components/StatusBadge';
|
import StatusBadge from '@app/components/StatusBadge';
|
||||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||||
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import { Permission, useUser } from '@app/hooks/useUser';
|
import { Permission, useUser } from '@app/hooks/useUser';
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
@@ -294,6 +295,7 @@ interface RequestItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||||
|
const settings = useSettings();
|
||||||
const { ref, inView } = useInView({
|
const { ref, inView } = useInView({
|
||||||
triggerOnce: true,
|
triggerOnce: true,
|
||||||
});
|
});
|
||||||
@@ -468,7 +470,14 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
<div className="card-field">
|
<div className="card-field">
|
||||||
<span className="card-field-name">
|
<span className="card-field-name">
|
||||||
{intl.formatMessage(messages.seasons, {
|
{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>
|
</span>
|
||||||
<div className="hide-scrollbar flex flex-nowrap overflow-x-scroll">
|
<div className="hide-scrollbar flex flex-nowrap overflow-x-scroll">
|
||||||
|
|||||||
@@ -49,12 +49,7 @@ const NotificationsPushover = () => {
|
|||||||
const { data: soundsData } = useSWR<PushoverSound[]>(
|
const { data: soundsData } = useSWR<PushoverSound[]>(
|
||||||
data?.options.accessToken
|
data?.options.accessToken
|
||||||
? `/api/v1/settings/notifications/pushover/sounds?token=${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({
|
const NotificationsPushoverSchema = Yup.object().shape({
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
|
|||||||
proxyBypassFilterTip:
|
proxyBypassFilterTip:
|
||||||
"Use ',' as a separator, and '*.' as a wildcard for subdomains",
|
"Use ',' as a separator, and '*.' as a wildcard for subdomains",
|
||||||
proxyBypassLocalAddresses: 'Bypass Proxy for Local Addresses',
|
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',
|
validationProxyPort: 'You must provide a valid port',
|
||||||
networkDisclaimer:
|
networkDisclaimer:
|
||||||
'Network parameters from your container/system should be used instead of these settings. See the {docs} for more information.',
|
'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');
|
} = useSWR<NetworkSettings>('/api/v1/settings/network');
|
||||||
|
|
||||||
const NetworkSettingsSchema = Yup.object().shape({
|
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', {
|
proxyPort: Yup.number().when('proxyEnabled', {
|
||||||
is: (proxyEnabled: boolean) => proxyEnabled,
|
is: (proxyEnabled: boolean) => proxyEnabled,
|
||||||
then: Yup.number().required(
|
then: Yup.number().required(
|
||||||
@@ -136,8 +120,8 @@ const SettingsNetwork = () => {
|
|||||||
trustProxy: values.trustProxy,
|
trustProxy: values.trustProxy,
|
||||||
dnsCache: {
|
dnsCache: {
|
||||||
enabled: values.dnsCacheEnabled,
|
enabled: values.dnsCacheEnabled,
|
||||||
forceMinTtl: Number(values.dnsCacheForceMinTtl),
|
forceMinTtl: values.dnsCacheForceMinTtl,
|
||||||
forceMaxTtl: Number(values.dnsCacheForceMaxTtl),
|
forceMaxTtl: values.dnsCacheForceMaxTtl,
|
||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
enabled: values.proxyEnabled,
|
enabled: values.proxyEnabled,
|
||||||
@@ -297,7 +281,7 @@ const SettingsNetwork = () => {
|
|||||||
<Field
|
<Field
|
||||||
id="dnsCacheForceMinTtl"
|
id="dnsCacheForceMinTtl"
|
||||||
name="dnsCacheForceMinTtl"
|
name="dnsCacheForceMinTtl"
|
||||||
type="number"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.dnsCacheForceMinTtl &&
|
{errors.dnsCacheForceMinTtl &&
|
||||||
@@ -321,7 +305,7 @@ const SettingsNetwork = () => {
|
|||||||
<Field
|
<Field
|
||||||
id="dnsCacheForceMaxTtl"
|
id="dnsCacheForceMaxTtl"
|
||||||
name="dnsCacheForceMaxTtl"
|
name="dnsCacheForceMaxTtl"
|
||||||
type="number"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.dnsCacheForceMaxTtl &&
|
{errors.dnsCacheForceMaxTtl &&
|
||||||
@@ -391,7 +375,7 @@ const SettingsNetwork = () => {
|
|||||||
<Field
|
<Field
|
||||||
id="proxyPort"
|
id="proxyPort"
|
||||||
name="proxyPort"
|
name="proxyPort"
|
||||||
type="number"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.proxyPort &&
|
{errors.proxyPort &&
|
||||||
|
|||||||
@@ -139,11 +139,7 @@ const StatusBadge = ({
|
|||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
absolute top-0 left-0 z-10 flex h-full bg-opacity-80 ${
|
absolute top-0 left-0 z-10 flex h-full bg-opacity-80 ${
|
||||||
status === MediaStatus.DELETED
|
status === MediaStatus.PROCESSING ? 'bg-indigo-500' : 'bg-green-500'
|
||||||
? 'bg-red-600'
|
|
||||||
: status === MediaStatus.PROCESSING
|
|
||||||
? 'bg-indigo-500'
|
|
||||||
: 'bg-green-500'
|
|
||||||
} transition-all duration-200 ease-in-out
|
} transition-all duration-200 ease-in-out
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
@@ -377,66 +373,11 @@ const StatusBadge = ({
|
|||||||
|
|
||||||
case MediaStatus.DELETED:
|
case MediaStatus.DELETED:
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip content={mediaLinkDescription}>
|
||||||
content={inProgress ? tooltipContent : mediaLinkDescription}
|
<Badge badgeType="danger">
|
||||||
className={`${
|
{intl.formatMessage(is4k ? messages.status4k : messages.status, {
|
||||||
inProgress && 'hidden max-h-96 w-96 overflow-y-auto sm:block'
|
status: intl.formatMessage(globalMessages.deleted),
|
||||||
}`}
|
})}
|
||||||
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>
|
</Badge>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1022,8 +1022,6 @@
|
|||||||
"components.Settings.SettingsNetwork.toastSettingsSuccess": "Settings saved successfully!",
|
"components.Settings.SettingsNetwork.toastSettingsSuccess": "Settings saved successfully!",
|
||||||
"components.Settings.SettingsNetwork.trustProxy": "Enable Proxy Support",
|
"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.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.SettingsNetwork.validationProxyPort": "You must provide a valid port",
|
||||||
"components.Settings.SettingsUsers.atLeastOneAuth": "At least one authentication method must be selected.",
|
"components.Settings.SettingsUsers.atLeastOneAuth": "At least one authentication method must be selected.",
|
||||||
"components.Settings.SettingsUsers.defaultPermissions": "Default Permissions",
|
"components.Settings.SettingsUsers.defaultPermissions": "Default Permissions",
|
||||||
|
|||||||
Reference in New Issue
Block a user