diff --git a/src/cli.rs b/src/cli.rs index cad45c3..c46f589 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,7 @@ use clap::{Parser, Subcommand}; use crate::commands::asset::AssetArgs; +use crate::commands::project::ProjectArgs; use crate::commands::upload::UploadArgs; #[derive(Parser, Debug)] @@ -42,6 +43,7 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Command { Asset(AssetArgs), + Project(ProjectArgs), Upload(UploadArgs), } diff --git a/src/commands/asset/mod.rs b/src/commands/asset/mod.rs index 44f907a..4513d31 100644 --- a/src/commands/asset/mod.rs +++ b/src/commands/asset/mod.rs @@ -1,8 +1,10 @@ use clap::{Args, Subcommand}; mod list; +mod set_anonymous_read; pub use list::{run as list_run, ListArgs}; +pub use set_anonymous_read::{run as set_anonymous_read_run, SetAnonymousReadArgs}; #[derive(Args, Debug)] pub struct AssetArgs { @@ -13,5 +15,7 @@ pub struct AssetArgs { #[derive(Subcommand, Debug)] pub enum AssetCommand { List(ListArgs), + /// Set anonymous read permission for an asset (allow unauthenticated read). + SetAnonymousRead(SetAnonymousReadArgs), } diff --git a/src/commands/asset/set_anonymous_read.rs b/src/commands/asset/set_anonymous_read.rs new file mode 100644 index 0000000..81dc761 --- /dev/null +++ b/src/commands/asset/set_anonymous_read.rs @@ -0,0 +1,60 @@ +use clap::Args; +use tellers_api_client::models::AssetVisibilityRequest; + +use crate::commands::api_config; + +#[derive(Args, Debug)] +pub struct SetAnonymousReadArgs { + /// Asset ID to set anonymous read for + pub asset_id: String, + + /// Set anonymous read to true or false (default: true). + #[arg(long, default_value_t = true, value_parser = clap::value_parser!(bool))] + pub allow: bool, + + #[arg(long, env = "TELLERS_API_KEY", hide = true)] + pub api_key: Option, + + #[arg(long, env = "TELLERS_AUTH_BEARER", hide = true)] + pub auth_bearer: Option, +} + +pub fn run(args: SetAnonymousReadArgs) -> Result<(), String> { + let base = api_config::get_api_base(); + let api_key = api_config::get_api_key(args.api_key)?; + let bearer = api_config::get_bearer_header(args.auth_bearer); + + let url = format!( + "{}/asset/{}/visibility", + base.trim_end_matches('/'), + args.asset_id + ); + let body = AssetVisibilityRequest::new(args.allow); + + tokio::runtime::Runtime::new() + .map_err(|e| format!("failed to start runtime: {}", e))? + .block_on(async move { + let client = reqwest::Client::new(); + let mut req = client + .put(&url) + .json(&body) + .header("x-api-key", &api_key); + if let Some(ref b) = bearer { + req = req.header("authorization", b); + } + let resp = req.send().await.map_err(|e| format!("request failed: {}", e))?; + let status = resp.status(); + if !status.is_success() { + let text = resp.text().await.unwrap_or_default(); + return Err(format!( + "set_anonymous_read failed; http_status: {}; response: {}", + status, text + )); + } + println!( + "anonymous_read set to {} for asset {}", + args.allow, args.asset_id + ); + Ok(()) + }) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 18b7ee7..aa83642 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod api_config; pub mod asset; pub mod prompt; +pub mod project; pub mod upload; diff --git a/src/commands/project/export.rs b/src/commands/project/export.rs new file mode 100644 index 0000000..ef3ae26 --- /dev/null +++ b/src/commands/project/export.rs @@ -0,0 +1,92 @@ +use clap::Args; +use tellers_api_client::apis::accepts_api_key_api as api; + +use crate::commands::api_config; + +#[derive(Args, Debug)] +pub struct ExportArgs { + /// Project ID to export + #[arg(value_name = "PROJECT_ID")] + pub project_id: String, + + /// Renditions to export (360p, 480p, 720p, 1080p, 1440p, 4k). Default 1080p when omitted. + #[arg(long = "rendition", short = 'r', value_name = "RESOLUTION")] + pub renditions: Vec, + + #[arg(long, env = "TELLERS_API_KEY")] + pub api_key: Option, + + #[arg(long, env = "TELLERS_AUTH_BEARER")] + pub auth_bearer: Option, +} + +const ALLOWED_RENDITIONS: &[&str] = &["360p", "480p", "720p", "1080p", "1440p", "4k"]; + +fn parse_rendition(s: &str) -> Result { + let v = s.trim().to_lowercase(); + if ALLOWED_RENDITIONS.contains(&v.as_str()) { + Ok(v) + } else { + Err(format!( + "Invalid rendition '{}'; allowed: {}", + s, + ALLOWED_RENDITIONS.join(", ") + )) + } +} + +pub fn run(args: ExportArgs) -> Result<(), String> { + let cfg = api_config::create_config(); + let api_key = api_config::get_api_key(args.api_key)?; + let bearer_header = api_config::get_bearer_header(args.auth_bearer); + + let rendition_strs: Vec = if args.renditions.is_empty() { + vec!["1080p".to_string()] + } else { + args.renditions + .iter() + .flat_map(|s| s.split(',').map(|s| s.trim().to_string())) + .filter(|s| !s.is_empty()) + .collect() + }; + + let renditions: Vec = rendition_strs + .iter() + .map(|s| parse_rendition(s)) + .collect::, _>>()?; + + tokio::runtime::Runtime::new() + .map_err(|e| format!("failed to start runtime: {}", e))? + .block_on(async move { + let resp = api::export_project_project_project_id_export_post( + &cfg, + &args.project_id, + renditions, + Some(&api_key), + bearer_header.as_deref(), + ) + .await + .map_err(|e| { + let mut m = format!("export failed: {}", e); + match &e { + tellers_api_client::apis::Error::Reqwest(req_err) => { + if let Some(status) = req_err.status() { + m.push_str(&format!("; http_status: {}", status)); + } + } + tellers_api_client::apis::Error::ResponseError(resp) => { + m.push_str(&format!("; http_status: {}", resp.status)); + if !resp.content.is_empty() { + m.push_str(&format!("; response: {}", resp.content)); + } + } + _ => {} + } + m + })?; + + println!("task_id: {}", resp.task_id); + println!("asset_id: {}", resp.asset_id); + Ok(()) + }) +} diff --git a/src/commands/project/mod.rs b/src/commands/project/mod.rs new file mode 100644 index 0000000..97e0c76 --- /dev/null +++ b/src/commands/project/mod.rs @@ -0,0 +1,17 @@ +use clap::{Args, Subcommand}; + +mod export; + +pub use export::{run as export_run, ExportArgs}; + +#[derive(Args, Debug)] +pub struct ProjectArgs { + #[command(subcommand)] + pub command: ProjectCommand, +} + +#[derive(Subcommand, Debug)] +pub enum ProjectCommand { + /// Export project to MP4 at one or more resolutions (optionally into a project folder). + Export(ExportArgs), +} diff --git a/src/main.rs b/src/main.rs index 605a1a9..e9b6d29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,22 @@ fn main() { std::process::exit(1); } } + commands::asset::AssetCommand::SetAnonymousRead(args) => { + if let Err(error) = commands::asset::set_anonymous_read_run(args) { + eprintln!("error: {}", error); + std::process::exit(1); + } + } + } + } + Some(cli::Command::Project(project_args)) => { + match project_args.command { + commands::project::ProjectCommand::Export(export_args) => { + if let Err(error) = commands::project::export_run(export_args) { + eprintln!("error: {}", error); + std::process::exit(1); + } + } } } Some(cli::Command::Upload(upload_args)) => { diff --git a/src/tellers_api/openapi.tellers_public_api.yaml b/src/tellers_api/openapi.tellers_public_api.yaml index 2366c77..fff2117 100644 --- a/src/tellers_api/openapi.tellers_public_api.yaml +++ b/src/tellers_api/openapi.tellers_public_api.yaml @@ -308,6 +308,103 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /asset/{asset_id}/visibility: + put: + tags: + - accepts-api-key + summary: Set Asset Visibility + description: Set visibility (is_public_read) for a single asset (SSE). WRITE + required. + operationId: set_asset_visibility_asset__asset_id__visibility_put + parameters: + - name: asset_id + in: path + required: true + schema: + type: string + title: Asset Id + - name: x-api-key + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Api-Key + - name: authorization + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Authorization + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AssetVisibilityRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /asset/{asset_id}/reference: + put: + tags: + - accepts-api-key + summary: Set Asset Reference + description: Set reference flag for a single asset (SSE). WRITE required. + operationId: set_asset_reference_asset__asset_id__reference_put + parameters: + - name: asset_id + in: path + required: true + schema: + type: string + title: Asset Id + - name: x-api-key + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Api-Key + - name: authorization + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Authorization + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AssetReferenceRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' /asset/most_recent: get: tags: @@ -459,6 +556,77 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /project/{project_id}/export: + post: + tags: + - accepts-api-key + summary: Export Project + description: 'Start async export of project to MP4 at one or more resolutions. + Creates the export asset + + in the API, then enqueues a task that generates each rendition. The export + appears as a + + new video asset in the project''s export folder (project name + date + .mp4), + with multiple + + renditions (e.g. 480p, 720p, 1080p, 4k) attached to that asset. + + + Returns task_id and asset_id. Poll GET /task/{task_id} for completion; result + includes asset_id.' + operationId: export_project_project__project_id__export_post + parameters: + - name: project_id + in: path + required: true + schema: + type: string + title: Project Id + - name: x-api-key + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Api-Key + - name: authorization + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Authorization + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + enum: + - 480p + - 720p + - 1080p + - 4k + type: string + description: List of resolutions to export (e.g. ["720p", "1080p"]) + title: Renditions + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/ExportProjectResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' /agent/response/{chat_id}: delete: tags: @@ -994,6 +1162,94 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /create: + get: + tags: + - accepts-api-key + summary: Create Agent Stream + description: 'SSE stream for agent creation. GET with query params; same behavior + as + + POST /agent/response/json. Emits event: tellers.json_result (progressing, + then done).' + operationId: create_agent_stream_create_get + parameters: + - name: prompt + in: query + required: true + schema: + type: string + description: The message/prompt to send to the agent + title: Prompt + description: The message/prompt to send to the agent + - name: tools + in: query + required: false + schema: + anyOf: + - type: array + items: + type: string + - type: 'null' + description: Tool IDs to use. If omitted, defaults to available agent tools. + title: Tools + description: Tool IDs to use. If omitted, defaults to available agent tools. + - name: llm_model + in: query + required: false + schema: + type: string + description: The LLM model to use for the response. + default: gpt-5.4-2026-03-05 + title: Llm Model + description: The LLM model to use for the response. + - name: parallel_tool_calls + in: query + required: false + schema: + type: boolean + description: Whether to allow parallel tool calls. + default: false + title: Parallel Tool Calls + description: Whether to allow parallel tool calls. + - name: chat_id + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + description: Optional chat ID for conversation tracking. Created if omitted. + title: Chat Id + description: Optional chat ID for conversation tracking. Created if omitted. + - name: x-api-key + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Api-Key + - name: authorization + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Authorization + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' /settings: get: tags: @@ -1323,6 +1579,15 @@ components: - available_agent_tools - available_llm_models title: AppSettings + AssetReferenceRequest: + properties: + is_reference: + type: boolean + title: Is Reference + type: object + required: + - is_reference + title: AssetReferenceRequest AssetUploadRequest: properties: content_length: @@ -1394,6 +1659,15 @@ components: - upload_id - asset_id title: AssetUploadResponse + AssetVisibilityRequest: + properties: + is_public_read: + type: boolean + title: Is Public Read + type: object + required: + - is_public_read + title: AssetVisibilityRequest ChatHistoryResponse: properties: id: @@ -1531,6 +1805,21 @@ components: - id - path title: CreateFolderResponse + ExportProjectResponse: + properties: + task_id: + type: string + title: Task Id + asset_id: + type: string + title: Asset Id + type: object + required: + - task_id + - asset_id + title: ExportProjectResponse + description: 'Response for POST /project/{project_id}/export: task_id and the + new export asset_id.' FileReference: properties: file_name: