@@ -1034,6 +1034,169 @@ def mock_metadata(media_type, tmdb_id, title, season_number=None):
10341034 self .assertEqual (new_season .status , Status .COMPLETED .value )
10351035 self .assertEqual (new_tv .status , Status .COMPLETED .value )
10361036
1037+ @patch ("app.models.providers.services.get_media_metadata" )
1038+ @patch ("integrations.imports.trakt.TraktImporter._get_metadata" )
1039+ def test_import_data_cascades_completed_tv_status_to_other_seasons (
1040+ self , mock_get_metadata , mock_get_media_metadata
1041+ ):
1042+ """Regression test for #985: a show that Trakt import marks
1043+ Completed must also cascade that completion down to its other
1044+ seasons/episodes, exactly like manually marking a show Completed in
1045+ the UI does (TV.save() -> _completed()). The bulk_update_with_history
1046+ flush in import_data() never calls TV.save(), so without the fix
1047+ this cascade is silently skipped.
1048+ """
1049+ TMDB_ID = 99997
1050+ SEASON_NUMBER = 2
1051+ TOTAL_EPISODES = 5
1052+
1053+ # Season 1 already exists and is only partially watched; it isn't
1054+ # touched by the watch-history entry below, so only the completion
1055+ # cascade (not the history walk) can bring it to Completed.
1056+ item_tv , _ = Item .objects .get_or_create (
1057+ media_id = TMDB_ID ,
1058+ source = Sources .TMDB .value ,
1059+ media_type = MediaTypes .TV .value ,
1060+ defaults = {"title" : "Test Show" , "image" : "" },
1061+ )
1062+ tv_obj = TV .objects .create (
1063+ item = item_tv , user = self .user , status = Status .IN_PROGRESS .value
1064+ )
1065+ item_season1 , _ = Item .objects .get_or_create (
1066+ media_id = TMDB_ID ,
1067+ source = Sources .TMDB .value ,
1068+ media_type = MediaTypes .SEASON .value ,
1069+ season_number = 1 ,
1070+ defaults = {"title" : "Test Show" , "image" : "" },
1071+ )
1072+ Season .objects .create (
1073+ item = item_season1 ,
1074+ user = self .user ,
1075+ related_tv = tv_obj ,
1076+ status = Status .IN_PROGRESS .value ,
1077+ )
1078+
1079+ def mock_metadata (media_type , tmdb_id , title , season_number = None ):
1080+ if media_type == MediaTypes .TV .value :
1081+ return {
1082+ "title" : "Test Show" ,
1083+ "image" : "" ,
1084+ "last_episode_season" : SEASON_NUMBER ,
1085+ "max_progress" : TOTAL_EPISODES ,
1086+ }
1087+ if media_type == MediaTypes .SEASON .value :
1088+ return {
1089+ "title" : f"Season { season_number } " ,
1090+ "image" : "" ,
1091+ "episodes" : [
1092+ {"episode_number" : i , "still_path" : None }
1093+ for i in range (1 , TOTAL_EPISODES + 1 )
1094+ ],
1095+ "max_progress" : TOTAL_EPISODES ,
1096+ }
1097+ return None
1098+
1099+ mock_get_metadata .side_effect = mock_metadata
1100+
1101+ # Metadata used by TV._completed(), the cascade helper the fix wires
1102+ # up. It reports season 1 as the only remaining incomplete season.
1103+ mock_get_media_metadata .return_value = {
1104+ "max_progress" : TOTAL_EPISODES ,
1105+ "related" : {"seasons" : [{"season_number" : 1 , "image" : "" }]},
1106+ "season/1" : {
1107+ "image" : "" ,
1108+ "season_number" : 1 ,
1109+ "episodes" : [{"episode_number" : i } for i in range (1 , TOTAL_EPISODES + 1 )],
1110+ },
1111+ }
1112+
1113+ entry = {
1114+ "type" : "episode" ,
1115+ "episode" : {
1116+ "season" : SEASON_NUMBER ,
1117+ "number" : TOTAL_EPISODES ,
1118+ "title" : "Finale" ,
1119+ },
1120+ "show" : {"title" : "Test Show" , "ids" : {"tmdb" : TMDB_ID }},
1121+ "watched_at" : "2024-06-01T00:00:00.000Z" ,
1122+ }
1123+
1124+ trakt_importer = TraktImporter ("testuser" , self .user , "overwrite" )
1125+ trakt_importer .process_watched_episode (entry )
1126+ trakt_importer .process_history = lambda : None
1127+ trakt_importer .process_watchlist = lambda : None
1128+ trakt_importer .process_ratings = lambda : None
1129+ trakt_importer .process_notes = lambda : None
1130+ trakt_importer .process_comments = lambda : None
1131+ trakt_importer .process_collection = lambda : None
1132+ trakt_importer .process_dropped = lambda : None
1133+ trakt_importer ._validate_username = lambda : None
1134+
1135+ trakt_importer .import_data ()
1136+
1137+ season1 = Season .objects .get (
1138+ user = self .user ,
1139+ item__media_id = str (TMDB_ID ),
1140+ item__season_number = 1 ,
1141+ )
1142+ self .assertEqual (season1 .status , Status .COMPLETED .value )
1143+ self .assertTrue (season1 .episodes .exists ())
1144+
1145+ def test_import_data_cascades_dropped_tv_status_to_in_progress_seasons (self ):
1146+ """Regression test for #985: a show hidden/dropped on Trakt must
1147+ have its in-progress seasons marked Dropped too, matching what
1148+ manually dropping a show does via TV.save() ->
1149+ _mark_in_progress_seasons_as_dropped(). Without the fix, the bulk
1150+ flush in import_data() only updates the TV row's own status.
1151+ """
1152+ TMDB_ID = 99996
1153+
1154+ item_tv , _ = Item .objects .get_or_create (
1155+ media_id = TMDB_ID ,
1156+ source = Sources .TMDB .value ,
1157+ media_type = MediaTypes .TV .value ,
1158+ defaults = {"title" : "Test Show" , "image" : "" },
1159+ )
1160+ tv_obj = TV .objects .create (
1161+ item = item_tv , user = self .user , status = Status .IN_PROGRESS .value
1162+ )
1163+ item_season , _ = Item .objects .get_or_create (
1164+ media_id = TMDB_ID ,
1165+ source = Sources .TMDB .value ,
1166+ media_type = MediaTypes .SEASON .value ,
1167+ season_number = 1 ,
1168+ defaults = {"title" : "Test Show" , "image" : "" },
1169+ )
1170+ Season .objects .create (
1171+ item = item_season ,
1172+ user = self .user ,
1173+ related_tv = tv_obj ,
1174+ status = Status .IN_PROGRESS .value ,
1175+ )
1176+
1177+ trakt_importer = TraktImporter ("testuser" , self .user , "overwrite" )
1178+ trakt_importer .dropped_tmdb_ids .add (TMDB_ID )
1179+ tv_obj .status = Status .DROPPED .value
1180+ trakt_importer .dropped_tvs .append (tv_obj )
1181+
1182+ trakt_importer .process_dropped = lambda : None
1183+ trakt_importer .process_history = lambda : None
1184+ trakt_importer .process_watchlist = lambda : None
1185+ trakt_importer .process_ratings = lambda : None
1186+ trakt_importer .process_notes = lambda : None
1187+ trakt_importer .process_comments = lambda : None
1188+ trakt_importer .process_collection = lambda : None
1189+ trakt_importer ._validate_username = lambda : None
1190+
1191+ trakt_importer .import_data ()
1192+
1193+ season = Season .objects .get (
1194+ user = self .user ,
1195+ item__media_id = str (TMDB_ID ),
1196+ item__season_number = 1 ,
1197+ )
1198+ self .assertEqual (season .status , Status .DROPPED .value )
1199+
10371200 @patch ("integrations.imports.trakt.TraktImporter._get_metadata" )
10381201 def test_last_episode_import_does_not_complete_existing_show_in_new_mode (
10391202 self , mock_get_metadata
0 commit comments