Skip to content

Commit bb3fe2c

Browse files
committed
fix: update tv show status to partially available when seasons are missing
1 parent a8f147d commit bb3fe2c

6 files changed

Lines changed: 552 additions & 73 deletions

File tree

next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import "./.next/types/routes.d.ts";
3+
import "./.next/dev/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

server/lib/availabilitySync.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,106 @@ describe('AvailabilitySync', () => {
688688
'Show should remain AVAILABLE when getEpisodes fails'
689689
);
690690
});
691+
692+
it('should mark show as PARTIALLY_AVAILABLE when some seasons are available and some are unknown', async () => {
693+
configureJellyfin();
694+
configureSonarr([{ syncEnabled: true }]);
695+
696+
const mediaRepository = getRepository(Media);
697+
698+
const media = new Media();
699+
media.tmdbId = 1412;
700+
media.mediaType = MediaType.TV;
701+
media.status = MediaStatus.AVAILABLE;
702+
media.jellyfinMediaId = 'jellyfin-partial-id';
703+
media.externalServiceId = 103;
704+
media.seasons = [
705+
new Season({
706+
seasonNumber: 1,
707+
status: MediaStatus.AVAILABLE,
708+
status4k: MediaStatus.UNKNOWN,
709+
}),
710+
new Season({
711+
seasonNumber: 2,
712+
status: MediaStatus.AVAILABLE,
713+
status4k: MediaStatus.UNKNOWN,
714+
}),
715+
new Season({
716+
seasonNumber: 3,
717+
status: MediaStatus.UNKNOWN,
718+
status4k: MediaStatus.UNKNOWN,
719+
}),
720+
new Season({
721+
seasonNumber: 4,
722+
status: MediaStatus.UNKNOWN,
723+
status4k: MediaStatus.UNKNOWN,
724+
}),
725+
];
726+
727+
await mediaRepository.save(media);
728+
729+
getItemDataImpl = async (id: string) => {
730+
if (id === 'jellyfin-partial-id') {
731+
return fakeJellyfinShow('jellyfin-partial-id', '1412');
732+
}
733+
return undefined;
734+
};
735+
736+
getSeasonsImpl = async (seriesID: string) => {
737+
if (seriesID === 'jellyfin-partial-id') {
738+
return [
739+
fakeJellyfinSeason(1, 'jellyfin-partial-s1-id'),
740+
fakeJellyfinSeason(2, 'jellyfin-partial-s2-id'),
741+
];
742+
}
743+
return [];
744+
};
745+
746+
getEpisodesImpl = async (_seriesID: string, seasonID: string) => {
747+
if (seasonID === 'jellyfin-partial-s1-id') {
748+
return fakeJellyfinEpisodes(10);
749+
}
750+
if (seasonID === 'jellyfin-partial-s2-id') {
751+
return fakeJellyfinEpisodes(10);
752+
}
753+
return [];
754+
};
755+
756+
getSeriesByIdImpl = async (id: number) => {
757+
if (id === 103) {
758+
return {
759+
tvdbId: 99997,
760+
id: 103,
761+
title: 'Partial Show',
762+
titleSlug: 'partial-show',
763+
monitored: true,
764+
statistics: {
765+
episodeFileCount: 20,
766+
totalEpisodeCount: 40,
767+
episodeCount: 40,
768+
percentOfEpisodes: 50,
769+
sizeOnDisk: 0,
770+
seasonCount: 4,
771+
},
772+
seasons: fakeSonarrSeasons(4, { 1: 10, 2: 10 }),
773+
} as unknown as SonarrSeries;
774+
}
775+
throw new Error('404');
776+
};
777+
778+
await availabilitySync.run();
779+
780+
const updated = await mediaRepository.findOneOrFail({
781+
where: { tmdbId: 1412 },
782+
relations: ['seasons'],
783+
});
784+
785+
assert.strictEqual(
786+
updated.status,
787+
MediaStatus.PARTIALLY_AVAILABLE,
788+
'Show should be PARTIALLY_AVAILABLE when some seasons are available and some are unknown'
789+
);
790+
});
691791
});
692792

693793
describe('TV season availability - Plex', () => {

server/lib/availabilitySync.ts

Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import PlexAPI from '@server/api/plexapi';
55
import RadarrAPI, { type RadarrMovie } from '@server/api/servarr/radarr';
66
import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr';
77
import SonarrAPI from '@server/api/servarr/sonarr';
8+
import TheMovieDb from '@server/api/themoviedb';
9+
import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces';
810
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
911
import { MediaServerType } from '@server/constants/server';
1012
import { getRepository } from '@server/datasource';
@@ -31,6 +33,8 @@ class AvailabilitySync {
3133
private radarrServers: RadarrSettings[];
3234
private sonarrServers: SonarrSettings[];
3335

36+
readonly tmdb = new TheMovieDb();
37+
3438
async run() {
3539
const settings = getSettings();
3640
const mediaServerType = getSettings().main.mediaServerType;
@@ -45,7 +49,7 @@ class AvailabilitySync {
4549

4650
try {
4751
logger.info(`Starting availability sync...`, {
48-
label: 'Availability Sync',
52+
label: 'AvailabilitySync',
4953
});
5054
const pageSize = 50;
5155

@@ -149,7 +153,7 @@ class AvailabilitySync {
149153

150154
if (existsInPlex || existsInRadarr) {
151155
movieExists = true;
152-
logger.info(
156+
logger.debug(
153157
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
154158
{
155159
label: 'AvailabilitySync',
@@ -159,7 +163,7 @@ class AvailabilitySync {
159163

160164
if (existsInPlex4k || existsInRadarr4k) {
161165
movieExists4k = true;
162-
logger.info(
166+
logger.debug(
163167
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
164168
{
165169
label: 'AvailabilitySync',
@@ -182,7 +186,7 @@ class AvailabilitySync {
182186

183187
if (existsInJellyfin || existsInRadarr) {
184188
movieExists = true;
185-
logger.info(
189+
logger.debug(
186190
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
187191
{
188192
label: 'AvailabilitySync',
@@ -192,7 +196,7 @@ class AvailabilitySync {
192196

193197
if (existsInJellyfin4k || existsInRadarr4k) {
194198
movieExists4k = true;
195-
logger.info(
199+
logger.debug(
196200
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
197201
{
198202
label: 'AvailabilitySync',
@@ -246,7 +250,7 @@ class AvailabilitySync {
246250
if (mediaServerType === MediaServerType.PLEX) {
247251
if (existsInPlex || existsInSonarr) {
248252
showExists = true;
249-
logger.info(
253+
logger.debug(
250254
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
251255
{
252256
label: 'AvailabilitySync',
@@ -258,7 +262,7 @@ class AvailabilitySync {
258262
if (mediaServerType === MediaServerType.PLEX) {
259263
if (existsInPlex4k || existsInSonarr4k) {
260264
showExists4k = true;
261-
logger.info(
265+
logger.debug(
262266
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
263267
{
264268
label: 'AvailabilitySync',
@@ -274,7 +278,7 @@ class AvailabilitySync {
274278
) {
275279
if (existsInJellyfin || existsInSonarr) {
276280
showExists = true;
277-
logger.info(
281+
logger.debug(
278282
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
279283
{
280284
label: 'AvailabilitySync',
@@ -289,7 +293,7 @@ class AvailabilitySync {
289293
) {
290294
if (existsInJellyfin4k || existsInSonarr4k) {
291295
showExists4k = true;
292-
logger.info(
296+
logger.debug(
293297
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
294298
{
295299
label: 'AvailabilitySync',
@@ -353,6 +357,40 @@ class AvailabilitySync {
353357
]);
354358
}
355359

360+
// We need to fetch from TMDB to get the episode count for each season
361+
let tvShow: TmdbTvDetails;
362+
if (media.tmdbId) {
363+
tvShow = await this.tmdb.getTvShow({
364+
tvId: Number(media.tmdbId),
365+
});
366+
} else if (media.tvdbId) {
367+
tvShow = await this.tmdb.getShowByTvdbId({
368+
tvdbId: Number(media.tvdbId),
369+
});
370+
} else {
371+
throw new Error('No ID provided');
372+
}
373+
374+
// fill the finalSeasons and finalSeasons4k maps with false for missing seasons
375+
media.seasons.forEach((season) => {
376+
if (
377+
!finalSeasons.has(season.seasonNumber) &&
378+
tvShow.seasons.find(
379+
(s) => s.season_number === season.seasonNumber
380+
)?.episode_count
381+
) {
382+
finalSeasons.set(season.seasonNumber, false);
383+
}
384+
if (
385+
!finalSeasons4k.has(season.seasonNumber) &&
386+
tvShow.seasons.find(
387+
(s) => s.season_number === season.seasonNumber
388+
)?.episode_count
389+
) {
390+
finalSeasons4k.set(season.seasonNumber, false);
391+
}
392+
});
393+
356394
if (
357395
!showExists &&
358396
(media.status === MediaStatus.AVAILABLE ||
@@ -405,11 +443,11 @@ class AvailabilitySync {
405443
} catch (ex) {
406444
logger.error('Failed to complete availability sync.', {
407445
errorMessage: ex.message,
408-
label: 'Availability Sync',
446+
label: 'AvailabilitySync',
409447
});
410448
} finally {
411449
logger.info(`Availability sync complete.`, {
412-
label: 'Availability Sync',
450+
label: 'AvailabilitySync',
413451
});
414452
this.running = false;
415453
}
@@ -508,7 +546,7 @@ class AvailabilitySync {
508546
? media[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId']
509547
: null;
510548
}
511-
logger.info(
549+
logger.debug(
512550
`The ${is4k ? '4K' : 'non-4K'} ${
513551
media.mediaType === 'movie' ? 'movie' : 'show'
514552
} [TMDB ID ${media.tmdbId}] was not found in any ${
@@ -531,7 +569,7 @@ class AvailabilitySync {
531569
} [TMDB ID ${media.tmdbId}].`,
532570
{
533571
errorMessage: ex.message,
534-
label: 'Availability Sync',
572+
label: 'AvailabilitySync',
535573
}
536574
);
537575
}
@@ -555,56 +593,44 @@ class AvailabilitySync {
555593
// Retrieve the season keys to pass into our log
556594
const seasonKeys = [...seasonsPendingRemoval.keys()];
557595

558-
// let isSeasonRemoved = false;
559-
560596
try {
561597
for (const mediaSeason of media.seasons) {
562-
if (seasonsPendingRemoval.has(mediaSeason.seasonNumber)) {
598+
if (
599+
seasonsPendingRemoval.has(mediaSeason.seasonNumber) &&
600+
mediaSeason[is4k ? 'status4k' : 'status'] !== MediaStatus.UNKNOWN
601+
) {
563602
mediaSeason[is4k ? 'status4k' : 'status'] = MediaStatus.DELETED;
564603
}
565604
}
566605

567-
if (media.status === MediaStatus.AVAILABLE && !is4k) {
568-
media.status = MediaStatus.PARTIALLY_AVAILABLE;
569-
logger.info(
570-
`Marking the non-4K show [TMDB ID ${media.tmdbId}] as PARTIALLY_AVAILABLE because season removal has occurred.`,
571-
{ label: 'Availability Sync' }
572-
);
573-
}
574-
575-
if (media.status4k === MediaStatus.AVAILABLE && is4k) {
576-
media.status4k = MediaStatus.PARTIALLY_AVAILABLE;
577-
logger.info(
578-
`Marking the 4K show [TMDB ID ${media.tmdbId}] as PARTIALLY_AVAILABLE because season removal has occurred.`,
579-
{ label: 'Availability Sync' }
606+
if (media[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE) {
607+
media[is4k ? 'status4k' : 'status'] = MediaStatus.PARTIALLY_AVAILABLE;
608+
logger.debug(
609+
`Marking the ${
610+
is4k ? '4K' : 'non-4K'
611+
} show [TMDB ID ${media.tmdbId}] as PARTIALLY_AVAILABLE because season(s) [${seasonKeys}] was not found in any ${
612+
media.mediaType === 'tv' ? 'Sonarr' : 'Radarr'
613+
} and ${
614+
mediaServerType === MediaServerType.PLEX
615+
? 'plex'
616+
: mediaServerType === MediaServerType.JELLYFIN
617+
? 'jellyfin'
618+
: 'emby'
619+
} instance.`,
620+
{ label: 'AvailabilitySync' }
580621
);
581622
}
582623

583624
media.lastSeasonChange = new Date();
584625
await mediaRepository.save(media);
585-
586-
logger.info(
587-
`The ${is4k ? '4K' : 'non-4K'} season(s) [${seasonKeys}] [TMDB ID ${
588-
media.tmdbId
589-
}] was not found in any ${
590-
media.mediaType === 'tv' ? 'Sonarr' : 'Radarr'
591-
} and ${
592-
mediaServerType === MediaServerType.PLEX
593-
? 'plex'
594-
: mediaServerType === MediaServerType.JELLYFIN
595-
? 'jellyfin'
596-
: 'emby'
597-
} instance. Status will be changed to deleted.`,
598-
{ label: 'AvailabilitySync' }
599-
);
600626
} catch (ex) {
601627
logger.debug(
602628
`Failure updating the ${
603629
is4k ? '4K' : 'non-4K'
604630
} season(s) [${seasonKeys}], TMDB ID ${media.tmdbId}.`,
605631
{
606632
errorMessage: ex.message,
607-
label: 'Availability Sync',
633+
label: 'AvailabilitySync',
608634
}
609635
);
610636
}
@@ -671,7 +697,7 @@ class AvailabilitySync {
671697
}] from Radarr.`,
672698
{
673699
errorMessage: ex.message,
674-
label: 'Availability Sync',
700+
label: 'AvailabilitySync',
675701
}
676702
);
677703
}
@@ -728,7 +754,7 @@ class AvailabilitySync {
728754
}] from Sonarr.`,
729755
{
730756
errorMessage: ex.message,
731-
label: 'Availability Sync',
757+
label: 'AvailabilitySync',
732758
}
733759
);
734760
}
@@ -895,7 +921,7 @@ class AvailabilitySync {
895921
} [TMDB ID ${media.tmdbId}] from Plex.`,
896922
{
897923
errorMessage: ex.message,
898-
label: 'Availability Sync',
924+
label: 'AvailabilitySync',
899925
}
900926
);
901927
}

0 commit comments

Comments
 (0)