mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
289 lines
13 KiB
Python
289 lines
13 KiB
Python
from rest_framework import viewsets, status
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.decorators import action
|
|
from drf_yasg.utils import swagger_auto_schema
|
|
from drf_yasg import openapi
|
|
from django.shortcuts import get_object_or_404
|
|
|
|
from .models import Stream, Channel, ChannelGroup
|
|
from .serializers import StreamSerializer, ChannelSerializer, ChannelGroupSerializer
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 1) Stream API (CRUD)
|
|
# ─────────────────────────────────────────────────────────
|
|
class StreamViewSet(viewsets.ModelViewSet):
|
|
queryset = Stream.objects.all()
|
|
serializer_class = StreamSerializer
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
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:
|
|
qs = qs.filter(channels__id=assigned)
|
|
unassigned = self.request.query_params.get('unassigned')
|
|
if unassigned == '1':
|
|
qs = qs.filter(channels__isnull=True)
|
|
return qs
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 2) Channel Group Management (CRUD)
|
|
# ─────────────────────────────────────────────────────────
|
|
class ChannelGroupViewSet(viewsets.ModelViewSet):
|
|
queryset = ChannelGroup.objects.all()
|
|
serializer_class = ChannelGroupSerializer
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 3) Channel Management (CRUD)
|
|
# ─────────────────────────────────────────────────────────
|
|
class ChannelViewSet(viewsets.ModelViewSet):
|
|
queryset = Channel.objects.all()
|
|
serializer_class = ChannelSerializer
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get_next_available_channel_number(self, starting_from=1):
|
|
used_numbers = set(Channel.objects.all().values_list('channel_number', flat=True))
|
|
n = starting_from
|
|
while n in used_numbers:
|
|
n += 1
|
|
return n
|
|
|
|
@swagger_auto_schema(
|
|
method='post',
|
|
operation_description="Auto-assign channel_number in bulk by an ordered list of channel IDs.",
|
|
request_body=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
required=["channel_order"],
|
|
properties={
|
|
"channel_order": openapi.Schema(
|
|
type=openapi.TYPE_ARRAY,
|
|
items=openapi.Items(type=openapi.TYPE_INTEGER),
|
|
description="List of channel IDs in the new order"
|
|
)
|
|
}
|
|
),
|
|
responses={200: "Channels have been auto-assigned!"}
|
|
)
|
|
@action(detail=False, methods=['post'], url_path='assign')
|
|
def assign(self, request):
|
|
channel_order = request.data.get('channel_order', [])
|
|
for order, channel_id in enumerate(channel_order, start=1):
|
|
Channel.objects.filter(id=channel_id).update(channel_number=order)
|
|
return Response({"message": "Channels have been auto-assigned!"}, status=status.HTTP_200_OK)
|
|
|
|
@swagger_auto_schema(
|
|
method='post',
|
|
operation_description=(
|
|
"Create a new channel from an existing stream. "
|
|
"If 'channel_number' is provided, it will be used (if available); "
|
|
"otherwise, the next available channel number is assigned."
|
|
),
|
|
request_body=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
required=["stream_id", "channel_name"],
|
|
properties={
|
|
"stream_id": openapi.Schema(
|
|
type=openapi.TYPE_INTEGER, description="ID of the stream to link"
|
|
),
|
|
"channel_number": openapi.Schema(
|
|
type=openapi.TYPE_INTEGER,
|
|
description="(Optional) Desired channel number. Must not be in use."
|
|
),
|
|
"channel_name": openapi.Schema(
|
|
type=openapi.TYPE_STRING, description="Desired channel name"
|
|
)
|
|
}
|
|
),
|
|
responses={201: ChannelSerializer()}
|
|
)
|
|
@action(detail=False, methods=['post'], url_path='from-stream')
|
|
def from_stream(self, request):
|
|
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)
|
|
channel_group, _ = ChannelGroup.objects.get_or_create(name=stream.group_name)
|
|
|
|
# Check if client provided a channel_number; if not, auto-assign one.
|
|
provided_number = request.data.get('channel_number')
|
|
if provided_number is None:
|
|
channel_number = self.get_next_available_channel_number()
|
|
else:
|
|
try:
|
|
channel_number = int(provided_number)
|
|
except ValueError:
|
|
return Response({"error": "channel_number must be an integer."}, status=status.HTTP_400_BAD_REQUEST)
|
|
# If the provided number is already used, return an error.
|
|
if Channel.objects.filter(channel_number=channel_number).exists():
|
|
return Response(
|
|
{"error": f"Channel number {channel_number} is already in use. Please choose a different number."},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
channel_data = {
|
|
'channel_number': channel_number,
|
|
'channel_name': request.data.get('channel_name', f"Channel from {stream.name}"),
|
|
'tvg_id': stream.tvg_id,
|
|
'channel_group_id': channel_group.id,
|
|
'logo_url': stream.logo_url,
|
|
}
|
|
serializer = self.get_serializer(data=channel_data)
|
|
serializer.is_valid(raise_exception=True)
|
|
channel = serializer.save()
|
|
channel.streams.add(stream)
|
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
|
|
@swagger_auto_schema(
|
|
method='post',
|
|
operation_description=(
|
|
"Bulk create channels from existing streams. For each object, if 'channel_number' is provided, "
|
|
"it is used (if available); otherwise, the next available number is auto-assigned. "
|
|
"Each object must include 'stream_id' and 'channel_name'."
|
|
),
|
|
request_body=openapi.Schema(
|
|
type=openapi.TYPE_ARRAY,
|
|
items=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
required=["stream_id", "channel_name"],
|
|
properties={
|
|
"stream_id": openapi.Schema(
|
|
type=openapi.TYPE_INTEGER, description="ID of the stream to link"
|
|
),
|
|
"channel_number": openapi.Schema(
|
|
type=openapi.TYPE_INTEGER,
|
|
description="(Optional) Desired channel number. Must not be in use."
|
|
),
|
|
"channel_name": openapi.Schema(
|
|
type=openapi.TYPE_STRING, description="Desired channel name"
|
|
)
|
|
}
|
|
)
|
|
),
|
|
responses={201: "Bulk channels created"}
|
|
)
|
|
@action(detail=False, methods=['post'], url_path='from-stream/bulk')
|
|
def from_stream_bulk(self, request):
|
|
data_list = request.data
|
|
if not isinstance(data_list, list):
|
|
return Response({"error": "Expected a list of channel objects"}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
created_channels = []
|
|
errors = []
|
|
|
|
# Gather current used numbers once.
|
|
used_numbers = set(Channel.objects.all().values_list('channel_number', flat=True))
|
|
next_number = 1
|
|
def get_auto_number():
|
|
nonlocal next_number
|
|
while next_number in used_numbers:
|
|
next_number += 1
|
|
used_numbers.add(next_number)
|
|
return next_number
|
|
|
|
for item in data_list:
|
|
stream_id = item.get('stream_id')
|
|
channel_name = item.get('channel_name')
|
|
if not all([stream_id, channel_name]):
|
|
errors.append({"item": item, "error": "Missing required fields: stream_id and channel_name are required."})
|
|
continue
|
|
|
|
try:
|
|
stream = get_object_or_404(Stream, pk=stream_id)
|
|
except Exception as e:
|
|
errors.append({"item": item, "error": str(e)})
|
|
continue
|
|
|
|
channel_group, _ = ChannelGroup.objects.get_or_create(name=stream.group_name)
|
|
|
|
# Determine channel number: if provided, use it (if free); else auto assign.
|
|
provided_number = item.get('channel_number')
|
|
if provided_number is None:
|
|
channel_number = get_auto_number()
|
|
else:
|
|
try:
|
|
channel_number = int(provided_number)
|
|
except ValueError:
|
|
errors.append({"item": item, "error": "channel_number must be an integer."})
|
|
continue
|
|
if channel_number in used_numbers or Channel.objects.filter(channel_number=channel_number).exists():
|
|
errors.append({"item": item, "error": f"Channel number {channel_number} is already in use."})
|
|
continue
|
|
used_numbers.add(channel_number)
|
|
|
|
channel_data = {
|
|
"channel_number": channel_number,
|
|
"channel_name": channel_name,
|
|
"tvg_id": stream.tvg_id,
|
|
"channel_group_id": channel_group.id,
|
|
"logo_url": stream.logo_url,
|
|
}
|
|
serializer = self.get_serializer(data=channel_data)
|
|
if serializer.is_valid():
|
|
channel = serializer.save()
|
|
channel.streams.add(stream)
|
|
created_channels.append(serializer.data)
|
|
else:
|
|
errors.append({"item": item, "error": serializer.errors})
|
|
|
|
response_data = {"created": created_channels}
|
|
if errors:
|
|
response_data["errors"] = errors
|
|
|
|
return Response(response_data, status=status.HTTP_201_CREATED)
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 4) Bulk Delete Streams
|
|
# ─────────────────────────────────────────────────────────
|
|
class BulkDeleteStreamsAPIView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
@swagger_auto_schema(
|
|
operation_description="Bulk delete streams by ID",
|
|
request_body=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
required=["stream_ids"],
|
|
properties={
|
|
"stream_ids": openapi.Schema(
|
|
type=openapi.TYPE_ARRAY,
|
|
items=openapi.Items(type=openapi.TYPE_INTEGER),
|
|
description="Stream IDs to delete"
|
|
)
|
|
},
|
|
),
|
|
responses={204: "Streams deleted"}
|
|
)
|
|
def delete(self, request, *args, **kwargs):
|
|
stream_ids = request.data.get('stream_ids', [])
|
|
Stream.objects.filter(id__in=stream_ids).delete()
|
|
return Response({"message": "Streams deleted successfully!"}, status=status.HTTP_204_NO_CONTENT)
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# 5) Bulk Delete Channels
|
|
# ─────────────────────────────────────────────────────────
|
|
class BulkDeleteChannelsAPIView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
@swagger_auto_schema(
|
|
operation_description="Bulk delete channels by ID",
|
|
request_body=openapi.Schema(
|
|
type=openapi.TYPE_OBJECT,
|
|
required=["channel_ids"],
|
|
properties={
|
|
"channel_ids": openapi.Schema(
|
|
type=openapi.TYPE_ARRAY,
|
|
items=openapi.Items(type=openapi.TYPE_INTEGER),
|
|
description="Channel IDs to delete"
|
|
)
|
|
},
|
|
),
|
|
responses={204: "Channels deleted"}
|
|
)
|
|
def destroy(self, request):
|
|
channel_ids = request.data.get('channel_ids', [])
|
|
Channel.objects.filter(id__in=channel_ids).delete()
|
|
return Response({"message": "Channels deleted"}, status=status.HTTP_204_NO_CONTENT)
|