Skip to content

Commit 442a166

Browse files
authored
Merge pull request #47 from video-db/add-meeting-recorder
Add meeting recorder
2 parents ff705d8 + 0d7e670 commit 442a166

File tree

6 files changed

+170
-3
lines changed

6 files changed

+170
-3
lines changed

videodb/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def connect(
5858
api_key: str = None,
5959
base_url: Optional[str] = VIDEO_DB_API,
6060
log_level: Optional[int] = logging.INFO,
61+
**kwargs,
6162
) -> Connection:
6263
"""A client for interacting with a videodb via REST API
6364
@@ -76,4 +77,4 @@ def connect(
7677
"No API key provided. Set an API key either as an environment variable (VIDEO_DB_API_KEY) or pass it as an argument."
7778
)
7879

79-
return Connection(api_key, base_url)
80+
return Connection(api_key, base_url, **kwargs)

videodb/_constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ class ApiPath:
8181
translate = "translate"
8282
dub = "dub"
8383
transcode = "transcode"
84+
meeting = "meeting"
85+
record = "record"
8486

8587

8688
class Status:

videodb/_utils/_http_client.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def __init__(
3434
base_url: str,
3535
version: str,
3636
max_retries: Optional[int] = HttpClientDefaultValues.max_retries,
37+
**kwargs,
3738
) -> None:
3839
"""Create a new http client instance
3940
@@ -52,11 +53,13 @@ def __init__(
5253
self.session.mount("http://", adapter)
5354
self.session.mount("https://", adapter)
5455
self.version = version
56+
kwargs = self._format_headers(kwargs)
5557
self.session.headers.update(
5658
{
5759
"x-access-token": api_key,
5860
"x-videodb-client": f"videodb-python/{self.version}",
5961
"Content-Type": "application/json",
62+
**kwargs,
6063
}
6164
)
6265
self.base_url = base_url
@@ -198,6 +201,14 @@ def _parse_response(self, response: requests.Response):
198201
f"Invalid request: {response.text}", response
199202
) from None
200203

204+
def _format_headers(self, headers: dict):
205+
"""Format the headers"""
206+
formatted_headers = {}
207+
for key, value in headers.items():
208+
key = key.lower().replace("_", "-")
209+
formatted_headers[f"x-{key}"] = value
210+
return formatted_headers
211+
201212
def get(
202213
self, path: str, show_progress: Optional[bool] = False, **kwargs
203214
) -> requests.Response:

videodb/client.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from videodb.video import Video
1919
from videodb.audio import Audio
2020
from videodb.image import Image
21+
from videodb.meeting import Meeting
2122

2223
from videodb._upload import (
2324
upload,
@@ -29,7 +30,7 @@
2930
class Connection(HttpClient):
3031
"""Connection class to interact with the VideoDB"""
3132

32-
def __init__(self, api_key: str, base_url: str) -> "Connection":
33+
def __init__(self, api_key: str, base_url: str, **kwargs) -> "Connection":
3334
"""Initializes a new instance of the Connection class with specified API credentials.
3435
3536
Note: Users should not initialize this class directly.
@@ -44,7 +45,7 @@ def __init__(self, api_key: str, base_url: str) -> "Connection":
4445
self.api_key = api_key
4546
self.base_url = base_url
4647
self.collection_id = "default"
47-
super().__init__(api_key=api_key, base_url=base_url, version=__version__)
48+
super().__init__(api_key=api_key, base_url=base_url, version=__version__, **kwargs)
4849

4950
def get_collection(self, collection_id: Optional[str] = "default") -> Collection:
5051
"""Get a collection object by its ID.
@@ -293,3 +294,38 @@ def upload(
293294
return Audio(self, **upload_data)
294295
elif media_id.startswith("img-"):
295296
return Image(self, **upload_data)
297+
298+
def record_meeting(
299+
self,
300+
link: str,
301+
bot_name: str,
302+
meeting_name: str,
303+
callback_url: str,
304+
callback_data: dict = {},
305+
time_zone: str = "UTC",
306+
) -> Meeting:
307+
"""Record a meeting and upload it to the default collection.
308+
309+
:param str link: Meeting link
310+
:param str bot_name: Name of the recorder bot
311+
:param str meeting_name: Name of the meeting
312+
:param str callback_url: URL to receive callback once recording is done
313+
:param dict callback_data: Data to be sent in the callback (optional)
314+
:param str time_zone: Time zone for the meeting (default ``UTC``)
315+
:return: :class:`Meeting <Meeting>` object representing the recording bot
316+
:rtype: :class:`videodb.meeting.Meeting`
317+
"""
318+
319+
response = self.post(
320+
path=f"{ApiPath.collection}/default/{ApiPath.meeting}/{ApiPath.record}",
321+
data={
322+
"link": link,
323+
"bot_name": bot_name,
324+
"meeting_name": meeting_name,
325+
"callback_url": callback_url,
326+
"callback_data": callback_data,
327+
"time_zone": time_zone,
328+
},
329+
)
330+
meeting_id = response.get("meeting_id")
331+
return Meeting(self, id=meeting_id, collection_id="default", **response)

videodb/collection.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from videodb.video import Video
1313
from videodb.audio import Audio
1414
from videodb.image import Image
15+
from videodb.meeting import Meeting
1516
from videodb.rtstream import RTStream
1617
from videodb.search import SearchFactory, SearchResult
1718

@@ -487,3 +488,38 @@ def make_private(self):
487488
path=f"{ApiPath.collection}/{self.id}", data={"is_public": False}
488489
)
489490
self.is_public = False
491+
492+
def record_meeting(
493+
self,
494+
link: str,
495+
bot_name: str = None,
496+
meeting_name: str = None,
497+
callback_url: str = None,
498+
callback_data: dict = {},
499+
time_zone: str = "UTC",
500+
) -> Meeting:
501+
"""Record a meeting and upload it to this collection.
502+
503+
:param str link: Meeting link
504+
:param str bot_name: Name of the recorder bot
505+
:param str meeting_name: Name of the meeting
506+
:param str callback_url: URL to receive callback once recording is done
507+
:param dict callback_data: Data to be sent in the callback (optional)
508+
:param str time_zone: Time zone for the meeting (default ``UTC``)
509+
:return: :class:`Meeting <Meeting>` object representing the recording bot
510+
:rtype: :class:`videodb.meeting.Meeting`
511+
"""
512+
513+
response = self._connection.post(
514+
path=f"{ApiPath.collection}/{self.id}/{ApiPath.meeting}/{ApiPath.record}",
515+
data={
516+
"link": link,
517+
"bot_name": bot_name,
518+
"meeting_name": meeting_name,
519+
"callback_url": callback_url,
520+
"callback_data": callback_data,
521+
"time_zone": time_zone,
522+
},
523+
)
524+
meeting_id = response.get("meeting_id")
525+
return Meeting(self._connection, id=meeting_id, collection_id=self.id, **response)

videodb/meeting.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from videodb._constants import ApiPath
2+
3+
from videodb.exceptions import (
4+
VideodbError,
5+
)
6+
7+
8+
class Meeting:
9+
"""Meeting class representing a meeting recording bot."""
10+
11+
def __init__(self, _connection, id: str, collection_id: str, **kwargs) -> None:
12+
self._connection = _connection
13+
self.id = id
14+
self.collection_id = collection_id
15+
self._update_attributes(kwargs)
16+
17+
def __repr__(self) -> str:
18+
return f"Meeting(id={self.id}, collection_id={self.collection_id}, name={self.name}, status={self.status}, bot_name={self.bot_name})"
19+
20+
def _update_attributes(self, data: dict) -> None:
21+
"""Update instance attributes from API response data."""
22+
self.bot_name = data.get("bot_name")
23+
self.name = data.get("meeting_name")
24+
self.meeting_url = data.get("meeting_url")
25+
self.status = data.get("status")
26+
self.time_zone = data.get("time_zone")
27+
28+
def refresh(self) -> "Meeting":
29+
"""Refresh meeting data from the server.
30+
31+
Returns:
32+
self: The Meeting instance with updated data
33+
34+
Raises:
35+
APIError: If the API request fails
36+
"""
37+
response = self._connection.get(
38+
path=f"{ApiPath.collection}/{self.collection_id}/{ApiPath.meeting}/{self.id}"
39+
)
40+
41+
if response:
42+
self._update_attributes(response)
43+
else:
44+
raise VideodbError(f"Failed to refresh meeting {self.id}")
45+
46+
return self
47+
48+
@property
49+
def is_active(self) -> bool:
50+
"""Check if the meeting is currently active."""
51+
return self.status in ["initializing", "processing"]
52+
53+
@property
54+
def is_completed(self) -> bool:
55+
"""Check if the meeting has completed."""
56+
return self.status in ["done"]
57+
58+
def wait_for_status(
59+
self, target_status: str, timeout: int = 14400, interval: int = 120
60+
) -> bool:
61+
"""Wait for the meeting to reach a specific status.
62+
63+
Args:
64+
target_status: The status to wait for
65+
timeout: Maximum time to wait in seconds
66+
interval: Time between status checks in seconds
67+
68+
Returns:
69+
bool: True if status reached, False if timeout
70+
"""
71+
import time
72+
73+
start_time = time.time()
74+
75+
while time.time() - start_time < timeout:
76+
self.refresh()
77+
if self.status == target_status:
78+
return True
79+
time.sleep(interval)
80+
81+
return False

0 commit comments

Comments
 (0)