DEV Community

Tim Gabrikowski
Tim Gabrikowski

Posted on

Simple Youtube Downloader in under 50 Javascript lines

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

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:

npm init -y
npm install fluent-ffmpeg ytdl-core nanospinner
Enter fullscreen mode Exit fullscreen mode

creating the script

Create an jsvascript file and call it index.js.

Now we will add all the imports.

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

var videoId = "VideoID";
Enter fullscreen mode Exit fullscreen mode

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.

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
    });
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

const command = ffmpeg();
Enter fullscreen mode Exit fullscreen mode

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
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();
Enter fullscreen mode Exit fullscreen mode

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.

let dlSpinner = createSpinner("Delete temporary created file").start();
fs.rmSync(videoId + ".mp4");
dlSpinner.success();
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)