From 128ab162103fda7cb2cf8798c4f2e55268ea7732 Mon Sep 17 00:00:00 2001 From: Dispatcharr Date: Tue, 25 Feb 2025 14:42:33 -0600 Subject: [PATCH] Pre Alpha v5 Added tvg-id's to channels and streams --- apps/channels/admin.py | 4 ++-- apps/channels/api_views.py | 20 ++++++++------------ apps/channels/apps.py | 4 ++++ apps/channels/forms.py | 1 - apps/channels/models.py | 1 - apps/channels/serializers.py | 1 - apps/channels/signals.py | 16 ++++++++++++++++ apps/m3u/tasks.py | 29 ++++++++++++++++++++++++----- 8 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 apps/channels/signals.py diff --git a/apps/channels/admin.py b/apps/channels/admin.py index 8a3d38d6..d4eb7fcb 100644 --- a/apps/channels/admin.py +++ b/apps/channels/admin.py @@ -5,9 +5,9 @@ from .models import Stream, Channel, ChannelGroup class StreamAdmin(admin.ModelAdmin): list_display = ( 'id', 'name', 'group_name', 'custom_url', - 'current_viewers', 'is_transcoded', 'updated_at', + 'current_viewers', 'updated_at', ) - list_filter = ('group_name', 'is_transcoded') + list_filter = ('group_name',) search_fields = ('name', 'custom_url', 'group_name') ordering = ('-updated_at',) diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index 46e1109d..cd14fa7f 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -10,7 +10,6 @@ from django.shortcuts import get_object_or_404 from .models import Stream, Channel, ChannelGroup from .serializers import StreamSerializer, ChannelSerializer, ChannelGroupSerializer - # ───────────────────────────────────────────────────────── # 1) Stream API (CRUD) # ───────────────────────────────────────────────────────── @@ -21,20 +20,19 @@ class StreamViewSet(viewsets.ModelViewSet): def get_queryset(self): qs = super().get_queryset() - + # Exclude streams from inactive M3U accounts + qs = qs.exclude(m3u_account__is_active=False) + assigned = self.request.query_params.get('assigned') if assigned is not None: - # Streams that belong to a given channel? qs = qs.filter(channels__id=assigned) - + unassigned = self.request.query_params.get('unassigned') if unassigned == '1': - # Streams that are not linked to any channel qs = qs.filter(channels__isnull=True) - + return qs - # ───────────────────────────────────────────────────────── # 2) Channel Group Management (CRUD) # ───────────────────────────────────────────────────────── @@ -43,7 +41,6 @@ class ChannelGroupViewSet(viewsets.ModelViewSet): serializer_class = ChannelGroupSerializer permission_classes = [IsAuthenticated] - # ───────────────────────────────────────────────────────── # 3) Channel Management (CRUD) # ───────────────────────────────────────────────────────── @@ -103,22 +100,22 @@ class ChannelViewSet(viewsets.ModelViewSet): stream_id = request.data.get('stream_id') if not stream_id: return Response({"error": "Missing stream_id"}, status=status.HTTP_400_BAD_REQUEST) - stream = get_object_or_404(Stream, pk=stream_id) + # Include the stream's tvg_id in the channel data channel_data = { 'channel_number': request.data.get('channel_number', 0), 'channel_name': request.data.get('channel_name', f"Channel from {stream.name}"), + 'tvg_id': stream.tvg_id, # Inherit tvg-id from the stream } serializer = self.get_serializer(data=channel_data) serializer.is_valid(raise_exception=True) channel = serializer.save() - # Optionally attach the stream to that channel + # Optionally attach the stream to the channel channel.streams.add(stream) return Response(serializer.data, status=status.HTTP_201_CREATED) - # ───────────────────────────────────────────────────────── # 4) Bulk Delete Streams # ───────────────────────────────────────────────────────── @@ -145,7 +142,6 @@ class BulkDeleteStreamsAPIView(APIView): Stream.objects.filter(id__in=stream_ids).delete() return Response({"message": "Streams deleted successfully!"}, status=status.HTTP_204_NO_CONTENT) - # ───────────────────────────────────────────────────────── # 5) Bulk Delete Channels # ───────────────────────────────────────────────────────── diff --git a/apps/channels/apps.py b/apps/channels/apps.py index 28bb8ca8..bcca01ee 100644 --- a/apps/channels/apps.py +++ b/apps/channels/apps.py @@ -4,3 +4,7 @@ class ChannelsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'apps.channels' verbose_name = "Channel & Stream Management" + + def ready(self): + # Import signals so they get registered. + import apps.channels.signals diff --git a/apps/channels/forms.py b/apps/channels/forms.py index 37d28916..171c77d9 100644 --- a/apps/channels/forms.py +++ b/apps/channels/forms.py @@ -43,6 +43,5 @@ class StreamForm(forms.ModelForm): 'logo_url', 'tvg_id', 'local_file', - 'is_transcoded', 'group_name', ] diff --git a/apps/channels/models.py b/apps/channels/models.py index fdeba659..b965e32d 100644 --- a/apps/channels/models.py +++ b/apps/channels/models.py @@ -23,7 +23,6 @@ class Stream(models.Model): tvg_id = models.CharField(max_length=255, blank=True, null=True) local_file = models.FileField(upload_to='uploads/', blank=True, null=True) current_viewers = models.PositiveIntegerField(default=0) - is_transcoded = models.BooleanField(default=False) updated_at = models.DateTimeField(auto_now=True) group_name = models.CharField(max_length=255, blank=True, null=True) stream_profile = models.ForeignKey( diff --git a/apps/channels/serializers.py b/apps/channels/serializers.py index 9e53abe7..5494196d 100644 --- a/apps/channels/serializers.py +++ b/apps/channels/serializers.py @@ -24,7 +24,6 @@ class StreamSerializer(serializers.ModelSerializer): 'tvg_id', 'local_file', 'current_viewers', - 'is_transcoded', 'updated_at', 'group_name', 'stream_profile_id', diff --git a/apps/channels/signals.py b/apps/channels/signals.py new file mode 100644 index 00000000..e36b4829 --- /dev/null +++ b/apps/channels/signals.py @@ -0,0 +1,16 @@ +from django.db.models.signals import m2m_changed +from django.dispatch import receiver +from .models import Channel, Stream + +@receiver(m2m_changed, sender=Channel.streams.through) +def update_channel_tvg_id(sender, instance, action, reverse, model, pk_set, **kwargs): + # When streams are added to a channel... + if action == "post_add": + # If the channel does not already have a tvg-id... + if not instance.tvg_id: + # Look for any of the newly added streams that have a nonempty tvg_id. + streams_with_tvg = model.objects.filter(pk__in=pk_set).exclude(tvg_id__exact='') + if streams_with_tvg.exists(): + # Update the channel's tvg_id with the first found tvg_id. + instance.tvg_id = streams_with_tvg.first().tvg_id + instance.save(update_fields=['tvg_id']) diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py index c4bcfd66..d39a6511 100644 --- a/apps/m3u/tasks.py +++ b/apps/m3u/tasks.py @@ -117,14 +117,23 @@ def refresh_single_m3u_account(account_id): if line.startswith('#EXTINF'): tvg_name_match = re.search(r'tvg-name="([^"]*)"', line) tvg_logo_match = re.search(r'tvg-logo="([^"]*)"', line) + # Extract tvg-id + tvg_id_match = re.search(r'tvg-id="([^"]*)"', line) + tvg_id = tvg_id_match.group(1) if tvg_id_match else "" + fallback_name = line.split(",", 1)[-1].strip() if "," in line else "Default Stream" name = tvg_name_match.group(1) if tvg_name_match else fallback_name logo_url = tvg_logo_match.group(1) if tvg_logo_match else "" group_title = _get_group_title(line) - logger.debug(f"Parsed EXTINF: name={name}, logo_url={logo_url}, group_title={group_title}") - current_info = {"name": name, "logo_url": logo_url, "group_title": group_title} + logger.debug(f"Parsed EXTINF: name={name}, logo_url={logo_url}, tvg_id={tvg_id}, group_title={group_title}") + current_info = { + "name": name, + "logo_url": logo_url, + "group_title": group_title, + "tvg_id": tvg_id, # save the tvg-id here + } elif current_info and line.startswith('http'): lower_line = line.lower() @@ -145,7 +154,11 @@ def refresh_single_m3u_account(account_id): current_info = None continue - defaults = {"logo_url": current_info["logo_url"]} + # Include tvg_id in the defaults so it gets saved + defaults = { + "logo_url": current_info["logo_url"], + "tvg_id": current_info["tvg_id"] + } try: obj, created = Stream.objects.update_or_create( name=current_info["name"], @@ -203,11 +216,13 @@ def parse_m3u_file(file_path, account): tvg_name_match = re.search(r'tvg-name="([^"]*)"', line) tvg_logo_match = re.search(r'tvg-logo="([^"]*)"', line) fallback_name = line.split(",", 1)[-1].strip() if "," in line else "Stream" + tvg_id_match = re.search(r'tvg-id="([^"]*)"', line) + tvg_id = tvg_id_match.group(1) if tvg_id_match else "" name = tvg_name_match.group(1) if tvg_name_match else fallback_name logo_url = tvg_logo_match.group(1) if tvg_logo_match else "" - current_info = {"name": name, "logo_url": logo_url} + current_info = {"name": name, "logo_url": logo_url, "tvg_id": tvg_id} elif current_info and line.startswith('http'): lower_line = line.lower() @@ -216,7 +231,11 @@ def parse_m3u_file(file_path, account): current_info = None continue - defaults = {"logo_url": current_info["logo_url"]} + defaults = { + "logo_url": current_info["logo_url"], + "tvg_id": current_info.get("tvg_id", "") + } + try: obj, created = Stream.objects.update_or_create( name=current_info["name"],