Video downloader with Tauri

Published

Table of Contents

In this tutorial, we're going to be building a simple video downloader with Tauri and rustube. You could, theoretically, use it to download videos from YouTube.*

Here's the standard disclaimer that downloading videos from YouTube is against YouTube policy.

This tutorial assumes you have a basic understanding of HTML, CSS, JS, and Rust.

Prerequisites

Before you start, make sure you have the prerequisites for Tauri installed. See https://tauri.app/v1/guides/getting-started/prerequisites/. Basically you have to have some system dependencies and Rust installed.

I'm going to be using VS Code for this tutorial. If you're using VS Code, you'll probably want the rust-analyzer and Tauri extensions installed.

Setting up Tauri

Since I'm most familiar with npm, I'll be using that to set up Tauri. First, open a terminal in the directory where you want this project to go and run

npm create tauri-app

and follow the prompts. I'm using the VS Code bash terminal.

For this project, I named it video-downloader, chose npm as the pacakge manager, and used the vanilla template.
undefined

undefined

undefined

After that finishes, if you're using VS Code, you probably want to do something like

cd video-downloader
code .

or just close whatever terminal you're using and open the video-downloader folder with VS Code.

Once you finish setting up Tauri (if you're using VS Code), the rust-analyzer extension will take some time to... analyze Rust, I guess. On my laptop this takes quite a bit of processing power and slows everything else down for a little while. My laptop is not a high-performance computer.

Don't forget to run npm install!

npm install

Installing Rustube

Rust's package manager, Cargo, is bundled with Rust. Since you should have Rust installed at this point, we can use Cargo to install rustube.

Simply run

cd src-tauri
cargo add rustube

You have to go into the src-tauri directory first, otherwise it'll just say "Could not find 'cargo.toml'". Also, don't use cargo install, because at least as of this writing, rustube is a library and not a compiled to binary crate.

Alternatively, just go into src-tauri/Cargo.toml and add rustube under [dependencies]. (v0.6.0 is the most recent as of this writing)

# src-tauri/Cargo.toml

[dependencies]
# other dependencies...
rustube = "0.6.0"

It might take a while for Tauri/Rust to check your dependencies and rebuild.

Starting Tauri

Again, don't forget to run npm install. Because I'm lazy but like to think I'm efficient, I run npm i instead. (It's probably a bit of both, actually)

Next, run

npm run tauri dev

so we can see what we're working on. The first time you run this Tauri has to compile everything, so it might take a bit. On my computer it took 2 minutes and 20 seconds. When it's done compiling, it should open a window that looks something like this:

undefined

Basic Downloader

Reset everything

First, go into src/index.html and delete everything inside the body and also the inline style tag in the head. I also went ahead and changed the title to "Video Downloader" even though it doesn't change anything.

This is what my index.html looks like now:

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<link rel="stylesheet" href="style.css" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Video Downloader</title>
		<script type="module" src="/main.js" defer></script>
	</head>

	<body>
		
	</body>
</html>

Now, hop over to main.js and delete everything except for the (fake) import statement at the top and the DOMContentLoaded listener.

// src/main.js

const { invoke } = window.__TAURI__.tauri;

window.addEventListener("DOMContentLoaded", () => {
	
}

Finally, go into src-tauri/src/main.rs and delete the greet function and remove the greet function from the tauri::generate_handler! call. It should look like this when you're done:

// src-tauri/src/main.rs

#![cfg_attr(
    all(not(debug_assertions), target_os = "windows"),
    windows_subsystem = "windows"
)]

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Note: if you change I think any file in the src-tauri directory, Tauri re-compiles the app, which means the app window closes while the app is being rebuilt.

HTML and CSS

Next, let's add the HTML for everything. Put the below code in the body. We have a div for displaying the save location, a button to select the save location, a text input for the URL, a download button, and a div to show a success message.

<!--src/index.html-->

<h1>Download a video</h1>
<div class="column">
	<div id="save-location"></div>
	<button id="select-save-location">Select save location</button>
	<form id="downloadForm" class="column">
		<label>Video URL: <br><input type="text" name="url" required></label>
		<button>Download</button>
	</form>
	<div id="success"></div>
</div>

and just to make things look nice, put this somewhere in style.css:

/* src/style.css */

.column {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 2ch;
}

It should look something like this when you're done:
undefined

Choosing a Save Location

Next, we'll make it so the user can choose a save location. Go into src/main.js and add the following code between the fake import at the top and the DOMContentLoaded event listener.

// src/main.js

const { dialog } = window.__TAURI__;

let path;

then, add the next bit of code inside the DOMContentLoaded listener.

// src/main.js

document.getElementById('select-save-location').addEventListener('click', async () => {
    path = await dialog.open({
      directory: true,
      multiple: false,
      title: 'Choose download location',
    });
    document.getElementById('save-location').textContent = path;
  });

open is the type of Tauri dialog that opens files or folders. By default it selects files, but by setting directory to true, it lets us select folders (or directories). I set multiple to false because it doesn't really make sense to select multiple download locations for one file, but if you want to do something weird with it, go ahead.

That's all the code we need for the user to be able to select a save location, but it won't work just yet. That's because Tauri only bundles the absolutely necessary core modules by default to try to keep the binary size down. For the folder dialog to work, we need to go into src-tauri/tauri.conf.json and add the following to tauri.allowlist. See https://tauri.app/v1/api/js/dialog/ for more details.

// src-tauri/tauri.conf.json

"dialog": {
	"all": false,
	"ask": false,
	"confirm": false,
	"message": false,
	"open": true,
	"save": false
}

Note that "open" is the only one set to true, and honestly I'm not sure if the other ones even need to be there (they don't). After you make that change, Tauri has to recompile. On my computer it took about a minute for this change.

Now the save location selection should work properly. When you click the button a folder selection dialog should show up, and once you select a folder the app should display the path for your selection.
undefined
Of course, it should display your actual username if you select it. I just don't feel like leaking my Windows username for this blog post.

Video downloading

Finally, we get to the important part of this tutorial. I mean, all the parts are important, but this one is the titular part.

JS (Render) Side

First, add this code in the DOMContentLoaded listener:

// src/main.js

document.forms.downloadForm.addEventListener('submit', async (e) => {
    const url = new FormData(e.target).get('url');
    if (!url || !path) {
      return;
    }
    e.preventDefault();
    const success = await invoke('download_video', {
      url,
      path,
    });
    document.getElementById('success').textContent = success;
  });

I'm using the document.forms interface to grab the form, because it's shorter than using querySelector or getElementById. Not a great reason to use it, but whatever. I'm using a submit event listener because it will trigger when you click the download button or if you hit enter while focused on the form, which feels more natural to me.

First, we check if there's anything in the URL input with new FormData. I use e.target again because it's shorter and I don't think it can go wrong even though TypeScript doesn't like it. If there's no URL or no file path, just return early. If there's no URL, this lets us take advantage of the browser's built-in form validation since we marked the URL input as required.

If both the URL and the file path exist, then we preventDefault, which stops the form from reloading the page. Finally, we invoke the download_video function we're just about to write, and display the result.

Rust (Main) Side

First, we have to import the required stuff from rustube. Add this somewhere at the top of main.rs.

// src-tauri/src/main.rs

use rustube::{Id, VideoFetcher};

Next, in main.rs, add the following function above the main function:

// src-tauri/src/main.rs

#[tauri::command]
async fn download_video(url: &str, path: &str) -> Result<String, String> {
    let id = match Id::from_raw(url) {
        Ok(id) => id,
        Err(err) => panic!("{}", err.to_string()),
    };
    let vidfetcher = match VideoFetcher::from_id(id.into_owned()) {
        Ok(descram) => descram,
        Err(err) => panic!("{}", err.to_string()),
    };
    let descrambler = match vidfetcher.fetch().await {
        Ok(fetched) => fetched,
        Err(err) => panic!("{}", err.to_string()),
    };
    let video = match descrambler.descramble() {
        Ok(vid) => vid,
        Err(err) => panic!("{}", err.to_string()),
    };

    let best_stream = video.best_quality().unwrap();

    match best_stream.download_to_dir(path).await {
        Ok(path) => Ok(format!("Downloaded to {:?}", path)),
        Err(err) => panic!("{}", err.to_string()),
    }
}

There might be a better way to hand all the possible errors. Actually, there's definitely a better way than just panicking at every error. Unfortunately, the way Tauri works, every possible return value from a Tauri command must be serializable, including any errors. This is the easy, lazy way because I'm not that good at Rust.

Anyway, what this does is basically extract the video ID, create a new VideoFetcher, use that to fetch the video details and descramble them, then selects the best quality video + audio and downloads that to the selected directory. It then sends a success message back to the JavaScript side, or an error message if something went wrong with the downloading. You can consult the rustube docs for more detail.

Finally, we have to expose the command so we can invoke it with JS, so add download_video to the tauri::generate_handler! call, where greet was originally. The line should look like this:

// src-tauri/src/main.rs

.invoke_handler(tauri::generate_handler![download_video])

Basically Done!

That's it! It should work now. Just select a download path, paste in the URL, and click Download. Also, there's very much an intended pun there, because you're only down with the basics. I have a much more refined version that I use.

Improvements

It should work at this point, but there are several improvements that could be made. This is left as an exercise for the reader.

Path validation

if you don't select a path before trying to download a video, it just fails silently. You could do a simple alert (you have to allow message dialogs in tauri.conf.json for that) or something more sleek.

Progress Callbacks

You could also try to get progress callbacks to work with rustube. I couldn't get them to work on my system for some reason. It just tries to tell me they don't exist.

Better File Names

Currently, it just saves the files as <video_id>.mp4 (or .webm or whatever). You could use the more advanced features of rustube to get the video title and details and generate a better filename.

More Video Options

Similarly to the above suggestion, you could use some more advanced features of rustube to offer multiple options for video and audio quality. This would require sending data from Rust to the display side and back.

Clickable Path

You could make the path in the success message clickable, to make it easier to get to the video once it's downloaded.

Window title

This doesn't matter that much, but if you want to change the window title, go into src-tauri/tauri.conf.json and change the tauri.windows[0] title property near the bottom.

Source Code

View the source code here: https://github.com/Redfluffydragon/tauri-video-downloader.

*In fact, since rustube only works for YouTube videos, you can only use this to download YouTube videos. Sorry.

#article #coding/rust/tauri

More posts: