Pre Alpha v5

Added tvg-id's to channels and streams
This commit is contained in:
Dispatcharr 2025-02-25 14:42:33 -06:00
parent d89bf35c0d
commit 128ab16210
8 changed files with 54 additions and 22 deletions

View file

@ -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',)

View file

@ -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
# ─────────────────────────────────────────────────────────

View file

@ -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

View file

@ -43,6 +43,5 @@ class StreamForm(forms.ModelForm):
'logo_url',
'tvg_id',
'local_file',
'is_transcoded',
'group_name',
]

View file

@ -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(

View file

@ -24,7 +24,6 @@ class StreamSerializer(serializers.ModelSerializer):
'tvg_id',
'local_file',
'current_viewers',
'is_transcoded',
'updated_at',
'group_name',
'stream_profile_id',

16
apps/channels/signals.py Normal file
View file

@ -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'])

View file

@ -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"],