Skip to content
This repository was archived by the owner on Feb 15, 2026. It is now read-only.

Commit d0836ce

Browse files
authored
fix: improved handling of edge case that could cause availability sync to fail (#3497)
1 parent 2c3f533 commit d0836ce

File tree

1 file changed

+165
-138
lines changed

1 file changed

+165
-138
lines changed

server/lib/availabilitySync.ts

Lines changed: 165 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,24 @@ class AvailabilitySync {
3030
this.sonarrSeasonsCache = {};
3131
this.radarrServers = settings.radarr.filter((server) => server.syncEnabled);
3232
this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled);
33-
await this.initPlexClient();
3433

35-
if (!this.plexClient) {
36-
return;
37-
}
34+
try {
35+
await this.initPlexClient();
3836

39-
logger.info(`Starting availability sync...`, {
40-
label: 'AvailabilitySync',
41-
});
42-
const mediaRepository = getRepository(Media);
43-
const requestRepository = getRepository(MediaRequest);
44-
const seasonRepository = getRepository(Season);
45-
const seasonRequestRepository = getRepository(SeasonRequest);
37+
if (!this.plexClient) {
38+
return;
39+
}
4640

47-
const pageSize = 50;
41+
logger.info(`Starting availability sync...`, {
42+
label: 'AvailabilitySync',
43+
});
44+
const mediaRepository = getRepository(Media);
45+
const requestRepository = getRepository(MediaRequest);
46+
const seasonRepository = getRepository(Season);
47+
const seasonRequestRepository = getRepository(SeasonRequest);
48+
49+
const pageSize = 50;
4850

49-
try {
5051
for await (const media of this.loadAvailableMediaPaginated(pageSize)) {
5152
if (!this.running) {
5253
throw new Error('Job aborted');
@@ -239,51 +240,60 @@ class AvailabilitySync {
239240

240241
const isTVType = media.mediaType === 'tv';
241242

242-
const request = await requestRepository.findOne({
243-
relations: {
244-
media: true,
245-
},
246-
where: { media: { id: media.id }, is4k: is4k ? true : false },
247-
});
248-
249-
logger.info(
250-
`Media ID ${media.id} does not exist in your ${is4k ? '4k' : 'non-4k'} ${
251-
isTVType ? 'Sonarr' : 'Radarr'
252-
} and Plex instance. Status will be changed to unknown.`,
253-
{ label: 'AvailabilitySync' }
254-
);
255-
256-
await mediaRepository.update(
257-
media.id,
258-
is4k
259-
? {
260-
status4k: MediaStatus.UNKNOWN,
261-
serviceId4k: null,
262-
externalServiceId4k: null,
263-
externalServiceSlug4k: null,
264-
ratingKey4k: null,
265-
}
266-
: {
267-
status: MediaStatus.UNKNOWN,
268-
serviceId: null,
269-
externalServiceId: null,
270-
externalServiceSlug: null,
271-
ratingKey: null,
272-
}
273-
);
243+
try {
244+
const request = await requestRepository.findOne({
245+
relations: {
246+
media: true,
247+
},
248+
where: { media: { id: media.id }, is4k: is4k ? true : false },
249+
});
274250

275-
if (isTVType) {
276-
const seasonRepository = getRepository(Season);
251+
logger.info(
252+
`Media ID ${media.id} does not exist in your ${
253+
is4k ? '4k' : 'non-4k'
254+
} ${
255+
isTVType ? 'Sonarr' : 'Radarr'
256+
} and Plex instance. Status will be changed to unknown.`,
257+
{ label: 'AvailabilitySync' }
258+
);
277259

278-
await seasonRepository?.update(
279-
{ media: { id: media.id } },
260+
await mediaRepository.update(
261+
media.id,
280262
is4k
281-
? { status4k: MediaStatus.UNKNOWN }
282-
: { status: MediaStatus.UNKNOWN }
263+
? {
264+
status4k: MediaStatus.UNKNOWN,
265+
serviceId4k: null,
266+
externalServiceId4k: null,
267+
externalServiceSlug4k: null,
268+
ratingKey4k: null,
269+
}
270+
: {
271+
status: MediaStatus.UNKNOWN,
272+
serviceId: null,
273+
externalServiceId: null,
274+
externalServiceSlug: null,
275+
ratingKey: null,
276+
}
283277
);
284-
}
285278

286-
await requestRepository.delete({ id: request?.id });
279+
if (isTVType) {
280+
const seasonRepository = getRepository(Season);
281+
282+
await seasonRepository?.update(
283+
{ media: { id: media.id } },
284+
is4k
285+
? { status4k: MediaStatus.UNKNOWN }
286+
: { status: MediaStatus.UNKNOWN }
287+
);
288+
}
289+
290+
await requestRepository.delete({ id: request?.id });
291+
} catch (ex) {
292+
logger.debug(`Failure updating media ID ${media.id}`, {
293+
errorMessage: ex.message,
294+
label: 'AvailabilitySync',
295+
});
296+
}
287297
}
288298

289299
private async mediaExistsInRadarr(
@@ -539,83 +549,90 @@ class AvailabilitySync {
539549
}
540550
}
541551

542-
const seasonToBeDeleted = await seasonRequestRepository.findOne({
543-
relations: {
544-
request: {
545-
media: true,
552+
try {
553+
const seasonToBeDeleted = await seasonRequestRepository.findOne({
554+
relations: {
555+
request: {
556+
media: true,
557+
},
546558
},
547-
},
548-
where: {
549-
request: {
550-
is4k: seasonExistsInSonarr ? true : false,
551-
media: {
552-
id: media.id,
559+
where: {
560+
request: {
561+
is4k: seasonExistsInSonarr ? true : false,
562+
media: {
563+
id: media.id,
564+
},
553565
},
566+
seasonNumber: season.seasonNumber,
554567
},
555-
seasonNumber: season.seasonNumber,
556-
},
557-
});
558-
559-
// If season does not exist, we will change status to unknown and delete related season request
560-
// If parent media request is empty(all related seasons have been removed), parent is automatically deleted
561-
if (
562-
!seasonExistsInSonarr &&
563-
(seasonExistsInSonarr4k || seasonExistsInPlex4k) &&
564-
!seasonExistsInPlex
565-
) {
566-
if (season.status !== MediaStatus.UNKNOWN) {
567-
logger.info(
568-
`Season ${season.seasonNumber}, media ID ${media.id} does not exist in your non-4k Sonarr and Plex instance. Status will be changed to unknown.`,
569-
{ label: 'AvailabilitySync' }
570-
);
571-
await seasonRepository.update(season.id, {
572-
status: MediaStatus.UNKNOWN,
573-
});
574-
575-
if (seasonToBeDeleted) {
576-
await seasonRequestRepository.remove(seasonToBeDeleted);
577-
}
568+
});
578569

579-
if (media.status === MediaStatus.AVAILABLE) {
570+
// If season does not exist, we will change status to unknown and delete related season request
571+
// If parent media request is empty(all related seasons have been removed), parent is automatically deleted
572+
if (
573+
!seasonExistsInSonarr &&
574+
(seasonExistsInSonarr4k || seasonExistsInPlex4k) &&
575+
!seasonExistsInPlex
576+
) {
577+
if (season.status !== MediaStatus.UNKNOWN) {
580578
logger.info(
581-
`Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
579+
`Season ${season.seasonNumber}, media ID ${media.id} does not exist in your non-4k Sonarr and Plex instance. Status will be changed to unknown.`,
582580
{ label: 'AvailabilitySync' }
583581
);
584-
await mediaRepository.update(media.id, {
585-
status: MediaStatus.PARTIALLY_AVAILABLE,
582+
await seasonRepository.update(season.id, {
583+
status: MediaStatus.UNKNOWN,
586584
});
587-
}
588-
}
589-
}
590585

591-
if (
592-
(seasonExistsInSonarr || seasonExistsInPlex) &&
593-
!seasonExistsInSonarr4k &&
594-
!seasonExistsInPlex4k
595-
) {
596-
if (season.status4k !== MediaStatus.UNKNOWN) {
597-
logger.info(
598-
`Season ${season.seasonNumber}, media ID ${media.id} does not exist in your 4k Sonarr and Plex instance. Status will be changed to unknown.`,
599-
{ label: 'AvailabilitySync' }
600-
);
601-
await seasonRepository.update(season.id, {
602-
status4k: MediaStatus.UNKNOWN,
603-
});
586+
if (seasonToBeDeleted) {
587+
await seasonRequestRepository.remove(seasonToBeDeleted);
588+
}
604589

605-
if (seasonToBeDeleted) {
606-
await seasonRequestRepository.remove(seasonToBeDeleted);
590+
if (media.status === MediaStatus.AVAILABLE) {
591+
logger.info(
592+
`Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
593+
{ label: 'AvailabilitySync' }
594+
);
595+
await mediaRepository.update(media.id, {
596+
status: MediaStatus.PARTIALLY_AVAILABLE,
597+
});
598+
}
607599
}
600+
}
608601

609-
if (media.status4k === MediaStatus.AVAILABLE) {
602+
if (
603+
(seasonExistsInSonarr || seasonExistsInPlex) &&
604+
!seasonExistsInSonarr4k &&
605+
!seasonExistsInPlex4k
606+
) {
607+
if (season.status4k !== MediaStatus.UNKNOWN) {
610608
logger.info(
611-
`Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
609+
`Season ${season.seasonNumber}, media ID ${media.id} does not exist in your 4k Sonarr and Plex instance. Status will be changed to unknown.`,
612610
{ label: 'AvailabilitySync' }
613611
);
614-
await mediaRepository.update(media.id, {
615-
status4k: MediaStatus.PARTIALLY_AVAILABLE,
612+
await seasonRepository.update(season.id, {
613+
status4k: MediaStatus.UNKNOWN,
616614
});
615+
616+
if (seasonToBeDeleted) {
617+
await seasonRequestRepository.remove(seasonToBeDeleted);
618+
}
619+
620+
if (media.status4k === MediaStatus.AVAILABLE) {
621+
logger.info(
622+
`Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
623+
{ label: 'AvailabilitySync' }
624+
);
625+
await mediaRepository.update(media.id, {
626+
status4k: MediaStatus.PARTIALLY_AVAILABLE,
627+
});
628+
}
617629
}
618630
}
631+
} catch (ex) {
632+
logger.debug(`Failure updating media ID ${media.id}`, {
633+
errorMessage: ex.message,
634+
label: 'AvailabilitySync',
635+
});
619636
}
620637

621638
if (
@@ -654,7 +671,10 @@ class AvailabilitySync {
654671
}
655672
} catch (ex) {
656673
if (!ex.message.includes('response code: 404')) {
657-
throw ex;
674+
logger.debug(`Failed to retrieve plex metadata`, {
675+
errorMessage: ex.message,
676+
label: 'AvailabilitySync',
677+
});
658678
}
659679
}
660680
// Base case if both media versions exist in plex
@@ -714,36 +734,43 @@ class AvailabilitySync {
714734
let seasonExistsInPlex = false;
715735
let seasonExistsInPlex4k = false;
716736

717-
if (ratingKey) {
718-
const children =
719-
this.plexSeasonsCache[ratingKey] ??
720-
(await this.plexClient?.getChildrenMetadata(ratingKey)) ??
721-
[];
722-
this.plexSeasonsCache[ratingKey] = children;
723-
const seasonMeta = children?.find(
724-
(child) => child.index === season.seasonNumber
725-
);
737+
try {
738+
if (ratingKey) {
739+
const children =
740+
this.plexSeasonsCache[ratingKey] ??
741+
(await this.plexClient?.getChildrenMetadata(ratingKey)) ??
742+
[];
743+
this.plexSeasonsCache[ratingKey] = children;
744+
const seasonMeta = children?.find(
745+
(child) => child.index === season.seasonNumber
746+
);
726747

727-
if (seasonMeta) {
728-
seasonExistsInPlex = true;
748+
if (seasonMeta) {
749+
seasonExistsInPlex = true;
750+
}
729751
}
730-
}
731-
732-
if (ratingKey4k) {
733-
const children4k =
734-
this.plexSeasonsCache[ratingKey4k] ??
735-
(await this.plexClient?.getChildrenMetadata(ratingKey4k)) ??
736-
[];
737-
this.plexSeasonsCache[ratingKey4k] = children4k;
738-
const seasonMeta4k = children4k?.find(
739-
(child) => child.index === season.seasonNumber
740-
);
752+
if (ratingKey4k) {
753+
const children4k =
754+
this.plexSeasonsCache[ratingKey4k] ??
755+
(await this.plexClient?.getChildrenMetadata(ratingKey4k)) ??
756+
[];
757+
this.plexSeasonsCache[ratingKey4k] = children4k;
758+
const seasonMeta4k = children4k?.find(
759+
(child) => child.index === season.seasonNumber
760+
);
741761

742-
if (seasonMeta4k) {
743-
seasonExistsInPlex4k = true;
762+
if (seasonMeta4k) {
763+
seasonExistsInPlex4k = true;
764+
}
765+
}
766+
} catch (ex) {
767+
if (!ex.message.includes('response code: 404')) {
768+
logger.debug(`Failed to retrieve plex's children metadata`, {
769+
errorMessage: ex.message,
770+
label: 'AvailabilitySync',
771+
});
744772
}
745773
}
746-
747774
// Base case if both season versions exist in plex
748775
if (seasonExistsInPlex && seasonExistsInPlex4k) {
749776
return true;

0 commit comments

Comments
 (0)