@@ -5421,6 +5421,154 @@ def test_season_details_secondary_fragment_renders_episodes(
54215421 )
54225422 mock_process_episodes .assert_called_once ()
54235423
5424+ @patch ("app.providers.services.get_media_metadata" )
5425+ @patch ("app.providers.tmdb.process_episodes" )
5426+ def test_season_details_persists_and_self_heals_episode_release_datetime (
5427+ self ,
5428+ mock_process_episodes ,
5429+ mock_get_metadata ,
5430+ ):
5431+ """Viewing a season page must persist release_datetime for every episode.
5432+
5433+ Regression test: episode Items created while browsing a season page
5434+ (as opposed to individually tracking/watching one) previously only
5435+ picked up runtime/rating from the live provider payload and silently
5436+ dropped release_datetime, leaving it NULL forever unless that exact
5437+ episode was later tracked or a manual metadata sync ran. This left
5438+ "not caught up" categorization permanently wrong for any unwatched
5439+ episode created this way.
5440+ """
5441+ tv_item = Item .objects .create (
5442+ media_id = "1668" ,
5443+ source = Sources .TMDB .value ,
5444+ media_type = MediaTypes .TV .value ,
5445+ title = "Test TV Show" ,
5446+ image = "http://example.com/show.jpg" ,
5447+ )
5448+ related_tv = TV .objects .create (
5449+ item = tv_item ,
5450+ user = self .user ,
5451+ status = Status .IN_PROGRESS .value ,
5452+ )
5453+ season_item = Item .objects .create (
5454+ media_id = "1668" ,
5455+ source = Sources .TMDB .value ,
5456+ media_type = MediaTypes .SEASON .value ,
5457+ title = "Test TV Show" ,
5458+ image = "http://example.com/show.jpg" ,
5459+ season_number = 1 ,
5460+ )
5461+ Season .objects .create (
5462+ item = season_item ,
5463+ user = self .user ,
5464+ status = Status .IN_PROGRESS .value ,
5465+ related_tv = related_tv ,
5466+ )
5467+
5468+ # Episode 1 already has a trusted release_datetime — must not be
5469+ # clobbered by a differing value from the live payload.
5470+ already_dated_episode = Item .objects .create (
5471+ media_id = "1668" ,
5472+ source = Sources .TMDB .value ,
5473+ media_type = MediaTypes .EPISODE .value ,
5474+ title = "No Shortcuts" ,
5475+ image = settings .IMG_NONE ,
5476+ season_number = 1 ,
5477+ episode_number = 1 ,
5478+ release_datetime = timezone .make_aware (datetime (2023 , 1 , 1 )),
5479+ )
5480+ # Episode 2 exists (e.g. created by an earlier page view before its
5481+ # air date was known) but was never given a release_datetime.
5482+ stale_episode = Item .objects .create (
5483+ media_id = "1668" ,
5484+ source = Sources .TMDB .value ,
5485+ media_type = MediaTypes .EPISODE .value ,
5486+ title = "Test TV Show" ,
5487+ image = settings .IMG_NONE ,
5488+ season_number = 1 ,
5489+ episode_number = 2 ,
5490+ release_datetime = None ,
5491+ )
5492+
5493+ mock_get_metadata .side_effect = lambda * _args , ** _kwargs : {
5494+ "title" : "Test TV Show" ,
5495+ "media_id" : "1668" ,
5496+ "source" : Sources .TMDB .value ,
5497+ "media_type" : MediaTypes .TV .value ,
5498+ "image" : "http://example.com/image.jpg" ,
5499+ "season/1" : {
5500+ "title" : "Test TV Show" ,
5501+ "season_title" : "Season 1" ,
5502+ "media_id" : "1668" ,
5503+ "media_type" : MediaTypes .SEASON .value ,
5504+ "source" : Sources .TMDB .value ,
5505+ "image" : "http://example.com/season.jpg" ,
5506+ "episodes" : [
5507+ {
5508+ "episode_number" : 1 ,
5509+ "name" : "No Shortcuts" ,
5510+ "air_date" : "2023-06-01" , # differs from the stored date
5511+ "runtime" : 48 ,
5512+ },
5513+ {
5514+ "episode_number" : 2 ,
5515+ "name" : "Episode 2" ,
5516+ "air_date" : "2023-01-08" ,
5517+ "runtime" : 42 ,
5518+ },
5519+ {
5520+ "episode_number" : 3 ,
5521+ "name" : "Episode 3" ,
5522+ "air_date" : "2023-01-15" ,
5523+ "runtime" : 44 ,
5524+ },
5525+ ],
5526+ },
5527+ }
5528+ mock_process_episodes .return_value = []
5529+
5530+ response = self .client .get (
5531+ reverse (
5532+ "season_details" ,
5533+ kwargs = {
5534+ "source" : Sources .TMDB .value ,
5535+ "media_id" : "1668" ,
5536+ "title" : "test-tv-show" ,
5537+ "season_number" : 1 ,
5538+ },
5539+ ),
5540+ {"fragment" : "secondary" },
5541+ )
5542+
5543+ self .assertEqual (response .status_code , 200 )
5544+
5545+ already_dated_episode .refresh_from_db ()
5546+ self .assertEqual (
5547+ already_dated_episode .release_datetime ,
5548+ timezone .make_aware (datetime (2023 , 1 , 1 )),
5549+ "an already-set release_datetime must not be overwritten" ,
5550+ )
5551+
5552+ stale_episode .refresh_from_db ()
5553+ self .assertEqual (
5554+ stale_episode .release_datetime ,
5555+ timezone .make_aware (datetime (2023 , 1 , 8 )),
5556+ "a NULL release_datetime must self-heal from the live payload" ,
5557+ )
5558+
5559+ new_episode = Item .objects .get (
5560+ media_id = "1668" ,
5561+ source = Sources .TMDB .value ,
5562+ media_type = MediaTypes .EPISODE .value ,
5563+ season_number = 1 ,
5564+ episode_number = 3 ,
5565+ )
5566+ self .assertEqual (
5567+ new_episode .release_datetime ,
5568+ timezone .make_aware (datetime (2023 , 1 , 15 )),
5569+ "a newly created episode must get release_datetime from the same payload" ,
5570+ )
5571+
54245572 @patch ("app.views.trakt_popularity_service.refresh_trakt_popularity" )
54255573 @patch ("app.providers.tmdb.get_tvdb_episode_image_map" )
54265574 @patch ("app.helpers.get_tmdb_backdrop_image" )
0 commit comments