diff --git a/apps/m3u/serializers.py b/apps/m3u/serializers.py index 0d65276b..587d98cb 100644 --- a/apps/m3u/serializers.py +++ b/apps/m3u/serializers.py @@ -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 diff --git a/frontend/src/components/forms/M3U.jsx b/frontend/src/components/forms/M3U.jsx index 423b2a05..c8caeaef 100644 --- a/frontend/src/components/forms/M3U.jsx +++ b/frontend/src/components/forms/M3U.jsx @@ -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)} /> )}