From c62e3c07535694185dbf4bfa84ae306126682fe1 Mon Sep 17 00:00:00 2001 From: Igor Monadical Date: Wed, 17 Dec 2025 09:51:55 -0500 Subject: [PATCH] incorporate daily api undocumented feature (#796) Co-authored-by: Igor Loskutov --- server/reflector/dailyco_api/__init__.py | 2 ++ server/reflector/dailyco_api/responses.py | 28 ++++++++++++++++++-- server/reflector/worker/process.py | 32 +++++++++++++++++++---- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/server/reflector/dailyco_api/__init__.py b/server/reflector/dailyco_api/__init__.py index 8ef95274..65be426e 100644 --- a/server/reflector/dailyco_api/__init__.py +++ b/server/reflector/dailyco_api/__init__.py @@ -18,6 +18,7 @@ from .requests import ( # Response models from .responses import ( + FinishedRecordingResponse, MeetingParticipant, MeetingParticipantsResponse, MeetingResponse, @@ -79,6 +80,7 @@ __all__ = [ "MeetingParticipant", "MeetingResponse", "RecordingResponse", + "FinishedRecordingResponse", "RecordingS3Info", "MeetingTokenResponse", "WebhookResponse", diff --git a/server/reflector/dailyco_api/responses.py b/server/reflector/dailyco_api/responses.py index 279682ae..6ac95188 100644 --- a/server/reflector/dailyco_api/responses.py +++ b/server/reflector/dailyco_api/responses.py @@ -121,7 +121,10 @@ class RecordingS3Info(BaseModel): class RecordingResponse(BaseModel): """ - Response from recording retrieval endpoint. + Response from recording retrieval endpoint (network layer). + + Duration may be None for recordings still being processed by Daily. + Use FinishedRecordingResponse for recordings ready for processing. Reference: https://docs.daily.co/reference/rest-api/recordings """ @@ -135,7 +138,9 @@ class RecordingResponse(BaseModel): max_participants: int | None = Field( None, description="Maximum participants during recording (may be missing)" ) - duration: int = Field(description="Recording duration in seconds") + duration: int | None = Field( + None, description="Recording duration in seconds (None if still processing)" + ) share_token: NonEmptyString | None = Field( None, description="Token for sharing recording" ) @@ -149,6 +154,25 @@ class RecordingResponse(BaseModel): None, description="Meeting session identifier (may be missing)" ) + def to_finished(self) -> "FinishedRecordingResponse | None": + """Convert to FinishedRecordingResponse if duration is available and status is finished.""" + if self.duration is None or self.status != "finished": + return None + return FinishedRecordingResponse(**self.model_dump()) + + +class FinishedRecordingResponse(RecordingResponse): + """ + Recording with confirmed duration - ready for processing. + + This model guarantees duration is present and status is finished. + """ + + status: Literal["finished"] = Field( + description="Recording status (always 'finished')" + ) + duration: int = Field(description="Recording duration in seconds") + class MeetingTokenResponse(BaseModel): """ diff --git a/server/reflector/worker/process.py b/server/reflector/worker/process.py index 21e73723..142d8dc1 100644 --- a/server/reflector/worker/process.py +++ b/server/reflector/worker/process.py @@ -12,7 +12,7 @@ from celery import shared_task from celery.utils.log import get_task_logger from pydantic import ValidationError -from reflector.dailyco_api import RecordingResponse +from reflector.dailyco_api import FinishedRecordingResponse, RecordingResponse from reflector.db.daily_participant_sessions import ( DailyParticipantSession, daily_participant_sessions_controller, @@ -322,16 +322,38 @@ async def poll_daily_recordings(): ) return - recording_ids = [rec.id for rec in api_recordings] + finished_recordings: List[FinishedRecordingResponse] = [] + for rec in api_recordings: + finished = rec.to_finished() + if finished is None: + logger.debug( + "Skipping unfinished recording", + recording_id=rec.id, + room_name=rec.room_name, + status=rec.status, + ) + continue + finished_recordings.append(finished) + + if not finished_recordings: + logger.debug( + "No finished recordings found from Daily.co API", + total_api_count=len(api_recordings), + ) + return + + recording_ids = [rec.id for rec in finished_recordings] existing_recordings = await recordings_controller.get_by_ids(recording_ids) existing_ids = {rec.id for rec in existing_recordings} - missing_recordings = [rec for rec in api_recordings if rec.id not in existing_ids] + missing_recordings = [ + rec for rec in finished_recordings if rec.id not in existing_ids + ] if not missing_recordings: logger.debug( "All recordings already in DB", - api_count=len(api_recordings), + api_count=len(finished_recordings), existing_count=len(existing_recordings), ) return @@ -339,7 +361,7 @@ async def poll_daily_recordings(): logger.info( "Found recordings missing from DB", missing_count=len(missing_recordings), - total_api_count=len(api_recordings), + total_api_count=len(finished_recordings), existing_count=len(existing_recordings), )