Compare commits

..

1 Commits

Author SHA1 Message Date
Fallenbagel
ac5e2ba6c1 fix(sonarr): re-monitor episodes when re-requesting deleted but monitored seasons
When Sonarr's "Unmonitor Deleted Episodes" is enabled, deleted files cause episodes to be
unmonitored while the season stays monitored. Re-requesting sets season.monitored = true, but Sonarr
only cascades to episodes on state change. Since the season is already monitored, episodes stay
unmonitored and searches find nothing. Now explicitly re-monitors episodes for requested seasons
before triggering a search.

fix #2309
2026-01-19 01:33:38 +05:00
3 changed files with 41 additions and 150 deletions

View File

@@ -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

View File

@@ -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.`,
@@ -615,21 +611,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 +637,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 +700,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 +799,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'} ${

View File

@@ -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