Simple Youtube Downloader in under 50 Javascript lines
Tags: javascript, node, tutorial, opensource
Published: 2023-11-04 11:00:52

# Why this project

Lately I was searching for a youtube Downloader and never found one where I can download videos with high quality (eg. 4k) without paying for a subscription. Well on research I stumbled across the `ytdl` npm package.

## The Task

Download a YouTube Video with the highest audio and video quality.

## What we will use

- [ytdl-core](https://www.npmjs.com/package/ytdl-core) for streaming from Youtube
- [fluent-ffmpeg](https://www.npmjs.com/package/fluent-ffmpeg) for transcoding video and audio to our likings
- [nanospinner](https://www.npmjs.com/package/nanospinner) for good looking progress spinners

## What problems do we need to solve

I will tell you from my experience with ytdl. If you look at the documentation of `ytdl-core` you can see, that you can either download with the quality setting `highestaudio` or `highestvideo`. But in the highestvideo we have bad audio and in the highestaudio we have bad video. We will fix this by first downloading the best video to a stream and write that to a file. In the second step we will download the best audio to a stream and then put the file and the stream through ffmpeg to combine into one file. 

## Writing the script

### initializing npm

Create a new folder and open a terminal inside that folder. Then create a new npm package and install the dependencies:

```bash
npm init -y
npm install fluent-ffmpeg ytdl-core nanospinner
```

### creating the script

Create an jsvascript file and call it `index.js`.

Now we will add all the imports.

```javascript
const fs = require("fs");
const ytdl = require("ytdl-core");
const ffmpeg = require("fluent-ffmpeg");
const { createSpinner } = require("nanospinner");

var videoId = "VideoID";
```

We will also create a variable for the videoID.

With that done we can take a look at how the download process will work.

1. Download the `highestvideo` to a file
2. Download the `highestaudio` to a readable stream
3. Take the video from the file and the audio from the stream using `ffmpeg` and write the output to a file
4. remove temporary file


#### Step 1

To download the `highestvideo` to a file we will use the default `ytdl(id, options)` function. This function returns a readable Stream which we will pipe with the `fs.createWriteStream(path)` function to write it to a file. As a filename we will use `VideoID.mp4`. 

To add a simple Terminal Output for the user to see that something is happening we first create a spinner and then start the download.

```javascript
let videoSpinner = createSpinner("Downloading highest video").start();

ytdl(videoId, { quality: "highestvideo" })
	.pipe(fs.createWriteStream(videoId + ".mp4"))
	.on("error", () => {
		videoSpinner.error();
		console.log("error downloading highest video");
	})
	.on("finish", () => {
		videoSpinner.success();
		// Do something after download of video
	});
```

See how we also added handlers for the `error` and `finish` event. In the errorhandler we stop the spinner and print an error message. In the onfinish function we will now implement the other streaming and compining.

#### Step 2

To download the highestaudio to a readable Stream we again call the `ytdl(id, options)` function but with the other quality option. And this time we will not pipe the stream directly. We will create a variable for the stream so that we can use it later.

But first we will create a loading spinner for the audio download and combining part. We will also define a variable for the output fulepath. This time using `mkv` as file format: `videoId.mkv`.

```javascript
let scSpinner = createSpinner("Downloading audio and combining").start();
const audioFileStream = ytdl(videoId, { quality: "highestaudio" });
const outputFilePath = videoId + ".mkv";
```

Remember this has to be in the onfinish function defined earlier!

#### Step 3

Now to the tricky part. Combining the audio and video together. For that we will first create a new ffmpeg command. Then we will use method-chaining to set some options and define callbacks and then as the last step we will run the command.

But first create the command.
```javascript
const command = ffmpeg();
```

Now for the options: I will add comments on the lines to explain it but here is the explanation in short:

1. Define both inputs using `.input(string|readable)`
2. Define audioCodec and videoCodec
3. Tell the inputFormat of the streams `.inputFormat('mp4')`
4. Define the output Filepath `.output(string)`
5. define callback functions
6. run the command

```javascript
command
	// Step 1
	.input(id + ".mp4")
	.input(audioFileStream)
	// Step 2
	.audioCodec("copy")
	.videoCodec("copy")
	// Step 3
	.inputFormat("mp4")
	// Step 4
	.output(outputFilePath)
	// Step 5
	.on("end", () => {
		scSpinner.success();
		// finish handling
	})
	.on("error", (err) => {
		scSpinner.error();
		// error handling
	})
	// Step 6
	.run();
```

#### Step 4

As the last step we will remove the `.mp4` file we downloaded the video to. We will do that in the onEnd function of the ffmpeg command. For that we will just create a Loading spinner and use the `fs.rmSync(path)` function.

```javascript
let dlSpinner = createSpinner("Delete temporary created file").start();
fs.rmSync(videoId + ".mp4");
dlSpinner.success();
```

### Run the downloader

To run the downloader you just need to replace the placeholder for the videoId with a real id and run the script using `node .`

Code can be downloaded [here](https://gist.github.com/Tim-Gabrikowski/31cc8ae0fdaccfc1177e05519ba97199).