Extend an in-progress recording's end_time without interrupting the stream. The running task re-reads end_time every ~2 s and adjusts its deadline dynamically. The pre_save signal skips task revocation while the recording status is 'recording'.
(self, request, pk=None)
| 3579 | |
| 3580 | @action(detail=True, methods=["post"], url_path="extend") |
| 3581 | def extend(self, request, pk=None): |
| 3582 | """Extend an in-progress recording's end_time without interrupting the stream. |
| 3583 | |
| 3584 | The running task re-reads end_time every ~2 s and adjusts its deadline |
| 3585 | dynamically. The pre_save signal skips task revocation while the |
| 3586 | recording status is 'recording'. |
| 3587 | """ |
| 3588 | instance = self.get_object() |
| 3589 | cp = instance.custom_properties or {} |
| 3590 | |
| 3591 | if cp.get("status") in ("completed", "stopped", "interrupted"): |
| 3592 | return Response( |
| 3593 | {"success": False, "error": "Recording has already finished"}, |
| 3594 | status=status.HTTP_400_BAD_REQUEST, |
| 3595 | ) |
| 3596 | |
| 3597 | try: |
| 3598 | extra_minutes = int(request.data.get("extra_minutes", 0)) |
| 3599 | except (TypeError, ValueError): |
| 3600 | extra_minutes = 0 |
| 3601 | |
| 3602 | if extra_minutes <= 0: |
| 3603 | return Response( |
| 3604 | {"success": False, "error": "extra_minutes must be a positive integer"}, |
| 3605 | status=status.HTTP_400_BAD_REQUEST, |
| 3606 | ) |
| 3607 | |
| 3608 | new_end_time = instance.end_time + timedelta(minutes=extra_minutes) |
| 3609 | # Use queryset .update() to bypass pre_save/post_save signals. |
| 3610 | # This avoids the pre_save signal revoking the scheduled/running |
| 3611 | # Celery task. The running task's 2-second polling loop re-reads |
| 3612 | # end_time from the DB and extends its deadline dynamically. |
| 3613 | # If the task hasn't started yet (still in Beat's queue), it will |
| 3614 | # read the updated end_time from the DB on its first poll cycle. |
| 3615 | Recording.objects.filter(pk=instance.pk).update(end_time=new_end_time) |
| 3616 | |
| 3617 | try: |
| 3618 | from core.utils import send_websocket_update |
| 3619 | send_websocket_update('updates', 'update', { |
| 3620 | "success": True, |
| 3621 | "type": "recording_extended", |
| 3622 | "recording_id": instance.id, |
| 3623 | "new_end_time": new_end_time.isoformat(), |
| 3624 | "extra_minutes": extra_minutes, |
| 3625 | "channel": instance.channel.name, |
| 3626 | }) |
| 3627 | except Exception: |
| 3628 | pass |
| 3629 | |
| 3630 | return Response({"success": True, "new_end_time": new_end_time.isoformat()}) |
| 3631 | |
| 3632 | @action(detail=True, methods=["post"], url_path="refresh-artwork") |
| 3633 | def refresh_artwork(self, request, pk=None): |
no test coverage detected