@@ -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,72 @@ 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+
131+ Object . defineProperty ( TheMovieDb . prototype , 'getTvShow' , {
132+ get ( ) {
133+ return async ( args : { tvId : number ; language ?: string } ) =>
134+ getTvShowImpl ( args ) ;
135+ } ,
136+ set ( ) { } ,
137+ configurable : true ,
138+ } ) ;
139+
140+ // --- Helpers ---
141+
142+ function fakeTmdbShow (
143+ tmdbId : number ,
144+ seasons : TmdbTvSeasonResult [ ] = [
145+ {
146+ id : 1 ,
147+ air_date : '2024-01-01' ,
148+ episode_count : 10 ,
149+ name : 'Season 1' ,
150+ overview : '' ,
151+ season_number : 1 ,
152+ } ,
153+ ]
154+ ) : TmdbTvDetails {
155+ return {
156+ id : tmdbId ,
157+ content_ratings : { results : [ ] } ,
158+ created_by : [ ] ,
159+ episode_run_time : [ ] ,
160+ first_air_date : '2024-01-01' ,
161+ genres : [ ] ,
162+ homepage : '' ,
163+ in_production : false ,
164+ languages : [ 'en' ] ,
165+ last_air_date : '2024-01-01' ,
166+ name : 'Test Show' ,
167+ networks : [ ] ,
168+ number_of_episodes : 10 ,
169+ number_of_seasons : seasons . length ,
170+ origin_country : [ 'US' ] ,
171+ original_language : 'en' ,
172+ original_name : 'Test Show' ,
173+ overview : '' ,
174+ popularity : 0 ,
175+ production_companies : [ ] ,
176+ production_countries : [ ] ,
177+ spoken_languages : [ ] ,
178+ seasons,
179+ status : 'Ended' ,
180+ type : 'Scripted' ,
181+ vote_average : 0 ,
182+ vote_count : 0 ,
183+ aggregate_credits : { cast : [ ] } ,
184+ credits : { crew : [ ] } ,
185+ external_ids : { } ,
186+ keywords : { results : [ ] } ,
187+ videos : { results : [ ] } ,
188+ } ;
189+ }
190+
120191import availabilitySync from '@server/lib/availabilitySync' ;
121192
122193setupTestDb ( ) ;
@@ -304,6 +375,18 @@ describe('AvailabilitySync', () => {
304375 getSeriesByIdImpl = async ( ) => {
305376 throw new Error ( '404' ) ;
306377 } ;
378+ getTvShowImpl = async ( { tvId } ) =>
379+ fakeTmdbShow (
380+ tvId ,
381+ Array . from ( { length : 12 } , ( _ , i ) => ( {
382+ id : i + 1 ,
383+ air_date : '2024-01-01' ,
384+ episode_count : 10 ,
385+ name : `Season ${ i + 1 } ` ,
386+ overview : '' ,
387+ season_number : i + 1 ,
388+ } ) )
389+ ) ;
307390
308391 const userRepository = getRepository ( User ) ;
309392 const existingAdmin = await userRepository . findOne ( { where : { id : 1 } } ) ;
@@ -688,6 +771,106 @@ describe('AvailabilitySync', () => {
688771 'Show should remain AVAILABLE when getEpisodes fails'
689772 ) ;
690773 } ) ;
774+
775+ it ( 'should mark show as PARTIALLY_AVAILABLE when some seasons are available and some are unknown' , async ( ) => {
776+ configureJellyfin ( ) ;
777+ configureSonarr ( [ { syncEnabled : true } ] ) ;
778+
779+ const mediaRepository = getRepository ( Media ) ;
780+
781+ const media = new Media ( ) ;
782+ media . tmdbId = 1412 ;
783+ media . mediaType = MediaType . TV ;
784+ media . status = MediaStatus . AVAILABLE ;
785+ media . jellyfinMediaId = 'jellyfin-partial-id' ;
786+ media . externalServiceId = 103 ;
787+ media . seasons = [
788+ new Season ( {
789+ seasonNumber : 1 ,
790+ status : MediaStatus . AVAILABLE ,
791+ status4k : MediaStatus . UNKNOWN ,
792+ } ) ,
793+ new Season ( {
794+ seasonNumber : 2 ,
795+ status : MediaStatus . AVAILABLE ,
796+ status4k : MediaStatus . UNKNOWN ,
797+ } ) ,
798+ new Season ( {
799+ seasonNumber : 3 ,
800+ status : MediaStatus . UNKNOWN ,
801+ status4k : MediaStatus . UNKNOWN ,
802+ } ) ,
803+ new Season ( {
804+ seasonNumber : 4 ,
805+ status : MediaStatus . UNKNOWN ,
806+ status4k : MediaStatus . UNKNOWN ,
807+ } ) ,
808+ ] ;
809+
810+ await mediaRepository . save ( media ) ;
811+
812+ getItemDataImpl = async ( id : string ) => {
813+ if ( id === 'jellyfin-partial-id' ) {
814+ return fakeJellyfinShow ( 'jellyfin-partial-id' , '1412' ) ;
815+ }
816+ return undefined ;
817+ } ;
818+
819+ getSeasonsImpl = async ( seriesID : string ) => {
820+ if ( seriesID === 'jellyfin-partial-id' ) {
821+ return [
822+ fakeJellyfinSeason ( 1 , 'jellyfin-partial-s1-id' ) ,
823+ fakeJellyfinSeason ( 2 , 'jellyfin-partial-s2-id' ) ,
824+ ] ;
825+ }
826+ return [ ] ;
827+ } ;
828+
829+ getEpisodesImpl = async ( _seriesID : string , seasonID : string ) => {
830+ if ( seasonID === 'jellyfin-partial-s1-id' ) {
831+ return fakeJellyfinEpisodes ( 10 ) ;
832+ }
833+ if ( seasonID === 'jellyfin-partial-s2-id' ) {
834+ return fakeJellyfinEpisodes ( 10 ) ;
835+ }
836+ return [ ] ;
837+ } ;
838+
839+ getSeriesByIdImpl = async ( id : number ) => {
840+ if ( id === 103 ) {
841+ return {
842+ tvdbId : 99997 ,
843+ id : 103 ,
844+ title : 'Partial Show' ,
845+ titleSlug : 'partial-show' ,
846+ monitored : true ,
847+ statistics : {
848+ episodeFileCount : 20 ,
849+ totalEpisodeCount : 40 ,
850+ episodeCount : 40 ,
851+ percentOfEpisodes : 50 ,
852+ sizeOnDisk : 0 ,
853+ seasonCount : 4 ,
854+ } ,
855+ seasons : fakeSonarrSeasons ( 4 , { 1 : 10 , 2 : 10 } ) ,
856+ } as unknown as SonarrSeries ;
857+ }
858+ throw new Error ( '404' ) ;
859+ } ;
860+
861+ await availabilitySync . run ( ) ;
862+
863+ const updated = await mediaRepository . findOneOrFail ( {
864+ where : { tmdbId : 1412 } ,
865+ relations : [ 'seasons' ] ,
866+ } ) ;
867+
868+ assert . strictEqual (
869+ updated . status ,
870+ MediaStatus . PARTIALLY_AVAILABLE ,
871+ 'Show should be PARTIALLY_AVAILABLE when some seasons are available and some are unknown'
872+ ) ;
873+ } ) ;
691874 } ) ;
692875
693876 describe ( 'TV season availability - Plex' , ( ) => {
0 commit comments