Skip to content

Add meeting recorder #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion videodb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
2 changes: 2 additions & 0 deletions videodb/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class ApiPath:
translate = "translate"
dub = "dub"
transcode = "transcode"
meeting = "meeting"
record = "record"


class Status:
Expand Down
11 changes: 11 additions & 0 deletions videodb/_utils/_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
40 changes: 38 additions & 2 deletions videodb/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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 <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)
36 changes: 36 additions & 0 deletions videodb/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <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)
81 changes: 81 additions & 0 deletions videodb/meeting.py
Original file line number Diff line number Diff line change
@@ -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