How to create your own youtube downloader web app in expressjs

This tutorial will guide you to create your own youtube downloader app in node expressjs with the help of awesome command line utility program called youtube-dl which extracts download links from many video sites including NSFW.
However unlike youtube-dl which has lot more functionality, this app will only extract all video resolutions and restrict itself to youtube url (and its redirects) only.

Download the source code from github.

Source Code

Now, before starting the tutorial, make sure you've nodejs installed on your window, mac or any linux machine and be sure to check it via following code:

node -v

1. Setting up the project

We'll use official scaffolding tool called express generator for this app. So, quickly start by entering into the new directory (name it whatever you want eg: ytdown) and install the generator tool globally using the following command :

npm install express-generator -g

After that, create the new project with handlebars as a default template engine into the working directory

express --hbs

And finally do npm install to initiate the project. Run the server using npm start and browse it using http://localhost:3000.

2. Creating the UI

We need two page views for this app. In first page user enter the video url and the second one will list the video link in different resolutions. Here's the code for the two pages:

index.hbs

In this file, /video is the routing url where video link appears.

<header class="site-head">
    <h1 class="h2 mb-4 title">Youtube Downloader</h1>
    <p>Download any youtube video in multiple formats and quality with fast download</p>
</header>
<div class="container content-wrap">
    <div class="row justify-content-center align-items-start">
        <div class="col">
            <form class="down-form" name="youtube-download" method="post" action="/video">
                <input type="url" name="url" placeholder="https://www.youtube.com/watch?v=kQW81ty6QFg" required class="form-control form-control-lg"/>
                <button type="submit" class="btn btn-success mt-3">Get Links</button>
            </form>
        </div>
    </div>
</div>
listvideo.hbs

This file has some extra variables and handlebars statement which output meta information of the video.

<div class="container content-wrap">
    <div class="mb-4">
        <a href="/" class="btn btn-secondary">← Back To Homepage</a>
    </div>
        {{#if error}}
        <p>{{error}}</p>
        {{else}}
            <div class="col">
                <iframe width="640" height="360" src="https://www.youtube.com/embed/{{meta.id}}" frameborder="0" allowfullscreen></iframe>
            </div>
            <div class="col-6">
                <h4 class="h5 mb-4 font-weight-bold">Download Links <small>(multiple sizes and formats)</small></h4>
                <ul class="list-group">
                    {{#each meta.formats}}
                    <li class="list-group-item">
                        <a href="{{url}}" style="font-size: 12px">{{format_note}}
                            <div class="small">Ext: {{ext}} | Size: {{filesize}}</div>
                        </a>
                    </li>
                    {{/each}}
                </ul>
            </div>
        {{/if}}
</div>

And here is the modified layout.hbs file which include link to bootstrap framework and some google fonts for styling.

layout.hbs
<!DOCTYPE html>
<html>
  <head>
    <title>{{title}}</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"  />
    <link rel="stylesheet" href="/stylesheets/style.css" />
    <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700" rel="stylesheet">
    <link href="https://fonts.googleapis.com/css?family=Bungee+Shade" rel="stylesheet">
  </head>
  <body>
          {{{body}}}
  </body>
</html>

Finally some css styling for the app in style.css

body {
    font-family: 'Open Sans', 'Lucida Grande', sans-serif;
}

::-webkit-input-placeholder { /* Chrome/Opera/Safari */
  color: rgba(0,0,0,.4)!important;
}
::-moz-placeholder { /* Firefox 19+ */
   color: rgba(0,0,0,.4)!important;
}
:-ms-input-placeholder { /* IE 10+ */
   color: rgba(0,0,0,.4)!important;
}

.site-head {
    padding: 1.4rem 2rem;
    text-align: center;
    margin-top: 6rem;
}
.site-head .title {
    position: relative;
    display: inline-block;
    font-family: 'Bungee Shade', cursive;
}
.site-head .title:before {
    content: "";
    position: absolute;
    width: 70px; height: 6px;
    background: rgba(144, 232, 24, 1);
    bottom: -10px;
    left: 6px;
}

.content-wrap {
    padding: 1rem;
}
.down-form input{
    border: 2px solid #efefef;
}
.down-form input:focus {
    border-color: rgba(104, 202, 24, .4);
}

Here's the final screenshot of the UI :
screen

3. Creating the endpoints

Basically we need to implement one additional routing endpoints for listvideo.hbs page. So, open the routes/index.js fileand add the following code:

// default endpoint
router.get('/', function(req, res) {
  res.render('index', { title: 'Youtube Downloader' });
// new endpoint
});
router.post('/video', function(req, res, next) {
  res.render('listvideo', {});
})

4. Using youtube-dl in the app

Now it's time to integrate youtube-dl into the code and using this is very easy with the help of node youtube-dl wrapper. It will also automatically install youtube-dl if it's not installed on your system.
So, we need to only do this:

npm install youtube-dl --save

After that, we need to call the method to get the download url via getInfo() method. So edit the previous index.js file to make it look like this:

// ... other code 

// utility function to convert bytes to human readable.
function bytesToSize(bytes) {
   var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
   if (bytes == 0) return '0 Byte';
   var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
   return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
};

var ytdl = require('youtube-dl');
router.post('/video', function(req, res, next) {
    var url = req.body.url,
        formats = [];
    
    ytdl.getInfo(url, ['--youtube-skip-dash-manifest'], function(err, info) {
        if(err) return res.render('listvideo', {error: 'The link you provided either not a valid url or it is not acceptable'});

        // push all video formats for download (skipping audio)
        info.formats.forEach(function(item) {
            if(item.format_note !== 'DASH audio' && item.filesize) {
                item.filesize = item.filesize ? bytesToSize(item.filesize): 'unknown';
                formats.push(item);
            }
        });
        res.render('listvideo', {meta: {id: info.id, formats: formats}});
    })

})

Ok, done. And now the app is almost completed and working but the thing that is missing is to restrict this app to youtube url only. This is necessary as it will prevent malicious url from users.
We also need to check for youtube redirection and short links which can be easily achievable by making a GET request.
Wrap the above with the following:

// this pattern check for youtube url.
var pattern = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
router.post('/video', function(req, res, next) {
  request.get(url, function (err, resp, body) {
   // check if it is valid url
   if(pattern.test(resp.request.uri.href)) {
     // .... above code as usual
   }

   else {
     res.render('listvideo', {error: 'The link you provided either not a valid url or it is not acceptable'});
   }

  })
});

So, finally the tutorial is completed. And there is lot more thing to do like making a nice 404 page, hiding the server ip, video to audio converter if you're planning to running it in production.