Manually re-pack visible channels in one of this account's groups into the group's [start, end] range. Override-pinned numbers are treated as reservations and skipped. Hidden channels without overrides have their channel_number set to NULL. Useful when the u
(self, request, pk=None)
| 390 | ) |
| 391 | @action(detail=True, methods=["post"], url_path="repack-group") |
| 392 | def repack_group(self, request, pk=None): |
| 393 | """ |
| 394 | Manually re-pack visible channels in one of this account's |
| 395 | groups into the group's [start, end] range. Override-pinned |
| 396 | numbers are treated as reservations and skipped. Hidden channels |
| 397 | without overrides have their channel_number set to NULL. |
| 398 | |
| 399 | Useful when the user has just finished customizing channels |
| 400 | (setting overrides as pins, hiding unwanted streams) and wants |
| 401 | the result reflected immediately rather than on the next M3U |
| 402 | refresh. Also acts as a one-shot cleanup for groups that aren't |
| 403 | running in compact mode but have accumulated gaps. |
| 404 | """ |
| 405 | account = self.get_object() |
| 406 | group_id_raw = request.query_params.get("channel_group_id") |
| 407 | if not group_id_raw: |
| 408 | return Response( |
| 409 | {"detail": "channel_group_id is required"}, |
| 410 | status=status.HTTP_400_BAD_REQUEST, |
| 411 | ) |
| 412 | try: |
| 413 | group_id = int(group_id_raw) |
| 414 | except (TypeError, ValueError): |
| 415 | return Response( |
| 416 | {"detail": "channel_group_id must be an integer"}, |
| 417 | status=status.HTTP_400_BAD_REQUEST, |
| 418 | ) |
| 419 | |
| 420 | from apps.channels.models import ChannelGroupM3UAccount |
| 421 | from apps.channels.compact_numbering import repack_group as _repack |
| 422 | from core.utils import acquire_task_lock, release_task_lock |
| 423 | |
| 424 | # Share the lock that wraps the entire refresh-plus-sync pipeline |
| 425 | # (`refresh_single_m3u_account`). The narrower |
| 426 | # `refresh_m3u_account_groups` lock is released before |
| 427 | # `sync_auto_channels` runs, so it would not protect this writer |
| 428 | # from racing against the channel_number writes inside sync. |
| 429 | if not acquire_task_lock("refresh_single_m3u_account", account.id): |
| 430 | return Response( |
| 431 | {"detail": "An M3U refresh is in progress for this account."}, |
| 432 | status=status.HTTP_409_CONFLICT, |
| 433 | ) |
| 434 | try: |
| 435 | # Re-fetch under the lock so a sync that just released its lock |
| 436 | # cannot leave the cached group_relation reflecting pre-sync |
| 437 | # custom_properties (auto_sync_channel_start/end, etc.). |
| 438 | try: |
| 439 | group_relation = ChannelGroupM3UAccount.objects.get( |
| 440 | m3u_account=account, channel_group_id=group_id |
| 441 | ) |
| 442 | except ChannelGroupM3UAccount.DoesNotExist: |
| 443 | return Response( |
| 444 | {"detail": "Group is not associated with this account"}, |
| 445 | status=status.HTTP_404_NOT_FOUND, |
| 446 | ) |
| 447 | result = _repack(group_relation) |
| 448 | finally: |
| 449 | try: |
nothing calls this directly
no test coverage detected