From 7c63d3128912d1e97ce41d3da2452e15ec86dd10 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Tue, 17 Jun 2025 20:42:18 +0530 Subject: [PATCH 1/7] Add meeting support --- videodb/_constants.py | 2 ++ videodb/client.py | 41 +++++++++++++++++++++++++++++++++++++++++ videodb/collection.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/videodb/_constants.py b/videodb/_constants.py index b98ddab..025749f 100644 --- a/videodb/_constants.py +++ b/videodb/_constants.py @@ -81,6 +81,8 @@ class ApiPath: translate = "translate" dub = "dub" transcode = "transcode" + meeting = "meeting" + record = "record" class Status: diff --git a/videodb/client.py b/videodb/client.py index 25ae399..f74c928 100644 --- a/videodb/client.py +++ b/videodb/client.py @@ -290,3 +290,44 @@ def upload( return Audio(self, **upload_data) elif media_id.startswith("img-"): return Image(self, **upload_data) + + def record_meeting( + self, + link: str, + bot_name: str, + meeting_name: str, + callback_url: str, + time_zone: str = "UTC", + ) -> dict: + """Record a meeting and upload it to the default collection. + + :param str link: Meeting link + :param str bot_name: Name of the recorder bot + :param str meeting_name: Name of the meeting + :param str callback_url: URL to receive callback once recording is done + :param str time_zone: Time zone for the meeting (default ``UTC``) + :return: Response data from the API + :rtype: dict + """ + + response = self.post( + path=f"{ApiPath.collection}/default/{ApiPath.meeting}/{ApiPath.record}", + data={ + "link": link, + "bot_name": bot_name, + "meeting_name": meeting_name, + "callback_url": callback_url, + "time_zone": time_zone, + }, + ) + return response + + def get_meeting_info(self, bot_id: str) -> dict: + """Get the information of a given meeting bot. + + :param str bot_id: ID returned when recording was initiated + :return: Information of the meeting bot + :rtype: dict + """ + + return self.get(path=f"{ApiPath.collection}/default/{ApiPath.meeting}/{bot_id}") diff --git a/videodb/collection.py b/videodb/collection.py index e941cf4..d69d0a9 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -484,3 +484,45 @@ def make_private(self): path=f"{ApiPath.collection}/{self.id}", data={"is_public": False} ) self.is_public = False + + def record_meeting( + self, + link: str, + bot_name: str, + meeting_name: str, + callback_url: str, + time_zone: str = "UTC", + ) -> dict: + """Record a meeting and upload it to this collection. + + :param str link: Meeting link + :param str bot_name: Name of the recorder bot + :param str meeting_name: Name of the meeting + :param str callback_url: URL to receive callback once recording is done + :param str time_zone: Time zone for the meeting (default ``UTC``) + :return: Response data from the API + :rtype: dict + """ + + return self._connection.post( + path=f"/collection/{self.id}/{ApiPath.meeting}/{ApiPath.record}", + data={ + "link": link, + "bot_name": bot_name, + "meeting_name": meeting_name, + "callback_url": callback_url, + "time_zone": time_zone, + }, + ) + + def get_meeting_info(self, bot_id: str) -> dict: + """Get the recording info for a meeting bot in this collection. + + :param str bot_id: ID returned when recording was initiated + :return: Information of the meeting bot + :rtype: dict + """ + + return self._connection.get( + path=f"{ApiPath.collection}/{self.id}/{ApiPath.meeting}/{bot_id}" + ) From 85ec10724f1b885e022f14a4de2297a3ad524b23 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Tue, 24 Jun 2025 11:36:51 +0530 Subject: [PATCH 2/7] Allow custom headers while connect --- videodb/__init__.py | 3 ++- videodb/_utils/_http_client.py | 11 +++++++++++ videodb/client.py | 4 ++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/videodb/__init__.py b/videodb/__init__.py index d1d3215..41244dc 100644 --- a/videodb/__init__.py +++ b/videodb/__init__.py @@ -58,6 +58,7 @@ def connect( api_key: str = None, base_url: Optional[str] = VIDEO_DB_API, log_level: Optional[int] = logging.INFO, + **kwargs, ) -> Connection: """A client for interacting with a videodb via REST API @@ -76,4 +77,4 @@ def connect( "No API key provided. Set an API key either as an environment variable (VIDEO_DB_API_KEY) or pass it as an argument." ) - return Connection(api_key, base_url) + return Connection(api_key, base_url, **kwargs) diff --git a/videodb/_utils/_http_client.py b/videodb/_utils/_http_client.py index 8633ebb..ab571f6 100644 --- a/videodb/_utils/_http_client.py +++ b/videodb/_utils/_http_client.py @@ -34,6 +34,7 @@ def __init__( base_url: str, version: str, max_retries: Optional[int] = HttpClientDefaultValues.max_retries, + **kwargs, ) -> None: """Create a new http client instance @@ -52,11 +53,13 @@ def __init__( self.session.mount("http://", adapter) self.session.mount("https://", adapter) self.version = version + kwargs = self._format_headers(kwargs) self.session.headers.update( { "x-access-token": api_key, "x-videodb-client": f"videodb-python/{self.version}", "Content-Type": "application/json", + **kwargs, } ) self.base_url = base_url @@ -198,6 +201,14 @@ def _parse_response(self, response: requests.Response): f"Invalid request: {response.text}", response ) from None + def _format_headers(self, headers: dict): + """Format the headers""" + formatted_headers = {} + for key, value in headers.items(): + key = key.lower().replace("_", "-") + formatted_headers[f"x-{key}"] = value + return formatted_headers + def get( self, path: str, show_progress: Optional[bool] = False, **kwargs ) -> requests.Response: diff --git a/videodb/client.py b/videodb/client.py index f74c928..16feed6 100644 --- a/videodb/client.py +++ b/videodb/client.py @@ -29,7 +29,7 @@ class Connection(HttpClient): """Connection class to interact with the VideoDB""" - def __init__(self, api_key: str, base_url: str) -> "Connection": + def __init__(self, api_key: str, base_url: str, **kwargs) -> "Connection": """Initializes a new instance of the Connection class with specified API credentials. Note: Users should not initialize this class directly. @@ -44,7 +44,7 @@ def __init__(self, api_key: str, base_url: str) -> "Connection": self.api_key = api_key self.base_url = base_url self.collection_id = "default" - super().__init__(api_key=api_key, base_url=base_url, version=__version__) + super().__init__(api_key=api_key, base_url=base_url, version=__version__, **kwargs) def get_collection(self, collection_id: Optional[str] = "default") -> Collection: """Get a collection object by its ID. From c04632e5bba3b11e9e9125ac4ee55ee3487724e0 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Thu, 26 Jun 2025 15:53:38 +0530 Subject: [PATCH 3/7] Add callback data option for meeting recroder --- videodb/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/videodb/client.py b/videodb/client.py index 16feed6..e6d8f7a 100644 --- a/videodb/client.py +++ b/videodb/client.py @@ -297,6 +297,7 @@ def record_meeting( bot_name: str, meeting_name: str, callback_url: str, + callback_data: dict = {}, time_zone: str = "UTC", ) -> dict: """Record a meeting and upload it to the default collection. @@ -305,6 +306,7 @@ def record_meeting( :param str bot_name: Name of the recorder bot :param str meeting_name: Name of the meeting :param str callback_url: URL to receive callback once recording is done + :param dict callback_data: Data to be sent in the callback (optional) :param str time_zone: Time zone for the meeting (default ``UTC``) :return: Response data from the API :rtype: dict @@ -317,6 +319,7 @@ def record_meeting( "bot_name": bot_name, "meeting_name": meeting_name, "callback_url": callback_url, + "callback_data": callback_data, "time_zone": time_zone, }, ) From 4542e36ab6d32fde87f28902bf5fd9aef420943f Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Thu, 26 Jun 2025 17:35:42 +0530 Subject: [PATCH 4/7] Add callback data option in collection for meeting recroder --- videodb/collection.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/videodb/collection.py b/videodb/collection.py index d69d0a9..e6370ba 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -491,6 +491,7 @@ def record_meeting( bot_name: str, meeting_name: str, callback_url: str, + callback_data: dict = {}, time_zone: str = "UTC", ) -> dict: """Record a meeting and upload it to this collection. @@ -499,6 +500,7 @@ def record_meeting( :param str bot_name: Name of the recorder bot :param str meeting_name: Name of the meeting :param str callback_url: URL to receive callback once recording is done + :param dict callback_data: Data to be sent in the callback (optional) :param str time_zone: Time zone for the meeting (default ``UTC``) :return: Response data from the API :rtype: dict @@ -511,6 +513,7 @@ def record_meeting( "bot_name": bot_name, "meeting_name": meeting_name, "callback_url": callback_url, + "callback_data": callback_data, "time_zone": time_zone, }, ) From d67785ea3ab3f1300a701ec7de05c6c947519b92 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Fri, 27 Jun 2025 23:47:58 +0530 Subject: [PATCH 5/7] Swap collection string with API Path constant in collection record meeting --- videodb/collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/videodb/collection.py b/videodb/collection.py index e6370ba..7182565 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -507,7 +507,7 @@ def record_meeting( """ return self._connection.post( - path=f"/collection/{self.id}/{ApiPath.meeting}/{ApiPath.record}", + path=f"{ApiPath.collection}/{self.id}/{ApiPath.meeting}/{ApiPath.record}", data={ "link": link, "bot_name": bot_name, From e410974a2824c5079a105c761c1929a91a983038 Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Thu, 3 Jul 2025 11:36:44 +0530 Subject: [PATCH 6/7] Add Meeting class for meeting recordings --- videodb/client.py | 20 ++++------- videodb/collection.py | 23 ++++-------- videodb/meeting.py | 81 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 30 deletions(-) create mode 100644 videodb/meeting.py diff --git a/videodb/client.py b/videodb/client.py index e6d8f7a..a3eb38a 100644 --- a/videodb/client.py +++ b/videodb/client.py @@ -18,6 +18,7 @@ from videodb.video import Video from videodb.audio import Audio from videodb.image import Image +from videodb.meeting import Meeting from videodb._upload import ( upload, @@ -299,7 +300,7 @@ def record_meeting( callback_url: str, callback_data: dict = {}, time_zone: str = "UTC", - ) -> dict: + ) -> Meeting: """Record a meeting and upload it to the default collection. :param str link: Meeting link @@ -308,8 +309,8 @@ def record_meeting( :param str callback_url: URL to receive callback once recording is done :param dict callback_data: Data to be sent in the callback (optional) :param str time_zone: Time zone for the meeting (default ``UTC``) - :return: Response data from the API - :rtype: dict + :return: :class:`Meeting ` object representing the recording bot + :rtype: :class:`videodb.meeting.Meeting` """ response = self.post( @@ -323,14 +324,5 @@ def record_meeting( "time_zone": time_zone, }, ) - return response - - def get_meeting_info(self, bot_id: str) -> dict: - """Get the information of a given meeting bot. - - :param str bot_id: ID returned when recording was initiated - :return: Information of the meeting bot - :rtype: dict - """ - - return self.get(path=f"{ApiPath.collection}/default/{ApiPath.meeting}/{bot_id}") + meeting_id = response.get("meeting_id") + return Meeting(self, id=meeting_id, collection_id="default", **response) diff --git a/videodb/collection.py b/videodb/collection.py index 7182565..513e4ed 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -12,6 +12,7 @@ from videodb.video import Video from videodb.audio import Audio from videodb.image import Image +from videodb.meeting import Meeting from videodb.rtstream import RTStream from videodb.search import SearchFactory, SearchResult @@ -493,7 +494,7 @@ def record_meeting( callback_url: str, callback_data: dict = {}, time_zone: str = "UTC", - ) -> dict: + ) -> Meeting: """Record a meeting and upload it to this collection. :param str link: Meeting link @@ -502,11 +503,11 @@ def record_meeting( :param str callback_url: URL to receive callback once recording is done :param dict callback_data: Data to be sent in the callback (optional) :param str time_zone: Time zone for the meeting (default ``UTC``) - :return: Response data from the API - :rtype: dict + :return: :class:`Meeting ` object representing the recording bot + :rtype: :class:`videodb.meeting.Meeting` """ - return self._connection.post( + response = self._connection.post( path=f"{ApiPath.collection}/{self.id}/{ApiPath.meeting}/{ApiPath.record}", data={ "link": link, @@ -517,15 +518,5 @@ def record_meeting( "time_zone": time_zone, }, ) - - def get_meeting_info(self, bot_id: str) -> dict: - """Get the recording info for a meeting bot in this collection. - - :param str bot_id: ID returned when recording was initiated - :return: Information of the meeting bot - :rtype: dict - """ - - return self._connection.get( - path=f"{ApiPath.collection}/{self.id}/{ApiPath.meeting}/{bot_id}" - ) + meeting_id = response.get("meeting_id") + return Meeting(self._connection, id=meeting_id, collection_id=self.id, **response) diff --git a/videodb/meeting.py b/videodb/meeting.py new file mode 100644 index 0000000..17a627a --- /dev/null +++ b/videodb/meeting.py @@ -0,0 +1,81 @@ +from videodb._constants import ApiPath + +from videodb.exceptions import ( + VideodbError, +) + + +class Meeting: + """Meeting class representing a meeting recording bot.""" + + def __init__(self, _connection, id: str, collection_id: str, **kwargs) -> None: + self._connection = _connection + self.id = id + self.collection_id = collection_id + self._update_attributes(kwargs) + + def __repr__(self) -> str: + return f"Meeting(id={self.id}, collection_id={self.collection_id}, name={self.name}, status={self.status}, bot_name={self.bot_name})" + + def _update_attributes(self, data: dict) -> None: + """Update instance attributes from API response data.""" + self.bot_name = data.get("bot_name") + self.name = data.get("meeting_name") + self.meeting_url = data.get("meeting_url") + self.status = data.get("status") + self.time_zone = data.get("time_zone") + + def refresh(self) -> "Meeting": + """Refresh meeting data from the server. + + Returns: + self: The Meeting instance with updated data + + Raises: + APIError: If the API request fails + """ + response = self._connection.get( + path=f"{ApiPath.collection}/{self.collection_id}/{ApiPath.meeting}/{self.id}" + ) + + if response: + self._update_attributes(response) + else: + raise VideodbError(f"Failed to refresh meeting {self.id}") + + return self + + @property + def is_active(self) -> bool: + """Check if the meeting is currently active.""" + return self.status in ["initializing", "processing"] + + @property + def is_completed(self) -> bool: + """Check if the meeting has completed.""" + return self.status in ["done"] + + def wait_for_status( + self, target_status: str, timeout: int = 14400, interval: int = 300 + ) -> bool: + """Wait for the meeting to reach a specific status. + + Args: + target_status: The status to wait for + timeout: Maximum time to wait in seconds + interval: Time between status checks in seconds + + Returns: + bool: True if status reached, False if timeout + """ + import time + + start_time = time.time() + + while time.time() - start_time < timeout: + self.refresh() + if self.status == target_status: + return True + time.sleep(interval) + + return False From 0d7e670153b94e150a0d1c0460d97e71a450228f Mon Sep 17 00:00:00 2001 From: ashish-spext Date: Sat, 5 Jul 2025 15:23:13 +0530 Subject: [PATCH 7/7] Make bot_name, meeting_name and callback url optional, decrease interval of wait for status --- videodb/collection.py | 6 +++--- videodb/meeting.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/videodb/collection.py b/videodb/collection.py index 513e4ed..b546844 100644 --- a/videodb/collection.py +++ b/videodb/collection.py @@ -489,9 +489,9 @@ def make_private(self): def record_meeting( self, link: str, - bot_name: str, - meeting_name: str, - callback_url: str, + bot_name: str = None, + meeting_name: str = None, + callback_url: str = None, callback_data: dict = {}, time_zone: str = "UTC", ) -> Meeting: diff --git a/videodb/meeting.py b/videodb/meeting.py index 17a627a..114e143 100644 --- a/videodb/meeting.py +++ b/videodb/meeting.py @@ -56,7 +56,7 @@ def is_completed(self) -> bool: return self.status in ["done"] def wait_for_status( - self, target_status: str, timeout: int = 14400, interval: int = 300 + self, target_status: str, timeout: int = 14400, interval: int = 120 ) -> bool: """Wait for the meeting to reach a specific status.