CLI Video Downloader in Rust: A Step-by-Step Tutorial

Eleftheria Batsou
6 min readApr 12, 2024

Introduction

Hello Rust friends and awesome people, welcome to our step-by-step tutorial on building a CLI video downloader in Rust! Whether you’re new to Rust or looking to expand your project portfolio, this guide will walk you through creating a functional command-line application that downloads videos.

We’ll cover setting up your project, parsing command-line arguments, making HTTP requests, and handling errors. By the end of this tutorial, you’ll have a deeper understanding of Rust’s powerful features and how to apply them in real-world scenarios. Let’s dive in!

Project Configuration (Cargo.toml)

The Cargo.toml file specifies our project's configuration, including the name, version, edition, and dependencies. We've included crates like clap for command-line argument parsing, reqwest for making HTTP requests, tokio for asynchronous operations, and serde for JSON parsing, among others.

[dependencies]
clap = "4.3.21"
dirs = "5.0.1"
tokio = { version = "1.15", features = ["full"] }
reqwest = { version = "0.11", features = ["json", "stream"] }
serde_json = "1.0.105"
serde = { version = "1.0.183", features = ["derive"] }
futures-util = "0.3.28"

Understanding the Main Logic (main.rs)

Our journey begins with the main.rs file, the heart of our CLI application. This file orchestrates the overall flow of the application, starting from parsing user input to calling the appropriate download functions based on the mode specified by the user.

Importing Necessary Crates and Modules:

  • The code begins by importing the clap crate for command-line argument parsing and std::env for accessing environment variables.
  • It also imports two local modules, download and errors, which contain the logic for downloading videos and handling errors, respectively.
    use clap::{Arg, Command};
use std::env;

mod download;
mod errors;

Setting Static Variables:

  • Two static variables, DEBUG and PREFIX, are defined. DEBUG is a boolean flag used for enabling or disabling debug mode, and PREFIX is a string literal used as a prefix in debug messages.
    static DEBUG: bool = false;
static PREFIX: &'static str = "[download-cli]";

Command-Line Argument Configuration:

  • The Command structure from clap is used to set up the CLI interface, including the application name, version, author, and description, all pulled from environment variables set by Cargo.
  • Several command-line arguments are defined using Arg::new(), including "mode", "apiurl", "path", "url", and others related to video and audio quality and format. Each argument is configured with options like .short(), .long(), and .help() to define how they are used.
    let matches = Command::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION"))
.author(env!("CARGO_PKG_AUTHORS"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.arg(Arg::new("mode").short('m').long("mode").help("set which mode to download with (default: auto, other: audio)"))
.arg(Arg::new("apiurl").short('a').long("apiurl").help("set api url, don't include https (default: co.wuk.sh)"))
.arg(Arg::new("path").short('p').long("path").help("path to save files to (default: ~/Downloads/)"))
.arg(Arg::new("url").short('u').long("url").help("url to download from"))
.arg(Arg::new("quality").short('q').long("quality").help("set video quality (default: 1080p, other: 4320p+, 2160p, 720p, 480p, 360p)"))
.arg(Arg::new("codec").short('c').long("codec").help("set youtube codec (default: h264, other: av1, vp9)"))
.arg(Arg::new("ttwatermark").short('w').long("ttwatermark").num_args(0).help("disable tiktok watermark (default: false)"))
.arg(Arg::new("audioformat").short('f').long("audioformat").help("set audio format (default: mp3, other: best, ogg, wav, opus)"))
.arg(Arg::new("dublang").short('d').long("dublang").num_args(0).help("dub language (default: false)"))
.arg(Arg::new("fullaudio").short('k').long("fullaudio").num_args(0).help("get tiktok full audio (default: false)"))
.arg(Arg::new("mute").short('j').long("mute").num_args(0).help("mute audio when possible (default: false)"))
.get_matches();

Parsing Command-Line Arguments:

  • The get_matches method is called on the Command instance to parse the command-line arguments provided by the user and store them in the matches variable.

Determining the Home Directory:

  • The dirs::home_dir() function is used to find the user's home directory, providing a default save path for downloaded files. This path is stored in homedir.
    let homedirpathbuf = dirs::home_dir();
let homedirexpect = homedirpathbuf.expect("method not found in `Option<PathBuf>`");
let homedir = homedirexpect.display();

Extracting and Validating Arguments:

  • The code extracts values for various command-line arguments (like “mode”, “apiurl”, “path”, and “url”). If essential arguments like “mode” or “url” are not specified, the errors::create_end function is called to display an error message and exit the application.
  • For some arguments, default values are provided if the user does not specify them.
    let mut mode = "unspecified".to_string();
if matches.get_one::<String>("mode").is_none() {
errors::create_end("you didn't specify a mode");
} else {
mode = matches.get_one::<String>("mode").unwrap().to_string();
}

Handling Boolean Flags:

  • Boolean flags such as “ttwatermark”, “dublang”, “fullaudio”, and “mute” are handled by checking if they were set in the command-line arguments and updating the corresponding boolean variables accordingly.
    let mut ttwatermark = false;
if matches.get_flag("ttwatermark") {
ttwatermark = true;
} else {
ttwatermark = false;
}r

Debugging Information:

  • If the DEBUG flag is true, the application prints out all the relevant configuration information, including the mode, API URL, path, and other settings. This is useful for development and troubleshooting.
    if DEBUG {
println!(" ");
println!("{PREFIX} {}", "====[ debug ]====");
// Printing out configuration information
println!("{PREFIX} {}", "====[ debug ]====");
println!(" ");
}

Mode Selection and Execution:

  • The application checks the “mode” argument to determine whether to perform an “auto” or “audio” download. Depending on the mode, it calls the appropriate function from the download module, passing all the necessary parameters.
  • If an invalid mode is specified, the errors::create_end function is called to display an error message and exit.
    if mode == "auto" {
download::auto(PREFIX, DEBUG, &apiurl, &path, &url, &quality, &codec, ttwatermark, &audioformat, dublang, fullaudio, mute)
} else if mode == "audio" {
download::audio(PREFIX, DEBUG, &apiurl, &path, &url, &quality, &codec, ttwatermark, &audioformat, dublang, fullaudio, mute)
} else {
errors::create_end("invalid mode. options: auto, audio");
}

This structured approach makes the application flexible and user-friendly, allowing users to specify various options for downloading videos or audio according to their preferences.

The Download Logic (download.rs)

The download.rs file contains the core logic for downloading videos or audio. Here, we make HTTP requests to the specified API URL and handle the responses.

Setting Up Asynchronous Environment:

The code snippet starts with the #[tokio::main] attribute, which sets up an asynchronous runtime for our function. This is necessary for making asynchronous network requests with reqwest.

  #[tokio::main]

Function Definition -getstream:

The getstream function is defined with parameters including a prefix (for logging), the URL of the API to post to, a body (as a HashMap containing parameters for the request), and the path where the downloaded content should be saved.

  async fn getstream(prefix: &str, url: &str, body: HashMap<&str, &str>, path: &str) {

Creating HTTP Client and Sending Request:

A new reqwest::Client is instantiated to make HTTP requests. The client sends a POST request to the specified URL, including headers for content type and acceptance, and the body serialized as JSON. The await keyword is used to asynchronously wait for the response.

  let client = reqwest::Client::new();
let response = client.post(url)
.header("CONTENT_TYPE", "application/json")
.header("ACCEPT", "application/json")
.json(&body)
.send()
.await;

Handling the Response:

The response is processed to extract the text content, which is then parsed as JSON using serde_json. The code checks if the status indicates a stream URL is available. If so, it proceeds to extract the URL and download the stream.

  let formatted_response = response.expect("method not found in `Result<Response, Error>`").text().await.unwrap();
let fmtd_res2: Value = serde_json::from_str(&formatted_response).unwrap();
if fmtd_res2.get("status").unwrap() == "stream" {
let streamurl = fmtd_res2.get("url").unwrap().to_string();
let streamurl: &str = &streamurl[1..streamurl.len() - 1];

Downloading the Stream:

Upon successfully fetching the stream URL, the downloadfromstream function is called to download the content. The await keyword is used again for asynchronous operation. The result of this operation is printed for debugging purposes.

  let idk: std::result::Result<(), Box<dyn Error + Send + Sync>> = downloadfromstream(prefix, &streamurl.to_string(), path).await;
println!("{:?}", idk);

Error Handling:

If the process fails at any point, such as not receiving a valid stream URL, the errors::create_end function is called to display an error message and terminate the application.

  } else {
errors::create_end(&format!("{} failed to get stream url. {}", prefix, fmtd_res2.get("text").unwrap()).as_str());
}

Error Handling (errors.rs)

The file contains a simple yet effective error handling function, create_end. This function prints a custom error message prefixed with [download-cli] and then terminates the application. It ensures that any issues are communicated clearly to the user, maintaining a good user experience even in failure scenarios.

Function Definition -create_end:

  • This function takes a string slice (&str) as an argument, which represents the error message to be displayed.
  pub fn create_end(message: &str) {

Printing the Error Message:

  • The function prints the error message to the console, prefixed with [download-cli] to identify the source of the message. This prefix helps in distinguishing application-specific messages from other console outputs.
  println!("[download-cli] uh-oh! {message}");

Terminating the Application:

  • After displaying the error message, the function calls std::process::exit(0); to terminate the application. The exit code 0 is typically used to indicate a normal termination, but in the context of error handling, you might consider using a non-zero exit code (e.g., 1) to indicate that the program ended due to an error.
  std::process::exit(0);

Conclusion

Overall, the program uses HTTP requests to communicate with an API for fetching video/audio stream URLs, downloads the streams using reqwest, and saves them to the specified path. It utilizes command-line arguments for user configuration and clap for parsing these arguments.

Building a CLI video downloader in Rust is a fantastic project for beginners looking to apply their Rust skills. Through this tutorial, you’ve learned how to parse command-line arguments, perform asynchronous HTTP requests, handle errors gracefully, and work with external crates.

Happy Rust Coding! 🤞🦀

👋 Hello, I’m Eleftheria, Community Manager, developer, public speaker, and content creator.

🥰 If you liked this article, consider sharing it.

🔗 All links | X | LinkedIn

Originally published at https://eleftheriabatsou.hashnode.dev on April 12, 2024.

--

--

Eleftheria Batsou

Hi, I’m a community manager and an app developer/UX researcher by passion. I love learning, teaching and sharing. My passions are tech, UX, arts & working out.