diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index d965d385..2d6ec1d9 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -698,266 +698,6 @@ class ChannelViewSet(viewsets.ModelViewSet): 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 'name'. " - "Supports single profile ID or array of profile IDs in 'channel_profile_ids'." - ), - request_body=openapi.Schema( - type=openapi.TYPE_ARRAY, - items=openapi.Schema( - type=openapi.TYPE_OBJECT, - required=["stream_id"], - properties={ - "stream_id": openapi.Schema( - type=openapi.TYPE_INTEGER, - description="ID of the stream to link", - ), - "channel_number": openapi.Schema( - type=openapi.TYPE_NUMBER, - description="(Optional) Desired channel number. Must not be in use.", - ), - "name": openapi.Schema( - type=openapi.TYPE_STRING, description="Desired channel name" - ), - "channel_profile_ids": openapi.Schema( - type=openapi.TYPE_ARRAY, - items=openapi.Items(type=openapi.TYPE_INTEGER), - description="(Optional) Channel profile ID(s) to add the channel to. Can be a single ID or array of IDs." - ), - }, - ), - ), - 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 - - logos_to_create = [] - channels_to_create = [] - streams_map = [] - logo_map = [] - profile_map = [] # Track which profiles each channel should be added to - - for item in data_list: - stream_id = item.get("stream_id") - if not stream_id: - errors.append( - { - "item": item, - "error": "Missing required field: stream_id is required.", - } - ) - continue - - try: - stream = get_object_or_404(Stream, pk=stream_id) - except Exception as e: - errors.append({"item": item, "error": str(e)}) - continue - - name = item.get("name") - if name is None: - name = stream.name - - channel_group = stream.channel_group - - stream_custom_props = stream.custom_properties or {} - - channel_number = None - if "tvg-chno" in stream_custom_props: - channel_number = float(stream_custom_props["tvg-chno"]) - elif "channel-number" in stream_custom_props: - channel_number = float(stream_custom_props["channel-number"]) - elif "num" in stream_custom_props: - channel_number = float(stream_custom_props["num"]) - # Get the tvc_guide_stationid from custom properties if it exists - tvc_guide_stationid = None - if "tvc-guide-stationid" in stream_custom_props: - tvc_guide_stationid = stream_custom_props["tvc-guide-stationid"] - - # Determine channel number: if provided, use it (if free); else auto assign. - if channel_number is None: - provided_number = item.get("channel_number") - if provided_number is None: - channel_number = get_auto_number() - else: - try: - channel_number = float(provided_number) - except ValueError: - errors.append( - { - "item": item, - "error": "channel_number must be a number.", - } - ) - 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, - "name": name, - "tvc_guide_stationid": tvc_guide_stationid, - "tvg_id": stream.tvg_id, - } - - # Only add channel_group_id if the stream has a channel group - if channel_group: - channel_data["channel_group_id"] = channel_group.id - - # Attempt to find existing EPGs with the same tvg-id - epgs = EPGData.objects.filter(tvg_id=stream.tvg_id) - if epgs: - channel_data["epg_data_id"] = epgs.first().id - - serializer = self.get_serializer(data=channel_data) - if serializer.is_valid(): - validated_data = serializer.validated_data - channel = Channel(**validated_data) - channels_to_create.append(channel) - - streams_map.append([stream_id]) - # Store which profiles this channel should be added to - normalize to array - channel_profile_ids = item.get("channel_profile_ids") - if channel_profile_ids is not None: - # Normalize single ID to array - if not isinstance(channel_profile_ids, list): - channel_profile_ids = [channel_profile_ids] - - profile_map.append(channel_profile_ids) - - if stream.logo_url: - logos_to_create.append( - Logo( - url=stream.logo_url, - name=stream.name or stream.tvg_id, - ) - ) - logo_map.append(stream.logo_url) - else: - logo_map.append(None) - - else: - errors.append({"item": item, "error": serializer.errors}) - - if logos_to_create: - Logo.objects.bulk_create(logos_to_create, ignore_conflicts=True) - - channel_logos = { - logo.url: logo - for logo in Logo.objects.filter( - url__in=[url for url in logo_map if url is not None] - ) - } - - # Get all profiles for default assignment - all_profiles = ChannelProfile.objects.all() - channel_profile_memberships = [] - - if channels_to_create: - with transaction.atomic(): - created_channels = Channel.objects.bulk_create(channels_to_create) - - update = [] - for channel, stream_ids, logo_url, channel_profile_ids in zip( - created_channels, streams_map, logo_map, profile_map - ): - if logo_url: - channel.logo = channel_logos[logo_url] - update.append(channel) - - # Handle channel profile membership based on channel_profile_ids - if channel_profile_ids: - # Add channel only to the specified profiles - try: - specific_profiles = ChannelProfile.objects.filter(id__in=channel_profile_ids) - channel_profile_memberships.extend([ - ChannelProfileMembership( - channel_profile=profile, - channel=channel, - enabled=True - ) - for profile in specific_profiles - ]) - except Exception: - # If profiles don't exist, add to all profiles as fallback - channel_profile_memberships.extend([ - ChannelProfileMembership( - channel_profile=profile, - channel=channel, - enabled=True - ) - for profile in all_profiles - ]) - else: - # Default behavior: add to all profiles - channel_profile_memberships.extend([ - ChannelProfileMembership( - channel_profile=profile, - channel=channel, - enabled=True - ) - for profile in all_profiles - ]) - - # Bulk create profile memberships - if channel_profile_memberships: - ChannelProfileMembership.objects.bulk_create( - channel_profile_memberships - ) - - # Update logos - if update: - Channel.objects.bulk_update(update, ["logo"]) - - # Set stream relationships - for channel, stream_ids in zip(created_channels, streams_map): - channel.streams.set(stream_ids) - - response_data = {"created": ChannelSerializer(created_channels, many=True).data} - if errors: - response_data["errors"] = errors - - return Response(response_data, status=status.HTTP_201_CREATED) - @swagger_auto_schema( method="post", operation_description=( @@ -983,8 +723,8 @@ class ChannelViewSet(viewsets.ModelViewSet): ), responses={202: "Task started successfully"}, ) - @action(detail=False, methods=["post"], url_path="from-stream/bulk-async") - def from_stream_bulk_async(self, request): + @action(detail=False, methods=["post"], url_path="from-stream/bulk") + def from_stream_bulk(self, request): from .tasks import bulk_create_channels_from_streams stream_ids = request.data.get("stream_ids", []) diff --git a/frontend/src/api.js b/frontend/src/api.js index 447a0d0a..282a57a1 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -560,7 +560,7 @@ export default class API { } const response = await request( - `${host}/api/channels/channels/from-stream/bulk-async/`, + `${host}/api/channels/channels/from-stream/bulk/`, { method: 'POST', body: requestBody,