Skip to content

Commit

Permalink
Merge pull request #6 from video-db/ar/add-timeline-and-asset
Browse files Browse the repository at this point in the history
Add Assets, Timeline and Audio uploads
  • Loading branch information
codeAshu authored Jan 30, 2024
2 parents a20f104 + 13e4da7 commit 6646cd8
Show file tree
Hide file tree
Showing 14 changed files with 235 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ body:
- label: Potential new bug in VideoDB API
required: false
- label: I've checked the current issues, and there's no record of this bug
required: true
required: false
- type: textarea
attributes:
label: Current Behavior
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ body:
- label: Potential new feature in VideoDB API
required: false
- label: I've checked the current issues, and there's no record of this feature request
required: true
required: false
- type: textarea
attributes:
label: Describe the feature
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
requests==2.31.0
backoff==2.2.1
tqdm==4.66.1
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def get_version():
install_requires=[
"requests>=2.25.1",
"backoff>=2.2.1",
"tqdm>=4.66.1",
],
classifiers=[
"Intended Audience :: Developers",
Expand Down
5 changes: 3 additions & 2 deletions videodb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from typing import Optional
from videodb._utils._video import play_stream
from videodb._constants import VIDEO_DB_API
from videodb._constants import VIDEO_DB_API, MediaType
from videodb.client import Connection
from videodb.exceptions import (
VideodbError,
Expand All @@ -16,7 +16,7 @@

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

__version__ = "0.0.2"
__version__ = "0.0.3"
__author__ = "videodb"

__all__ = [
Expand All @@ -25,6 +25,7 @@
"InvalidRequestError",
"SearchError",
"play_stream",
"MediaType",
]


Expand Down
10 changes: 10 additions & 0 deletions videodb/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
VIDEO_DB_API: str = "https://api.videodb.io"


class MediaType:
video = "video"
audio = "audio"


class SearchType:
semantic = "semantic"
Expand All @@ -26,6 +30,7 @@ class ApiPath:
collection = "collection"
upload = "upload"
video = "video"
audio = "audio"
stream = "stream"
thumbnail = "thumbnail"
upload_url = "upload_url"
Expand All @@ -34,6 +39,7 @@ class ApiPath:
search = "search"
compile = "compile"
workflow = "workflow"
timeline = "timeline"


class Status:
Expand All @@ -46,3 +52,7 @@ class HttpClientDefaultValues:
timeout = 30
backoff_factor = 0.1
status_forcelist = [502, 503, 504]


class MaxSupported:
fade_duration = 5
2 changes: 2 additions & 0 deletions videodb/_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def upload(
_connection,
file_path: str = None,
url: str = None,
media_type: Optional[str] = None,
name: Optional[str] = None,
description: Optional[str] = None,
callback_url: Optional[str] = None,
Expand Down Expand Up @@ -53,6 +54,7 @@ def upload(
"name": name,
"description": description,
"callback_url": callback_url,
"media_type": media_type,
},
)
return upload_data
32 changes: 29 additions & 3 deletions videodb/_utils/_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import requests
import backoff

from tqdm import tqdm
from typing import (
Callable,
Optional,
Expand Down Expand Up @@ -52,6 +53,8 @@ def __init__(
{"x-access-token": api_key, "Content-Type": "application/json"}
)
self.base_url = base_url
self.show_progress = False
self.progress_bar = None
logger.debug(f"Initialized http client with base url: {self.base_url}")

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

@backoff.on_exception(backoff.expo, Exception, max_time=500, logger=None)
@backoff.on_exception(
backoff.constant, Exception, max_time=500, interval=5, logger=None, jitter=None
)
def _get_output(self, url: str):
"""Get the output from an async request"""
response_json = self.session.get(url).json()
if (
response_json.get("status") == Status.in_progress
or response_json.get("status") == Status.processing
):
percentage = response_json.get("data").get("percentage")
if percentage and self.show_progress and self.progress_bar:
self.progress_bar.n = int(percentage)
self.progress_bar.update(0)

logger.debug("Waiting for processing to complete")
raise Exception("Stuck on processing status") from None
if self.show_progress and self.progress_bar:
self.progress_bar.n = 100
self.progress_bar.update(0)
self.progress_bar.close()
self.progress_bar = None
self.show_progress = False
return response_json.get("response") or response_json

def _parse_response(self, response: requests.Response):
Expand All @@ -145,6 +161,13 @@ def _parse_response(self, response: requests.Response):
response_json.get("status") == Status.processing
and response_json.get("request_type", "sync") == "sync"
):
if self.show_progress:
self.progress_bar = tqdm(
total=100,
position=0,
leave=True,
bar_format="{l_bar}{bar:100}{r_bar}{bar:-100b}",
)
response_json = self._get_output(
response_json.get("data").get("output_url")
)
Expand All @@ -168,9 +191,12 @@ def _parse_response(self, response: requests.Response):
f"Invalid request: {response.text}", response
) from None

def get(self, path: str, **kwargs) -> requests.Response:
def get(
self, path: str, show_progress: Optional[bool] = False, **kwargs
) -> requests.Response:
"""Make a get request"""
return self._make_request(self.session.get, path, **kwargs)
self.show_progress = show_progress
return self._make_request(method=self.session.get, path=path, **kwargs)

def post(self, path: str, data=None, **kwargs) -> requests.Response:
"""Make a post request"""
Expand Down
87 changes: 87 additions & 0 deletions videodb/asset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import copy
import logging

from typing import Optional, Union

from videodb._constants import MaxSupported

logger = logging.getLogger(__name__)


def validate_max_supported(
duration: Union[int, float], max_duration: Union[int, float], attribute: str = ""
) -> Union[int, float, None]:
if duration is None:
return 0
if duration is not None and max_duration is not None and duration > max_duration:
logger.warning(
f"{attribute}: {duration} is greater than max supported: {max_duration}"
)
return duration


class MediaAsset:
def __init__(self, asset_id: str) -> None:
self.asset_id: str = asset_id

def to_json(self) -> dict:
return self.__dict__


class VideoAsset(MediaAsset):
def __init__(
self,
asset_id: str,
start: Optional[int] = 0,
end: Optional[Union[int, None]] = None,
) -> None:
super().__init__(asset_id)
self.start: int = start
self.end: Union[int, None] = end

def to_json(self) -> dict:
return copy.deepcopy(self.__dict__)

def __repr__(self) -> str:
return (
f"VideoAsset("
f"asset_id={self.asset_id}, "
f"start={self.start}, "
f"end={self.end})"
)


class AudioAsset(MediaAsset):
def __init__(
self,
asset_id: str,
start: Optional[int] = 0,
end: Optional[Union[int, None]] = None,
disable_other_tracks: Optional[bool] = True,
fade_in_duration: Optional[Union[int, float]] = 0,
fade_out_duration: Optional[Union[int, float]] = 0,
):
super().__init__(asset_id)
self.start: int = start
self.end: Union[int, None] = end
self.disable_other_tracks: bool = disable_other_tracks
self.fade_in_duration: Union[int, float] = validate_max_supported(
fade_in_duration, MaxSupported.fade_duration, "fade_in_duration"
)
self.fade_out_duration: Union[int, float] = validate_max_supported(
fade_out_duration, MaxSupported.fade_duration, "fade_out_duration"
)

def to_json(self) -> dict:
return copy.deepcopy(self.__dict__)

def __repr__(self) -> str:
return (
f"AudioAsset("
f"asset_id={self.asset_id}, "
f"start={self.start}, "
f"end={self.end}, "
f"disable_other_tracks={self.disable_other_tracks}, "
f"fade_in_duration={self.fade_in_duration}, "
f"fade_out_duration={self.fade_out_duration})"
)
24 changes: 24 additions & 0 deletions videodb/audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from videodb._constants import (
ApiPath,
)


class Audio:
def __init__(self, _connection, id: str, collection_id: str, **kwargs) -> None:
self._connection = _connection
self.id = id
self.collection_id = collection_id
self.name = kwargs.get("name", None)
self.length = kwargs.get("length", None)

def __repr__(self) -> str:
return (
f"Audio("
f"id={self.id}, "
f"collection_id={self.collection_id}, "
f"name={self.name}, "
f"length={self.length})"
)

def delete(self) -> None:
self._connection.delete(f"{ApiPath.audio}/{self.id}")
11 changes: 9 additions & 2 deletions videodb/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import (
Optional,
Union,
)

from videodb._constants import (
Expand All @@ -11,6 +12,7 @@
from videodb.collection import Collection
from videodb._utils._http_client import HttpClient
from videodb.video import Video
from videodb.audio import Audio

from videodb._upload import (
upload,
Expand Down Expand Up @@ -40,16 +42,21 @@ def upload(
self,
file_path: str = None,
url: str = None,
media_type: Optional[str] = None,
name: Optional[str] = None,
description: Optional[str] = None,
callback_url: Optional[str] = None,
) -> Video:
) -> Union[Video, Audio, None]:
upload_data = upload(
self,
file_path,
url,
media_type,
name,
description,
callback_url,
)
return Video(self, **upload_data) if upload_data else None
if upload_data.get("id").startswith("m-"):
return Video(self, **upload_data) if upload_data else None
elif upload_data.get("id").startswith("a-"):
return Audio(self, **upload_data) if upload_data else None
22 changes: 20 additions & 2 deletions videodb/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import (
Optional,
Union,
)
from videodb._upload import (
upload,
Expand All @@ -11,6 +12,7 @@
SearchType,
)
from videodb.video import Video
from videodb.audio import Audio
from videodb.search import SearchFactory, SearchResult

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

def get_audios(self) -> list[Audio]:
audios_data = self._connection.get(path=f"{ApiPath.audio}")
return [Audio(self._connection, **audio) for audio in audios_data.get("audios")]

def get_audio(self, audio_id: str) -> Audio:
audio_data = self._connection.get(path=f"{ApiPath.audio}/{audio_id}")
return Audio(self._connection, **audio_data)

def delete_audio(self, audio_id: str) -> None:
return self._connection.delete(path=f"{ApiPath.audio}/{audio_id}")

def search(
self,
query: str,
Expand All @@ -62,16 +75,21 @@ def upload(
self,
file_path: str = None,
url: Optional[str] = None,
media_type: Optional[str] = None,
name: Optional[str] = None,
description: Optional[str] = None,
callback_url: Optional[str] = None,
) -> Video:
) -> Union[Video, Audio, None]:
upload_data = upload(
self._connection,
file_path,
url,
media_type,
name,
description,
callback_url,
)
return Video(self._connection, **upload_data) if upload_data else None
if upload_data.get("id").startswith("m-"):
return Video(self._connection, **upload_data) if upload_data else None
elif upload_data.get("id").startswith("a-"):
return Audio(self._connection, **upload_data) if upload_data else None
Loading

0 comments on commit 6646cd8

Please sign in to comment.