Skip to content

Commit e3cd34a

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

8 files changed

Lines changed: 712 additions & 99 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: 215 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import type { PlexMetadata } from '@server/api/plexapi';
1010
import PlexAPI from '@server/api/plexapi';
1111
import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr';
1212
import SonarrAPI from '@server/api/servarr/sonarr';
13+
import TheMovieDb from '@server/api/themoviedb';
14+
import type {
15+
TmdbTvDetails,
16+
TmdbTvSeasonResult,
17+
} from '@server/api/themoviedb/interfaces';
1318
import { MediaStatus, MediaType } from '@server/constants/media';
1419
import { MediaServerType } from '@server/constants/server';
1520
import { getRepository } from '@server/datasource';
@@ -117,6 +122,85 @@ Object.defineProperty(SonarrAPI.prototype, 'getSeriesById', {
117122
configurable: true,
118123
});
119124

125+
// --- Mock TheMovieDb ---
126+
let getTvShowImpl: (args: {
127+
tvId: number;
128+
language?: string;
129+
}) => Promise<TmdbTvDetails> = async () => fakeTmdbShow(1);
130+
let getShowByTvdbIdImpl: (args: {
131+
tvdbId: number;
132+
language?: string;
133+
}) => Promise<TmdbTvDetails> = async () => fakeTmdbShow(1);
134+
135+
Object.defineProperty(TheMovieDb.prototype, 'getTvShow', {
136+
get() {
137+
return async (args: { tvId: number; language?: string }) =>
138+
getTvShowImpl(args);
139+
},
140+
set() {},
141+
configurable: true,
142+
});
143+
144+
Object.defineProperty(TheMovieDb.prototype, 'getShowByTvdbId', {
145+
get() {
146+
return async (args: { tvdbId: number; language?: string }) =>
147+
getShowByTvdbIdImpl(args);
148+
},
149+
set() {},
150+
configurable: true,
151+
});
152+
153+
// --- Helpers ---
154+
155+
function fakeTmdbShow(
156+
tmdbId: number,
157+
seasons: TmdbTvSeasonResult[] = [
158+
{
159+
id: 1,
160+
air_date: '2024-01-01',
161+
episode_count: 10,
162+
name: 'Season 1',
163+
overview: '',
164+
season_number: 1,
165+
},
166+
]
167+
): TmdbTvDetails {
168+
return {
169+
id: tmdbId,
170+
content_ratings: { results: [] },
171+
created_by: [],
172+
episode_run_time: [],
173+
first_air_date: '2024-01-01',
174+
genres: [],
175+
homepage: '',
176+
in_production: false,
177+
languages: ['en'],
178+
last_air_date: '2024-01-01',
179+
name: 'Test Show',
180+
networks: [],
181+
number_of_episodes: 10,
182+
number_of_seasons: seasons.length,
183+
origin_country: ['US'],
184+
original_language: 'en',
185+
original_name: 'Test Show',
186+
overview: '',
187+
popularity: 0,
188+
production_companies: [],
189+
production_countries: [],
190+
spoken_languages: [],
191+
seasons,
192+
status: 'Ended',
193+
type: 'Scripted',
194+
vote_average: 0,
195+
vote_count: 0,
196+
aggregate_credits: { cast: [] },
197+
credits: { crew: [] },
198+
external_ids: {},
199+
keywords: { results: [] },
200+
videos: { results: [] },
201+
};
202+
}
203+
120204
import availabilitySync from '@server/lib/availabilitySync';
121205

122206
setupTestDb();
@@ -282,8 +366,8 @@ function fakeSonarrSeasons(
282366
monitored: true,
283367
statistics: {
284368
episodeFileCount: seasonsWithFiles[i + 1] ?? 0,
285-
totalEpisodeCount: 22,
286-
episodeCount: 22,
369+
totalEpisodeCount: 10,
370+
episodeCount: 10,
287371
percentOfEpisodes: seasonsWithFiles[i + 1] ? 100 : 0,
288372
sizeOnDisk: seasonsWithFiles[i + 1] ? 7516192768 : 0,
289373
previousAiring: undefined,
@@ -304,6 +388,30 @@ describe('AvailabilitySync', () => {
304388
getSeriesByIdImpl = async () => {
305389
throw new Error('404');
306390
};
391+
getTvShowImpl = async ({ tvId }) =>
392+
fakeTmdbShow(
393+
tvId,
394+
Array.from({ length: 4 }, (_, i) => ({
395+
id: i + 1,
396+
air_date: '2024-01-01',
397+
episode_count: 10,
398+
name: `Season ${i + 1}`,
399+
overview: '',
400+
season_number: i + 1,
401+
}))
402+
);
403+
getShowByTvdbIdImpl = async ({ tvdbId }) =>
404+
fakeTmdbShow(
405+
tvdbId,
406+
Array.from({ length: 4 }, (_, i) => ({
407+
id: i + 1,
408+
air_date: '2024-01-01',
409+
episode_count: 10,
410+
name: `Season ${i + 1}`,
411+
overview: '',
412+
season_number: i + 1,
413+
}))
414+
);
307415

308416
const userRepository = getRepository(User);
309417
const existingAdmin = await userRepository.findOne({ where: { id: 1 } });
@@ -363,7 +471,7 @@ describe('AvailabilitySync', () => {
363471

364472
getEpisodesImpl = async (_seriesID: string, seasonID: string) => {
365473
if (seasonID === 'jellyfin-season-6-id') {
366-
return fakeJellyfinEpisodes(21);
474+
return fakeJellyfinEpisodes(10);
367475
}
368476
return [];
369477
};
@@ -378,13 +486,13 @@ describe('AvailabilitySync', () => {
378486
monitored: true,
379487
statistics: {
380488
episodeFileCount: 21,
381-
totalEpisodeCount: 177,
382-
episodeCount: 177,
383-
percentOfEpisodes: 11.86,
489+
totalEpisodeCount: 10,
490+
episodeCount: 10,
491+
percentOfEpisodes: 100,
384492
sizeOnDisk: 0,
385493
seasonCount: 8,
386494
},
387-
seasons: fakeSonarrSeasons(8, { 6: 21 }),
495+
seasons: fakeSonarrSeasons(8, { 6: 10 }),
388496
} as unknown as SonarrSeries;
389497
}
390498
throw new Error('404');
@@ -688,6 +796,106 @@ describe('AvailabilitySync', () => {
688796
'Show should remain AVAILABLE when getEpisodes fails'
689797
);
690798
});
799+
800+
it('should mark show as PARTIALLY_AVAILABLE when some seasons are available and some are unknown', async () => {
801+
configureJellyfin();
802+
configureSonarr([{ syncEnabled: true }]);
803+
804+
const mediaRepository = getRepository(Media);
805+
806+
const media = new Media();
807+
media.tmdbId = 1412;
808+
media.mediaType = MediaType.TV;
809+
media.status = MediaStatus.AVAILABLE;
810+
media.jellyfinMediaId = 'jellyfin-partial-id';
811+
media.externalServiceId = 103;
812+
media.seasons = [
813+
new Season({
814+
seasonNumber: 1,
815+
status: MediaStatus.AVAILABLE,
816+
status4k: MediaStatus.UNKNOWN,
817+
}),
818+
new Season({
819+
seasonNumber: 2,
820+
status: MediaStatus.AVAILABLE,
821+
status4k: MediaStatus.UNKNOWN,
822+
}),
823+
new Season({
824+
seasonNumber: 3,
825+
status: MediaStatus.UNKNOWN,
826+
status4k: MediaStatus.UNKNOWN,
827+
}),
828+
new Season({
829+
seasonNumber: 4,
830+
status: MediaStatus.UNKNOWN,
831+
status4k: MediaStatus.UNKNOWN,
832+
}),
833+
];
834+
835+
await mediaRepository.save(media);
836+
837+
getItemDataImpl = async (id: string) => {
838+
if (id === 'jellyfin-partial-id') {
839+
return fakeJellyfinShow('jellyfin-partial-id', '1412');
840+
}
841+
return undefined;
842+
};
843+
844+
getSeasonsImpl = async (seriesID: string) => {
845+
if (seriesID === 'jellyfin-partial-id') {
846+
return [
847+
fakeJellyfinSeason(1, 'jellyfin-partial-s1-id'),
848+
fakeJellyfinSeason(2, 'jellyfin-partial-s2-id'),
849+
];
850+
}
851+
return [];
852+
};
853+
854+
getEpisodesImpl = async (_seriesID: string, seasonID: string) => {
855+
if (seasonID === 'jellyfin-partial-s1-id') {
856+
return fakeJellyfinEpisodes(10);
857+
}
858+
if (seasonID === 'jellyfin-partial-s2-id') {
859+
return fakeJellyfinEpisodes(10);
860+
}
861+
return [];
862+
};
863+
864+
getSeriesByIdImpl = async (id: number) => {
865+
if (id === 103) {
866+
return {
867+
tvdbId: 99997,
868+
id: 103,
869+
title: 'Partial Show',
870+
titleSlug: 'partial-show',
871+
monitored: true,
872+
statistics: {
873+
episodeFileCount: 20,
874+
totalEpisodeCount: 40,
875+
episodeCount: 40,
876+
percentOfEpisodes: 50,
877+
sizeOnDisk: 0,
878+
seasonCount: 4,
879+
},
880+
seasons: fakeSonarrSeasons(4, { 1: 10, 2: 10 }),
881+
} as unknown as SonarrSeries;
882+
}
883+
throw new Error('404');
884+
};
885+
886+
await availabilitySync.run();
887+
888+
const updated = await mediaRepository.findOneOrFail({
889+
where: { tmdbId: 1412 },
890+
relations: ['seasons'],
891+
});
892+
893+
assert.strictEqual(
894+
updated.status,
895+
MediaStatus.PARTIALLY_AVAILABLE,
896+
'Show should be PARTIALLY_AVAILABLE when some seasons are available and some are unknown'
897+
);
898+
});
691899
});
692900

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

0 commit comments

Comments
 (0)