@@ -10,6 +10,11 @@ import type { PlexMetadata } from '@server/api/plexapi';
1010import PlexAPI from '@server/api/plexapi' ;
1111import type { SonarrSeason , SonarrSeries } from '@server/api/servarr/sonarr' ;
1212import 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' ;
1318import { MediaStatus , MediaType } from '@server/constants/media' ;
1419import { MediaServerType } from '@server/constants/server' ;
1520import { 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+
120204import availabilitySync from '@server/lib/availabilitySync' ;
121205
122206setupTestDb ( ) ;
@@ -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