From 52313a82a665c04c38c072055718be58fef8a630 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Sun, 1 Mar 2026 10:43:04 +0100 Subject: [PATCH] Add --title, --audio, --description, --audio-url to the `upload` command --- src/api.rs | 50 +++++++++++++++++++++++++++++++++++++++++++--- src/cli.rs | 33 ++++++++++++++++++++++++++---- src/cmd/session.rs | 8 ++++---- src/cmd/upload.rs | 18 +++++++++++++++-- 4 files changed, 96 insertions(+), 13 deletions(-) diff --git a/src/api.rs b/src/api.rs index 432c550..1e626fd 100644 --- a/src/api.rs +++ b/src/api.rs @@ -32,6 +32,18 @@ pub enum Visibility { Private, } +#[derive(Default, Serialize)] +pub struct RecordingChangeset { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub visibility: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub audio_url: Option>, +} + #[derive(Default, Serialize)] pub struct StreamChangeset { #[serde(skip_serializing_if = "Option::is_none")] @@ -66,11 +78,15 @@ pub fn get_auth_url(config: &mut Config) -> Result { Ok(url) } -pub async fn create_recording(path: &str, config: &mut Config) -> Result { +pub async fn create_recording( + path: &str, + changeset: RecordingChangeset, + config: &mut Config, +) -> Result { let server_url = &config.get_server_url()?; let install_id = config.get_install_id()?; - let response = create_recording_request(server_url, path, install_id) + let response = create_recording_request(server_url, install_id, path, changeset) .await? .send() .await?; @@ -94,18 +110,46 @@ pub async fn create_recording(path: &str, config: &mut Config) -> Result Result { let client = Client::new(); let mut url = server_url.clone(); url.set_path("api/v1/recordings"); let form = Form::new().file("file", path).await?; + let form = add_recording_changeset_fields(form, changeset); let builder = client.post(url).multipart(form); Ok(add_headers(builder, &install_id)) } +fn add_recording_changeset_fields(mut form: Form, changeset: RecordingChangeset) -> Form { + if let Some(Some(title)) = changeset.title { + form = form.text("title", title); + } + + if let Some(Some(description)) = changeset.description { + form = form.text("description", description); + } + + if let Some(visibility) = changeset.visibility { + let visibility = match visibility { + Visibility::Public => "public", + Visibility::Unlisted => "unlisted", + Visibility::Private => "private", + }; + + form = form.text("visibility", visibility); + } + + if let Some(Some(audio_url)) = changeset.audio_url { + form = form.text("audio_url", audio_url); + } + + form +} + pub async fn list_user_streams(prefix: &str, config: &mut Config) -> Result> { let server_url = config.get_server_url()?; let install_id = config.get_install_id()?; diff --git a/src/cli.rs b/src/cli.rs index cc6baf8..cf2ad3c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -408,7 +408,7 @@ pub struct Stream { /// Set the visibility level for the stream (applies to remote streaming with --remote). Public streams appear in listings and on your profile page. Unlisted streams are accessible via direct URL but don't appear in listings. Private streams are only accessible to the owner. #[arg(long, value_enum, help = "Visibility level", long_help)] - pub visibility: Option, + pub visibility: Option, /// Specify URL of a live audio stream (e.g., Icecast MP3/OGG) to synchronize with the terminal stream (applies to remote streaming with --remote). When set, viewers can listen to audio commentary while watching the terminal. The audio URL is stored in the stream metadata and used by the player for synchronized playback. For example: --audio-url https://icecast.example.com/live.mp3 #[arg( @@ -444,9 +444,9 @@ pub struct Stream { pub server_url: Option, } -/// Visibility level for streams +/// Visibility level for uploads and streams #[derive(Debug, Clone, Copy, clap::ValueEnum)] -pub enum StreamVisibility { +pub enum Visibility { Public, Unlisted, Private, @@ -533,7 +533,7 @@ pub struct Session { /// Set the visibility level for the stream (applies to remote streaming with --stream-remote). Public streams appear in listings and on your profile page. Unlisted streams are accessible via direct URL but don't appear in listings. Private streams are only accessible to the owner. #[arg(long, value_enum, help = "Stream visibility level", long_help)] - pub visibility: Option, + pub visibility: Option, /// Specify URL of a live audio stream (e.g., Icecast MP3/OGG) to synchronize with the terminal stream (applies to remote streaming with --stream-remote). When set, viewers can listen to audio commentary while watching the terminal. The audio URL is stored in the stream metadata and used by the player for synchronized playback. For example: --audio-url https://icecast.example.com/live.mp3 #[arg( @@ -622,6 +622,31 @@ pub struct Upload { /// The path to the asciicast recording file to upload, in a supported asciicast format (v1, v2, or v3). pub file: String, + /// Set a title for the recording that will be stored in the recording metadata and displayed to the viewers. For example: --title "Installing Podman on Ubuntu". This option takes precedence over the "title" field from the recording file itself. + #[arg(short, long, help = "Title of the recording", long_help)] + pub title: Option, + + /// Set a description for the recording. This description is displayed on the recording page and can include formatting, links, and code blocks. Useful for providing context, instructions, or documentation for viewers. + #[arg( + long, + help = "Description of the recording (Markdown supported)", + long_help + )] + pub description: Option, + + /// Set the visibility level for the recording. Public recordings appear in listings, search results and on your profile page. Unlisted recordings are accessible via direct URL but don't appear in listings. Private recordings are only accessible to the owner. + #[arg(long, value_enum, help = "Recording visibility level", long_help)] + pub visibility: Option, + + /// Specify URL of an audio file (e.g., MP3/OGG) to synchronize with the terminal playback. When set, viewers can listen to audio commentary while watching the terminal. The audio URL is stored in the recording metadata and used by the player for synchronized playback. For example: --audio-url https://example.com/commentary.mp3 + #[arg( + long, + value_name = "URL", + help = "Audio stream URL for synchronized playback", + long_help + )] + pub audio_url: Option, + /// Specify a custom asciinema server URL for uploading to self-hosted servers. Use the base server URL (e.g., https://asciinema.example.com). Can also be set via environment variable ASCIINEMA_SERVER_URL or config file option server.url. If no server URL is configured via this option, environment variable, or config file, you will be prompted to choose one (defaulting to asciinema.org), which will be saved as a default. #[arg(long, value_name = "URL", help = "asciinema server URL", long_help)] pub server_url: Option, diff --git a/src/cmd/session.rs b/src/cmd/session.rs index 83b41d9..4ead302 100644 --- a/src/cmd/session.rs +++ b/src/cmd/session.rs @@ -16,7 +16,7 @@ use url::Url; use crate::api::{self, StreamChangeset, StreamResponse, Visibility}; use crate::asciicast::{self, Version}; -use crate::cli::{self, Format, RelayTarget, StreamVisibility}; +use crate::cli::{self, Format, RelayTarget}; use crate::config::{self, Config}; use crate::encoder::{AsciicastV2Encoder, AsciicastV3Encoder, Encoder, RawEncoder, TextEncoder}; use crate::file_writer::FileWriter; @@ -340,9 +340,9 @@ impl cli::Session { }; let visibility = self.visibility.map(|v| match v { - StreamVisibility::Public => Visibility::Public, - StreamVisibility::Unlisted => Visibility::Unlisted, - StreamVisibility::Private => Visibility::Private, + cli::Visibility::Public => Visibility::Public, + cli::Visibility::Unlisted => Visibility::Unlisted, + cli::Visibility::Private => Visibility::Private, }); let changeset = StreamChangeset { diff --git a/src/cmd/upload.rs b/src/cmd/upload.rs index f17913c..4bca360 100644 --- a/src/cmd/upload.rs +++ b/src/cmd/upload.rs @@ -1,7 +1,7 @@ use anyhow::Result; use tokio::runtime::Runtime; -use crate::api; +use crate::api::{self, RecordingChangeset}; use crate::asciicast; use crate::cli; use crate::config::Config; @@ -14,7 +14,21 @@ impl cli::Upload { async fn do_run(self) -> Result<()> { let mut config = Config::new(self.server_url.clone())?; let _ = asciicast::open_from_path(&self.file)?; - let response = api::create_recording(&self.file, &mut config).await?; + + let visibility = self.visibility.map(|v| match v { + cli::Visibility::Public => api::Visibility::Public, + cli::Visibility::Unlisted => api::Visibility::Unlisted, + cli::Visibility::Private => api::Visibility::Private, + }); + + let changeset = RecordingChangeset { + title: self.title.map(Some), + description: self.description.map(Some), + visibility, + audio_url: self.audio_url.map(Some), + }; + + let response = api::create_recording(&self.file, changeset, &mut config).await?; println!("{}", response.message.unwrap_or(response.url)); Ok(())