diff --git a/videodb/editor.py b/videodb/editor.py index 4c3ae25..5c40282 100644 --- a/videodb/editor.py +++ b/videodb/editor.py @@ -1,7 +1,14 @@ +import json +import requests + from typing import List, Optional, Union from enum import Enum from videodb._constants import ApiPath +from videodb.exceptions import InvalidRequestError + + +MAX_PAYLOAD_SIZE = 100 * 1024 class AssetType(str, Enum): @@ -349,7 +356,6 @@ class Font: :ivar int size: Font size in pixels :ivar str color: Font color in hex format (e.g., "#FFFFFF") :ivar float opacity: Font opacity (0.0 to 1.0) - :ivar int weight: (optional) Font weight (100 to 900) """ def __init__( @@ -358,7 +364,6 @@ def __init__( size: int = 48, color: str = "#FFFFFF", opacity: float = 1.0, - weight: Optional[int] = None, ): """Initialize a Font instance. @@ -366,21 +371,17 @@ def __init__( :param int size: Font size in pixels (default: 48) :param str color: Font color in hex format (default: "#FFFFFF") :param float opacity: Font opacity between 0.0 and 1.0 (default: 1.0) - :param int weight: (optional) Font weight between 100 and 900 - :raises ValueError: If size < 1, opacity not in [0.0, 1.0], or weight not in [100, 900] + :raises ValueError: If size < 1, opacity not in [0.0, 1.0] """ if size < 1: raise ValueError("size must be at least 1") if not (0.0 <= opacity <= 1.0): raise ValueError("opacity must be between 0.0 and 1.0") - if weight is not None and not (100 <= weight <= 900): - raise ValueError("weight must be between 100 and 900") self.family = family self.size = size self.color = color self.opacity = opacity - self.weight = weight def to_json(self) -> dict: """Convert the font settings to a JSON-serializable dictionary. @@ -394,8 +395,6 @@ def to_json(self) -> dict: "color": self.color, "opacity": self.opacity, } - if self.weight is not None: - data["weight"] = self.weight return data @@ -1100,17 +1099,61 @@ def generate_stream(self) -> str: Makes an API request to render the timeline and generate streaming URLs. Updates the stream_url and player_url instance variables. + If the timeline data exceeds the max payload size, it will be uploaded + as a file first to avoid HTTP content length limits. + :return: The stream URL of the generated video :rtype: str """ - stream_data = self.connection.post( - path=ApiPath.editor, - data=self.to_json(), - ) + timeline_data = self.to_json() + json_str = json.dumps(timeline_data) + payload_size = len(json_str.encode("utf-8")) + + if payload_size > MAX_PAYLOAD_SIZE: + # Upload timeline data as a file to avoid HTTP content length limits + timeline_url = self._upload_timeline_data(json_str) + stream_data = self.connection.post( + path=ApiPath.editor, + data={"timeline_url": timeline_url}, + ) + else: + stream_data = self.connection.post( + path=ApiPath.editor, + data=timeline_data, + ) + self.stream_url = stream_data.get("stream_url") self.player_url = stream_data.get("player_url") return stream_data.get("stream_url", None) + def _upload_timeline_data(self, json_str: str) -> str: + """Upload timeline JSON data as a file and return the URL. + + :param str json_str: The JSON string of timeline data to upload + :return: The URL of the uploaded file + :rtype: str + :raises InvalidRequestError: If upload fails + """ + # Get a presigned upload URL + upload_url_data = self.connection.get( + path=f"{ApiPath.collection}/{self.connection.collection_id}/{ApiPath.upload_url}", + params={"name": "timeline_data.json"}, + ) + upload_url = upload_url_data.get("upload_url") + + # Upload the JSON data as a file + try: + files = {"file": ("timeline_data.json", json_str, "application/json")} + response = requests.post(upload_url, files=files) + response.raise_for_status() + except requests.exceptions.RequestException as e: + raise InvalidRequestError( + f"Failed to upload timeline data: {str(e)}", + getattr(e, "response", None), + ) from None + + return upload_url + def download_stream(self, stream_url: str) -> dict: """Download a stream from the timeline.