diff --git a/apps/m3u/api_views.py b/apps/m3u/api_views.py index 34f3cd77..3b856dc8 100644 --- a/apps/m3u/api_views.py +++ b/apps/m3u/api_views.py @@ -40,7 +40,7 @@ class M3UAccountViewSet(viewsets.ModelViewSet): queryset = M3UAccount.objects.select_related( "refresh_task__crontab", "refresh_task__interval" - ).prefetch_related("channel_group") + ).prefetch_related("channel_group", "profiles") serializer_class = M3UAccountSerializer def get_permissions(self): diff --git a/apps/m3u/serializers.py b/apps/m3u/serializers.py index c629cac9..0d65276b 100644 --- a/apps/m3u/serializers.py +++ b/apps/m3u/serializers.py @@ -212,8 +212,9 @@ class M3UAccountSerializer(serializers.ModelSerializer): cron_expr = f"{ct.minute} {ct.hour} {ct.day_of_month} {ct.month_of_year} {ct.day_of_week}" data["cron_expression"] = cron_expr - # Surface default profile's exp_date for the form - default_profile = instance.profiles.filter(is_default=True).first() + # Surface default profile's exp_date for the form. + # Use prefetch cache (obj.profiles.all()) to avoid an extra query per account. + default_profile = next((p for p in instance.profiles.all() if p.is_default), None) data["exp_date"] = ( default_profile.exp_date.isoformat() if default_profile and default_profile.exp_date else None ) @@ -337,15 +338,19 @@ class M3UAccountSerializer(serializers.ModelSerializer): def get_earliest_expiration(self, obj): """Return the soonest exp_date across all active profiles for this account.""" - profiles_with_exp = obj.profiles.filter( - is_active=True, exp_date__isnull=False - ).order_by("exp_date") - first = profiles_with_exp.first() - return first.exp_date.isoformat() if first else None + # Filter in Python over the prefetch cache to avoid an extra query per account. + expiring = [p.exp_date for p in obj.profiles.all() if p.is_active and p.exp_date] + if not expiring: + return None + return min(expiring).isoformat() def get_all_expirations(self, obj): """Return exp_date info for every profile that has one (for tooltip).""" - profiles = obj.profiles.filter(exp_date__isnull=False).order_by("exp_date") + # Filter in Python over the prefetch cache to avoid an extra query per account. + profiles = sorted( + (p for p in obj.profiles.all() if p.exp_date), + key=lambda p: p.exp_date, + ) return [ { "profile_id": p.id, diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py index 0af5d382..996226ec 100644 --- a/apps/m3u/tasks.py +++ b/apps/m3u/tasks.py @@ -3199,9 +3199,12 @@ def evaluate_profile_expiration_notification(profile): from core.models import SystemNotification from core.utils import send_websocket_notification, send_notification_dismissed + exp = profile.exp_date + if not exp: + return None + now = timezone.now() warning_threshold = now + timezone.timedelta(days=7) - exp = profile.exp_date warning_key = f"xc-exp-warning-{profile.id}" expired_key = f"xc-exp-expired-{profile.id}" @@ -3299,7 +3302,7 @@ def evaluate_profile_expiration_notification(profile): @shared_task -def check_xc_account_expirations(): +def check_account_expirations(): """ Daily task: check all account profiles for upcoming expirations. Creates/updates SystemNotifications for profiles expiring within 7 days. diff --git a/dispatcharr/settings.py b/dispatcharr/settings.py index 46e03d1f..b0d387b2 100644 --- a/dispatcharr/settings.py +++ b/dispatcharr/settings.py @@ -267,9 +267,9 @@ CELERY_BEAT_SCHEDULE = { "task": "core.tasks.check_for_version_update", "schedule": 86400.0, # Once every 24 hours }, - # Check for XC account expirations daily - "check-xc-account-expirations": { - "task": "apps.m3u.tasks.check_xc_account_expirations", + # Check for account expirations daily + "check-account-expirations": { + "task": "apps.m3u.tasks.check_account_expirations", "schedule": 86400.0, # Once every 24 hours }, }