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/_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/_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 25ae399..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, @@ -29,7 +30,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 +45,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. @@ -290,3 +291,38 @@ 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, + callback_data: dict = {}, + time_zone: str = "UTC", + ) -> Meeting: + """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 dict callback_data: Data to be sent in the callback (optional) + :param str time_zone: Time zone for the meeting (default ``UTC``) + :return: :class:`Meeting ` object representing the recording bot + :rtype: :class:`videodb.meeting.Meeting` + """ + + 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, + "callback_data": callback_data, + "time_zone": time_zone, + }, + ) + 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 e941cf4..b546844 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 @@ -484,3 +485,38 @@ 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 = None, + meeting_name: str = None, + callback_url: str = None, + callback_data: dict = {}, + time_zone: str = "UTC", + ) -> Meeting: + """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 dict callback_data: Data to be sent in the callback (optional) + :param str time_zone: Time zone for the meeting (default ``UTC``) + :return: :class:`Meeting ` object representing the recording bot + :rtype: :class:`videodb.meeting.Meeting` + """ + + response = self._connection.post( + path=f"{ApiPath.collection}/{self.id}/{ApiPath.meeting}/{ApiPath.record}", + data={ + "link": link, + "bot_name": bot_name, + "meeting_name": meeting_name, + "callback_url": callback_url, + "callback_data": callback_data, + "time_zone": time_zone, + }, + ) + 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..114e143 --- /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 = 120 + ) -> 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