Skip to content

Commit 6646cd8

Browse files
authored
Merge pull request #6 from video-db/ar/add-timeline-and-asset
Add Assets, Timeline and Audio uploads
2 parents a20f104 + 13e4da7 commit 6646cd8

14 files changed

+235
-11
lines changed

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ body:
1717
- label: Potential new bug in VideoDB API
1818
required: false
1919
- label: I've checked the current issues, and there's no record of this bug
20-
required: true
20+
required: false
2121
- type: textarea
2222
attributes:
2323
label: Current Behavior

.github/ISSUE_TEMPLATE/feature_request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ body:
1717
- label: Potential new feature in VideoDB API
1818
required: false
1919
- label: I've checked the current issues, and there's no record of this feature request
20-
required: true
20+
required: false
2121
- type: textarea
2222
attributes:
2323
label: Describe the feature

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
requests==2.31.0
22
backoff==2.2.1
3+
tqdm==4.66.1

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def get_version():
3232
install_requires=[
3333
"requests>=2.25.1",
3434
"backoff>=2.2.1",
35+
"tqdm>=4.66.1",
3536
],
3637
classifiers=[
3738
"Intended Audience :: Developers",

videodb/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from typing import Optional
77
from videodb._utils._video import play_stream
8-
from videodb._constants import VIDEO_DB_API
8+
from videodb._constants import VIDEO_DB_API, MediaType
99
from videodb.client import Connection
1010
from videodb.exceptions import (
1111
VideodbError,
@@ -16,7 +16,7 @@
1616

1717
logger: logging.Logger = logging.getLogger("videodb")
1818

19-
__version__ = "0.0.2"
19+
__version__ = "0.0.3"
2020
__author__ = "videodb"
2121

2222
__all__ = [
@@ -25,6 +25,7 @@
2525
"InvalidRequestError",
2626
"SearchError",
2727
"play_stream",
28+
"MediaType",
2829
]
2930

3031

videodb/_constants.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
VIDEO_DB_API: str = "https://api.videodb.io"
55

66

7+
class MediaType:
8+
video = "video"
9+
audio = "audio"
10+
711

812
class SearchType:
913
semantic = "semantic"
@@ -26,6 +30,7 @@ class ApiPath:
2630
collection = "collection"
2731
upload = "upload"
2832
video = "video"
33+
audio = "audio"
2934
stream = "stream"
3035
thumbnail = "thumbnail"
3136
upload_url = "upload_url"
@@ -34,6 +39,7 @@ class ApiPath:
3439
search = "search"
3540
compile = "compile"
3641
workflow = "workflow"
42+
timeline = "timeline"
3743

3844

3945
class Status:
@@ -46,3 +52,7 @@ class HttpClientDefaultValues:
4652
timeout = 30
4753
backoff_factor = 0.1
4854
status_forcelist = [502, 503, 504]
55+
56+
57+
class MaxSupported:
58+
fade_duration = 5

videodb/_upload.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def upload(
1717
_connection,
1818
file_path: str = None,
1919
url: str = None,
20+
media_type: Optional[str] = None,
2021
name: Optional[str] = None,
2122
description: Optional[str] = None,
2223
callback_url: Optional[str] = None,
@@ -53,6 +54,7 @@ def upload(
5354
"name": name,
5455
"description": description,
5556
"callback_url": callback_url,
57+
"media_type": media_type,
5658
},
5759
)
5860
return upload_data

videodb/_utils/_http_client.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import requests
55
import backoff
66

7+
from tqdm import tqdm
78
from typing import (
89
Callable,
910
Optional,
@@ -52,6 +53,8 @@ def __init__(
5253
{"x-access-token": api_key, "Content-Type": "application/json"}
5354
)
5455
self.base_url = base_url
56+
self.show_progress = False
57+
self.progress_bar = None
5558
logger.debug(f"Initialized http client with base url: {self.base_url}")
5659

5760
def _make_request(
@@ -120,16 +123,29 @@ def _handle_request_error(self, e: requests.exceptions.RequestException) -> None
120123
f"Invalid request: {str(e)}", e.response
121124
) from None
122125

123-
@backoff.on_exception(backoff.expo, Exception, max_time=500, logger=None)
126+
@backoff.on_exception(
127+
backoff.constant, Exception, max_time=500, interval=5, logger=None, jitter=None
128+
)
124129
def _get_output(self, url: str):
125130
"""Get the output from an async request"""
126131
response_json = self.session.get(url).json()
127132
if (
128133
response_json.get("status") == Status.in_progress
129134
or response_json.get("status") == Status.processing
130135
):
136+
percentage = response_json.get("data").get("percentage")
137+
if percentage and self.show_progress and self.progress_bar:
138+
self.progress_bar.n = int(percentage)
139+
self.progress_bar.update(0)
140+
131141
logger.debug("Waiting for processing to complete")
132142
raise Exception("Stuck on processing status") from None
143+
if self.show_progress and self.progress_bar:
144+
self.progress_bar.n = 100
145+
self.progress_bar.update(0)
146+
self.progress_bar.close()
147+
self.progress_bar = None
148+
self.show_progress = False
133149
return response_json.get("response") or response_json
134150

135151
def _parse_response(self, response: requests.Response):
@@ -145,6 +161,13 @@ def _parse_response(self, response: requests.Response):
145161
response_json.get("status") == Status.processing
146162
and response_json.get("request_type", "sync") == "sync"
147163
):
164+
if self.show_progress:
165+
self.progress_bar = tqdm(
166+
total=100,
167+
position=0,
168+
leave=True,
169+
bar_format="{l_bar}{bar:100}{r_bar}{bar:-100b}",
170+
)
148171
response_json = self._get_output(
149172
response_json.get("data").get("output_url")
150173
)
@@ -168,9 +191,12 @@ def _parse_response(self, response: requests.Response):
168191
f"Invalid request: {response.text}", response
169192
) from None
170193

171-
def get(self, path: str, **kwargs) -> requests.Response:
194+
def get(
195+
self, path: str, show_progress: Optional[bool] = False, **kwargs
196+
) -> requests.Response:
172197
"""Make a get request"""
173-
return self._make_request(self.session.get, path, **kwargs)
198+
self.show_progress = show_progress
199+
return self._make_request(method=self.session.get, path=path, **kwargs)
174200

175201
def post(self, path: str, data=None, **kwargs) -> requests.Response:
176202
"""Make a post request"""

videodb/asset.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import copy
2+
import logging
3+
4+
from typing import Optional, Union
5+
6+
from videodb._constants import MaxSupported
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def validate_max_supported(
12+
duration: Union[int, float], max_duration: Union[int, float], attribute: str = ""
13+
) -> Union[int, float, None]:
14+
if duration is None:
15+
return 0
16+
if duration is not None and max_duration is not None and duration > max_duration:
17+
logger.warning(
18+
f"{attribute}: {duration} is greater than max supported: {max_duration}"
19+
)
20+
return duration
21+
22+
23+
class MediaAsset:
24+
def __init__(self, asset_id: str) -> None:
25+
self.asset_id: str = asset_id
26+
27+
def to_json(self) -> dict:
28+
return self.__dict__
29+
30+
31+
class VideoAsset(MediaAsset):
32+
def __init__(
33+
self,
34+
asset_id: str,
35+
start: Optional[int] = 0,
36+
end: Optional[Union[int, None]] = None,
37+
) -> None:
38+
super().__init__(asset_id)
39+
self.start: int = start
40+
self.end: Union[int, None] = end
41+
42+
def to_json(self) -> dict:
43+
return copy.deepcopy(self.__dict__)
44+
45+
def __repr__(self) -> str:
46+
return (
47+
f"VideoAsset("
48+
f"asset_id={self.asset_id}, "
49+
f"start={self.start}, "
50+
f"end={self.end})"
51+
)
52+
53+
54+
class AudioAsset(MediaAsset):
55+
def __init__(
56+
self,
57+
asset_id: str,
58+
start: Optional[int] = 0,
59+
end: Optional[Union[int, None]] = None,
60+
disable_other_tracks: Optional[bool] = True,
61+
fade_in_duration: Optional[Union[int, float]] = 0,
62+
fade_out_duration: Optional[Union[int, float]] = 0,
63+
):
64+
super().__init__(asset_id)
65+
self.start: int = start
66+
self.end: Union[int, None] = end
67+
self.disable_other_tracks: bool = disable_other_tracks
68+
self.fade_in_duration: Union[int, float] = validate_max_supported(
69+
fade_in_duration, MaxSupported.fade_duration, "fade_in_duration"
70+
)
71+
self.fade_out_duration: Union[int, float] = validate_max_supported(
72+
fade_out_duration, MaxSupported.fade_duration, "fade_out_duration"
73+
)
74+
75+
def to_json(self) -> dict:
76+
return copy.deepcopy(self.__dict__)
77+
78+
def __repr__(self) -> str:
79+
return (
80+
f"AudioAsset("
81+
f"asset_id={self.asset_id}, "
82+
f"start={self.start}, "
83+
f"end={self.end}, "
84+
f"disable_other_tracks={self.disable_other_tracks}, "
85+
f"fade_in_duration={self.fade_in_duration}, "
86+
f"fade_out_duration={self.fade_out_duration})"
87+
)

videodb/audio.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from videodb._constants import (
2+
ApiPath,
3+
)
4+
5+
6+
class Audio:
7+
def __init__(self, _connection, id: str, collection_id: str, **kwargs) -> None:
8+
self._connection = _connection
9+
self.id = id
10+
self.collection_id = collection_id
11+
self.name = kwargs.get("name", None)
12+
self.length = kwargs.get("length", None)
13+
14+
def __repr__(self) -> str:
15+
return (
16+
f"Audio("
17+
f"id={self.id}, "
18+
f"collection_id={self.collection_id}, "
19+
f"name={self.name}, "
20+
f"length={self.length})"
21+
)
22+
23+
def delete(self) -> None:
24+
self._connection.delete(f"{ApiPath.audio}/{self.id}")

videodb/client.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from typing import (
44
Optional,
5+
Union,
56
)
67

78
from videodb._constants import (
@@ -11,6 +12,7 @@
1112
from videodb.collection import Collection
1213
from videodb._utils._http_client import HttpClient
1314
from videodb.video import Video
15+
from videodb.audio import Audio
1416

1517
from videodb._upload import (
1618
upload,
@@ -40,16 +42,21 @@ def upload(
4042
self,
4143
file_path: str = None,
4244
url: str = None,
45+
media_type: Optional[str] = None,
4346
name: Optional[str] = None,
4447
description: Optional[str] = None,
4548
callback_url: Optional[str] = None,
46-
) -> Video:
49+
) -> Union[Video, Audio, None]:
4750
upload_data = upload(
4851
self,
4952
file_path,
5053
url,
54+
media_type,
5155
name,
5256
description,
5357
callback_url,
5458
)
55-
return Video(self, **upload_data) if upload_data else None
59+
if upload_data.get("id").startswith("m-"):
60+
return Video(self, **upload_data) if upload_data else None
61+
elif upload_data.get("id").startswith("a-"):
62+
return Audio(self, **upload_data) if upload_data else None

videodb/collection.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from typing import (
44
Optional,
5+
Union,
56
)
67
from videodb._upload import (
78
upload,
@@ -11,6 +12,7 @@
1112
SearchType,
1213
)
1314
from videodb.video import Video
15+
from videodb.audio import Audio
1416
from videodb.search import SearchFactory, SearchResult
1517

1618
logger = logging.getLogger(__name__)
@@ -41,6 +43,17 @@ def delete_video(self, video_id: str) -> None:
4143
"""
4244
return self._connection.delete(path=f"{ApiPath.video}/{video_id}")
4345

46+
def get_audios(self) -> list[Audio]:
47+
audios_data = self._connection.get(path=f"{ApiPath.audio}")
48+
return [Audio(self._connection, **audio) for audio in audios_data.get("audios")]
49+
50+
def get_audio(self, audio_id: str) -> Audio:
51+
audio_data = self._connection.get(path=f"{ApiPath.audio}/{audio_id}")
52+
return Audio(self._connection, **audio_data)
53+
54+
def delete_audio(self, audio_id: str) -> None:
55+
return self._connection.delete(path=f"{ApiPath.audio}/{audio_id}")
56+
4457
def search(
4558
self,
4659
query: str,
@@ -62,16 +75,21 @@ def upload(
6275
self,
6376
file_path: str = None,
6477
url: Optional[str] = None,
78+
media_type: Optional[str] = None,
6579
name: Optional[str] = None,
6680
description: Optional[str] = None,
6781
callback_url: Optional[str] = None,
68-
) -> Video:
82+
) -> Union[Video, Audio, None]:
6983
upload_data = upload(
7084
self._connection,
7185
file_path,
7286
url,
87+
media_type,
7388
name,
7489
description,
7590
callback_url,
7691
)
77-
return Video(self._connection, **upload_data) if upload_data else None
92+
if upload_data.get("id").startswith("m-"):
93+
return Video(self._connection, **upload_data) if upload_data else None
94+
elif upload_data.get("id").startswith("a-"):
95+
return Audio(self._connection, **upload_data) if upload_data else None

0 commit comments

Comments
 (0)