Update expiration date handling in M3U components to ensure UTC format

This commit is contained in:
SergeantPanda 2026-03-15 16:00:23 -05:00
parent 1d4603dadd
commit 09d5f6bb9f
2 changed files with 30 additions and 15 deletions

View file

@ -7,6 +7,7 @@ from apps.channels.models import ChannelGroup, ChannelGroupM3UAccount
from apps.channels.serializers import (
ChannelGroupM3UAccountSerializer,
)
from datetime import timezone as dt_tz
import logging
import json
@ -214,10 +215,14 @@ class M3UAccountSerializer(serializers.ModelSerializer):
# Surface default profile's exp_date for the form.
# Use prefetch cache (obj.profiles.all()) to avoid an extra query per account.
# Always emit a Z-suffix UTC string so JS new Date() never misinterprets it as local.
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
)
exp = default_profile.exp_date if default_profile else None
if exp:
exp_utc = exp.astimezone(dt_tz.utc) if exp.tzinfo else exp.replace(tzinfo=dt_tz.utc)
data["exp_date"] = exp_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
else:
data["exp_date"] = None
return data
@ -286,12 +291,19 @@ class M3UAccountSerializer(serializers.ModelSerializer):
memberships_to_update, ["enabled"]
)
# Write exp_date through to the default profile
# Write exp_date through to the default profile.
# Use a fresh DB query (not the prefetch cache) so we get the profile
# object AFTER the post_save signal (create_profile_for_m3u_account)
# has already updated max_streams, avoiding a stale-value overwrite.
if exp_date != "__NOT_SET__":
default_profile = instance.profiles.filter(is_default=True).first()
if default_profile:
default_profile.exp_date = exp_date
default_profile.save()
default_profile.save(update_fields=['exp_date'])
# Invalidate the profiles prefetch cache so to_representation
# sees the updated exp_date rather than the pre-request snapshot.
if '_prefetched_objects_cache' in instance.__dict__:
instance._prefetched_objects_cache.pop('profiles', None)
return instance
@ -342,7 +354,9 @@ class M3UAccountSerializer(serializers.ModelSerializer):
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()
exp = min(expiring)
exp_utc = exp.astimezone(dt_tz.utc) if exp.tzinfo else exp.replace(tzinfo=dt_tz.utc)
return exp_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
def get_all_expirations(self, obj):
"""Return exp_date info for every profile that has one (for tooltip)."""
@ -355,7 +369,7 @@ class M3UAccountSerializer(serializers.ModelSerializer):
{
"profile_id": p.id,
"profile_name": p.name,
"exp_date": p.exp_date.isoformat(),
"exp_date": (p.exp_date.astimezone(dt_tz.utc) if p.exp_date.tzinfo else p.exp_date.replace(tzinfo=dt_tz.utc)).strftime('%Y-%m-%dT%H:%M:%SZ'),
"is_active": p.is_active,
}
for p in profiles

View file

@ -46,6 +46,7 @@ const M3U = ({
const [playlist, setPlaylist] = useState(null);
const [file, setFile] = useState(null);
const [expDate, setExpDate] = useState(null);
const [profileModalOpen, setProfileModalOpen] = useState(false);
const [groupFilterModalOpen, setGroupFilterModalOpen] = useState(false);
const [filterModalOpen, setFilterModalOpen] = useState(false);
@ -70,7 +71,6 @@ const M3U = ({
stale_stream_days: 7,
priority: 0,
enable_vod: false,
exp_date: null,
},
validate: {
@ -103,8 +103,8 @@ const M3U = ({
? m3uAccount.priority
: 0,
enable_vod: m3uAccount.enable_vod || false,
exp_date: m3uAccount.exp_date ? new Date(m3uAccount.exp_date) : null,
});
setExpDate(m3uAccount.exp_date ? new Date(m3uAccount.exp_date) : null);
// Determine schedule type from existing data
setScheduleType(
@ -122,6 +122,7 @@ const M3U = ({
setPlaylist(null);
form.reset();
setScheduleType('interval');
setExpDate(null);
}
}, [m3uAccount]);
@ -134,13 +135,13 @@ const M3U = ({
const onSubmit = async () => {
const { create_epg, ...values } = form.getValues();
// Convert exp_date Date object to ISO string for the API
// Convert exp_date (from controlled state) to ISO string for the API
if (values.account_type === 'XC') {
// XC accounts have exp_date auto-managed server-side; don't send it
delete values.exp_date;
} else if (values.exp_date instanceof Date) {
values.exp_date = values.exp_date.toISOString();
} else if (!values.exp_date) {
} else if (expDate instanceof Date) {
values.exp_date = expDate.toISOString();
} else {
values.exp_date = null;
}
@ -387,8 +388,8 @@ const M3U = ({
placeholder="No expiration"
clearable
valueFormat="MMM D, YYYY h:mm A"
{...form.getInputProps('exp_date')}
key={form.key('exp_date')}
value={expDate}
onChange={(v) => setExpDate(v ? new Date(v) : null)}
/>
</>
)}