From a5f7a88ba034ee3678edd6c48c9e3cfb030dc86b Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 17 Jul 2025 16:54:37 -0500 Subject: [PATCH] Add option to force dummy epg. --- apps/channels/serializers.py | 27 ++++++- apps/m3u/api_views.py | 7 ++ .../src/components/forms/M3UGroupFilter.jsx | 81 +++++++++++++++---- 3 files changed, 96 insertions(+), 19 deletions(-) diff --git a/apps/channels/serializers.py b/apps/channels/serializers.py index 278399dd..84a68e50 100644 --- a/apps/channels/serializers.py +++ b/apps/channels/serializers.py @@ -302,13 +302,34 @@ class ChannelGroupM3UAccountSerializer(serializers.ModelSerializer): enabled = serializers.BooleanField() auto_channel_sync = serializers.BooleanField(default=False) auto_sync_channel_start = serializers.FloatField(allow_null=True, required=False) + custom_properties = serializers.JSONField(required=False) class Meta: model = ChannelGroupM3UAccount - fields = ["id", "channel_group", "enabled", "auto_channel_sync", "auto_sync_channel_start"] + fields = ["id", "channel_group", "enabled", "auto_channel_sync", "auto_sync_channel_start", "custom_properties"] - # Optionally, if you only need the id of the ChannelGroup, you can customize it like this: - # channel_group = serializers.PrimaryKeyRelatedField(queryset=ChannelGroup.objects.all()) + def to_representation(self, instance): + ret = super().to_representation(instance) + # Ensure custom_properties is always a dict or None + val = ret.get("custom_properties") + if isinstance(val, str): + import json + try: + ret["custom_properties"] = json.loads(val) + except Exception: + ret["custom_properties"] = None + return ret + + def to_internal_value(self, data): + # Accept both dict and JSON string for custom_properties + val = data.get("custom_properties") + if isinstance(val, str): + import json + try: + data["custom_properties"] = json.loads(val) + except Exception: + pass + return super().to_internal_value(data) class RecordingSerializer(serializers.ModelSerializer): diff --git a/apps/m3u/api_views.py b/apps/m3u/api_views.py index 39b9e22e..d3739f19 100644 --- a/apps/m3u/api_views.py +++ b/apps/m3u/api_views.py @@ -15,6 +15,7 @@ import os from rest_framework.decorators import action from django.conf import settings from .tasks import refresh_m3u_groups +import json from .models import M3UAccount, M3UFilter, ServerGroup, M3UAccountProfile from core.models import UserAgent @@ -154,6 +155,7 @@ class M3UAccountViewSet(viewsets.ModelViewSet): enabled = setting.get("enabled", True) auto_sync = setting.get("auto_channel_sync", False) sync_start = setting.get("auto_sync_channel_start") + custom_properties = setting.get("custom_properties", {}) if group_id: ChannelGroupM3UAccount.objects.update_or_create( @@ -163,6 +165,11 @@ class M3UAccountViewSet(viewsets.ModelViewSet): "enabled": enabled, "auto_channel_sync": auto_sync, "auto_sync_channel_start": sync_start, + "custom_properties": ( + custom_properties + if isinstance(custom_properties, str) + else json.dumps(custom_properties) + ), }, ) diff --git a/frontend/src/components/forms/M3UGroupFilter.jsx b/frontend/src/components/forms/M3UGroupFilter.jsx index e0a204ab..b7d095d3 100644 --- a/frontend/src/components/forms/M3UGroupFilter.jsx +++ b/frontend/src/components/forms/M3UGroupFilter.jsx @@ -43,12 +43,26 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => { } setGroupStates( - playlist.channel_groups.map((group) => ({ - ...group, - name: channelGroups[group.channel_group].name, - auto_channel_sync: group.auto_channel_sync || false, - auto_sync_channel_start: group.auto_sync_channel_start || 1.0, - })) + playlist.channel_groups.map((group) => { + // Parse custom_properties if present + let customProps = {}; + if (group.custom_properties) { + try { + customProps = typeof group.custom_properties === 'string' + ? JSON.parse(group.custom_properties) + : group.custom_properties; + } catch (e) { + customProps = {}; + } + } + return { + ...group, + name: channelGroups[group.channel_group].name, + auto_channel_sync: group.auto_channel_sync || false, + auto_sync_channel_start: group.auto_sync_channel_start || 1.0, + custom_properties: customProps, + }; + }) ); }, [playlist, channelGroups]); @@ -79,11 +93,36 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => { ); }; + // Toggle force_dummy_epg in custom_properties for a group + const toggleForceDummyEPG = (id) => { + setGroupStates( + groupStates.map((state) => { + if (state.channel_group == id) { + const customProps = { ...(state.custom_properties || {}) }; + customProps.force_dummy_epg = !customProps.force_dummy_epg; + return { + ...state, + custom_properties: customProps, + }; + } + return state; + }) + ); + }; + const submit = async () => { setIsLoading(true); try { + // Prepare groupStates for API: custom_properties must be stringified + const payload = groupStates.map((state) => ({ + ...state, + custom_properties: state.custom_properties + ? JSON.stringify(state.custom_properties) + : undefined, + })); + // Update group settings via API endpoint - await API.updateM3UGroupSettings(playlist.id, groupStates); + await API.updateM3UGroupSettings(playlist.id, payload); // Show notification about the refresh process notifications.show({ @@ -224,15 +263,25 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => { /> {group.auto_channel_sync && group.enabled && ( - updateChannelStart(group.channel_group, value)} - min={1} - step={1} - size="xs" - precision={1} - /> + <> + updateChannelStart(group.channel_group, value)} + min={1} + step={1} + size="xs" + precision={1} + /> + + {/* Force Dummy EPG Checkbox */} + toggleForceDummyEPG(group.channel_group)} + size="xs" + /> + )}