mirror of
https://github.com/FuzzyGrim/Yamtrack.git
synced 2026-04-18 12:09:33 +00:00
fix untracked old seasons created from migration #1348
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
# Generated by Django 5.2.12 on 2026-04-05 17:30
|
||||
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
MEDIA_TYPE_TV = "tv"
|
||||
@@ -7,6 +10,7 @@ MEDIA_TYPE_SEASON = "season"
|
||||
STATUS_COMPLETED = "Completed"
|
||||
STATUS_IN_PROGRESS = "In progress"
|
||||
STATUS_PLANNING = "Planning"
|
||||
MIGRATION_CREATED_AT = datetime(2026, 4, 5, 15, 30, tzinfo=ZoneInfo("UTC"))
|
||||
|
||||
|
||||
def reopen_completed_tv_with_new_seasons(apps, _):
|
||||
@@ -32,13 +36,14 @@ def reopen_completed_tv_with_new_seasons(apps, _):
|
||||
existing_seasons.values_list("item__season_number", flat=True),
|
||||
)
|
||||
|
||||
# Only genuinely upcoming seasons should reopen a completed show.
|
||||
missing_season_items = list(
|
||||
Item.objects.filter(
|
||||
media_id=tv.item.media_id,
|
||||
source=tv.item.source,
|
||||
media_type=MEDIA_TYPE_SEASON,
|
||||
season_number__gt=0,
|
||||
event__isnull=False,
|
||||
event__datetime__gte=MIGRATION_CREATED_AT,
|
||||
)
|
||||
.exclude(season_number__in=existing_season_numbers)
|
||||
.distinct()
|
||||
|
||||
104
src/app/migrations/0060_fix_reopened_completed_tv_seasons.py
Normal file
104
src/app/migrations/0060_fix_reopened_completed_tv_seasons.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# Generated by Django 5.2.12 on 2026-04-14 12:00
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django.db import migrations, transaction
|
||||
from django.db.models import Exists, OuterRef
|
||||
from django.utils import timezone
|
||||
|
||||
MEDIA_TYPE_TV = "tv"
|
||||
STATUS_COMPLETED = "Completed"
|
||||
STATUS_IN_PROGRESS = "In progress"
|
||||
STATUS_PLANNING = "Planning"
|
||||
BUGGY_MIGRATION_START = datetime(2026, 4, 6, 20, 0, tzinfo=ZoneInfo("UTC"))
|
||||
BUGGY_MIGRATION_END = datetime(2026, 4, 14, 23, 0, tzinfo=ZoneInfo("UTC"))
|
||||
|
||||
|
||||
def fix_reopened_completed_tv_seasons(apps, _):
|
||||
"""Remove bad planning seasons created by 0057 and restore affected TVs."""
|
||||
Event = apps.get_model("events", "Event")
|
||||
HistoricalSeason = apps.get_model("app", "HistoricalSeason")
|
||||
HistoricalTV = apps.get_model("app", "HistoricalTV")
|
||||
Season = apps.get_model("app", "Season")
|
||||
TV = apps.get_model("app", "TV")
|
||||
|
||||
now = timezone.now()
|
||||
season_history = HistoricalSeason.objects.filter(id=OuterRef("id"))
|
||||
future_events = Event.objects.filter(
|
||||
item_id=OuterRef("item_id"),
|
||||
datetime__gte=now,
|
||||
)
|
||||
|
||||
# 0057 created plain Planning seasons without history. If those seasons also
|
||||
# have no future events and were created during the v0.25.1 release window
|
||||
# (April 6, 2026 22:00 UTC+2 to April 15, 2026 01:00 UTC+2), they are very
|
||||
# likely the bogus backfill we want to remove rather than a legitimate
|
||||
# upcoming season.
|
||||
bogus_seasons = list(
|
||||
Season.objects
|
||||
.filter(
|
||||
status=STATUS_PLANNING,
|
||||
related_tv__status=STATUS_IN_PROGRESS,
|
||||
related_tv__item__media_type=MEDIA_TYPE_TV,
|
||||
created_at__gte=BUGGY_MIGRATION_START,
|
||||
created_at__lte=BUGGY_MIGRATION_END,
|
||||
)
|
||||
.annotate(
|
||||
has_history=Exists(season_history),
|
||||
has_future_events=Exists(future_events),
|
||||
)
|
||||
.filter(
|
||||
has_history=False,
|
||||
has_future_events=False,
|
||||
)
|
||||
.select_related("related_tv", "user")
|
||||
)
|
||||
if not bogus_seasons:
|
||||
return
|
||||
|
||||
seasons_by_tv_id = defaultdict(list)
|
||||
for season in bogus_seasons:
|
||||
seasons_by_tv_id[season.related_tv_id].append(season)
|
||||
|
||||
with transaction.atomic():
|
||||
Season.objects.filter(
|
||||
id__in=[season.id for season in bogus_seasons],
|
||||
).delete()
|
||||
|
||||
for tv_id, tv_seasons in seasons_by_tv_id.items():
|
||||
tv = tv_seasons[0].related_tv
|
||||
latest_history = (
|
||||
HistoricalTV.objects
|
||||
.filter(id=tv.id)
|
||||
.order_by("-history_date", "-history_id")
|
||||
.first()
|
||||
)
|
||||
if not latest_history or latest_history.status != STATUS_COMPLETED:
|
||||
continue
|
||||
|
||||
# Users often had the parent TV marked completed without tracking
|
||||
# seasons individually, so once the bogus 0057 seasons are gone we
|
||||
# can safely restore the parent TV to completed.
|
||||
if tv.status != STATUS_IN_PROGRESS:
|
||||
continue
|
||||
|
||||
tv.status = STATUS_COMPLETED
|
||||
TV.objects.filter(pk=tv.pk).update(status=tv.status)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""Repair bad planning seasons created for already completed shows."""
|
||||
|
||||
dependencies = [
|
||||
("events", "0014_delete_empty_content_number_comic_events"),
|
||||
("app", "0059_usermessage_user_shown_idx"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
fix_reopened_completed_tv_seasons,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
),
|
||||
]
|
||||
@@ -6,6 +6,7 @@ import requests
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
from django.db.models import Prefetch
|
||||
from django.utils import timezone
|
||||
from simple_history.utils import bulk_create_with_history, bulk_update_with_history
|
||||
|
||||
from app.models import TV, Item, MediaTypes, Season, Status
|
||||
@@ -33,7 +34,11 @@ def process_tv(tv_item, events_bulk):
|
||||
seasons_to_process,
|
||||
events_bulk,
|
||||
)
|
||||
reopen_completed_tv_with_new_seasons(tv_item, processed_season_items)
|
||||
reopen_completed_tv_with_new_seasons(
|
||||
tv_item,
|
||||
processed_season_items,
|
||||
events_bulk,
|
||||
)
|
||||
|
||||
except services.ProviderAPIError:
|
||||
logger.warning(
|
||||
@@ -126,12 +131,15 @@ def process_tv_seasons(tv_item, seasons_to_process, events_bulk):
|
||||
return processed_season_items
|
||||
|
||||
|
||||
def reopen_completed_tv_with_new_seasons(tv_item, season_items):
|
||||
def reopen_completed_tv_with_new_seasons(tv_item, season_items, events_bulk):
|
||||
"""Reopen completed TV entries and create planning seasons when needed."""
|
||||
season_item_map = {
|
||||
season_item.season_number: season_item
|
||||
eligible_season_items = [
|
||||
season_item
|
||||
for season_item in season_items
|
||||
if season_item.season_number and season_item.season_number > 0
|
||||
]
|
||||
season_item_map = {
|
||||
season_item.season_number: season_item for season_item in eligible_season_items
|
||||
}
|
||||
if not season_item_map:
|
||||
logger.info(
|
||||
@@ -140,7 +148,22 @@ def reopen_completed_tv_with_new_seasons(tv_item, season_items):
|
||||
)
|
||||
return
|
||||
|
||||
sorted_season_numbers = sorted(season_item_map)
|
||||
# Only future season events should reopen a completed show; processed
|
||||
# past-only seasons can exist when local season tracking was incomplete.
|
||||
future_season_numbers = {
|
||||
event.item.season_number
|
||||
for event in events_bulk
|
||||
if event.item in eligible_season_items and event.datetime >= timezone.now()
|
||||
}
|
||||
if not future_season_numbers:
|
||||
logger.info(
|
||||
"%s - Processed seasons have no future events; "
|
||||
"skipping completed-TV reopening",
|
||||
tv_item,
|
||||
)
|
||||
return
|
||||
|
||||
sorted_season_numbers = sorted(future_season_numbers)
|
||||
completed_tvs = list(
|
||||
TV.objects.filter(
|
||||
item=tv_item,
|
||||
|
||||
@@ -126,7 +126,7 @@ class CalendarTVTests(CalendarFixturesMixin, TestCase):
|
||||
"image": "http://example.com/season2.jpg",
|
||||
"season_number": 2,
|
||||
"episodes": [
|
||||
{"episode_number": 1, "air_date": "2026-01-20"},
|
||||
{"episode_number": 1, "air_date": "2027-01-20"},
|
||||
],
|
||||
"tvdb_id": "81189",
|
||||
},
|
||||
@@ -149,6 +149,64 @@ class CalendarTVTests(CalendarFixturesMixin, TestCase):
|
||||
self.assertEqual(season_two.status, Status.PLANNING.value)
|
||||
self.assertEqual(len(events_bulk), 1)
|
||||
|
||||
@patch("events.calendar.tv.get_tvmaze_episode_map")
|
||||
@patch("events.calendar.tv.tmdb.tv_with_seasons")
|
||||
@patch("events.calendar.tv.tmdb.tv")
|
||||
def test_process_tv_does_not_reopen_completed_show_for_past_only_season(
|
||||
self,
|
||||
mock_tv,
|
||||
mock_tv_with_seasons,
|
||||
mock_get_tvmaze_episode_map,
|
||||
):
|
||||
"""Past-only seasons should not reopen a completed TV entry."""
|
||||
TV.objects.filter(item=self.tv_item, user=self.user).update(
|
||||
status=Status.COMPLETED.value,
|
||||
)
|
||||
Season.objects.filter(item=self.season_item, user=self.user).update(
|
||||
status=Status.COMPLETED.value,
|
||||
)
|
||||
Event.objects.create(
|
||||
item=self.season_item,
|
||||
content_number=1,
|
||||
datetime=date_parser("2008-01-20"),
|
||||
)
|
||||
|
||||
mock_tv.return_value = {
|
||||
"related": {
|
||||
"seasons": [
|
||||
{"season_number": 1, "episodes": [1]},
|
||||
{"season_number": 2, "episodes": [1]},
|
||||
],
|
||||
},
|
||||
"next_episode_season": 2,
|
||||
}
|
||||
mock_tv_with_seasons.return_value = {
|
||||
"season/2": {
|
||||
"image": "http://example.com/season2.jpg",
|
||||
"season_number": 2,
|
||||
"episodes": [
|
||||
{"episode_number": 1, "air_date": "2010-01-20"},
|
||||
],
|
||||
"tvdb_id": "81189",
|
||||
},
|
||||
}
|
||||
mock_get_tvmaze_episode_map.return_value = {}
|
||||
|
||||
events_bulk = []
|
||||
process_tv(self.tv_item, events_bulk)
|
||||
|
||||
tv = TV.objects.get(item=self.tv_item, user=self.user)
|
||||
self.assertEqual(tv.status, Status.COMPLETED.value)
|
||||
self.assertFalse(
|
||||
Season.objects.filter(
|
||||
item__media_id=self.tv_item.media_id,
|
||||
item__source=self.tv_item.source,
|
||||
item__season_number=2,
|
||||
user=self.user,
|
||||
).exists(),
|
||||
)
|
||||
self.assertEqual(len(events_bulk), 1)
|
||||
|
||||
@patch("events.calendar.tv.services.api_request")
|
||||
def test_get_tvmaze_episode_map(self, mock_api_request):
|
||||
"""Test get_tvmaze_episode_map function."""
|
||||
|
||||
Reference in New Issue
Block a user