Build a Youtube Downloader App Using Django

Python library provides us many libraries to download a youtube video from URL, so I chose the Pytube library for this tutorial.
And therefore, we're going to build a simple youtube Downloader app.

Before getting started, let me show you what we're going to build:

Django Youtube Downloader second example

And now, let' get started

Technologies

  • Django==3.1.4
  • pytube==10.4.1

Setting Up Django Project

If you already have installed Django and libraries, you can move to the next part.
If not, you need to follow these steps:


mkdir DjangoYoutubeDownloader



cd DjangoYoutubeDownloader

Create a Virtual Environments.


virtualenv -p /usr/bin/python3 env

Activate it.


source env/bin/activate

Install Django.


pip install django

Start our project.


django-admin startproject DjangoYoutubeDownloader

Start the app.


cd DjangoYoutubeDownloader



django-admin startapp core

in DjangoYoutubeDownloader/settings.py add app to INSTALLED_APPS


INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    #Apps
    'core',
]

Allow all hosts.


ALLOWED_HOSTS = ['*']

Set TEMPLATES directory path.


'DIRS': [os.path.join(BASE_DIR, 'TEMPLATES')],

Initialize the media directory


#DjangoYoutubeDownloader/settings.py
MEDIA_URL = 'media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')



#DjangoYoutubeDownloader/urls.py

from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static

# core.views
from core.views import *

urlpatterns = [
    path('admin/', admin.site.urls),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Create TEMPLATES and media folders inside our project.


mkdir TEMPLATES



mkdir media

Migrate.


python3 manage.py migrate

Run server.


python3 manage.py runserver


System check identified no issues (0 silenced).
December 28, 2020 - 22:47:26
Django version 3.1.4, using settings 'Backend.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Great!

Project structure:


├── core
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   ├── models.py
│   ├── __pycache__
│   ├── tests.py
│   └── views.py
├── db.sqlite3
├── DjangoYoutubeDownloader
│   ├── asgi.py
│   ├── __init__.py
│   ├── __pycache__
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
└── TEMPLATES
└── media

Installing the Pytube library and Quickstart

Install:


pip install pytube

Quickstart:

importing YouTube Class:


>>> from pytube import YouTube

Getting video's info:


>>> yt = YouTube('https://www.youtube.com/watch?v=F7mKD2Un65I')

Getting the title:


>>> yt.title
'5 Second Countdown Intro video   YouTube'

Getting video's image:


>>> yt.thumbnail_url
'https://i.ytimg.com/vi/F7mKD2Un65I/maxresdefault.jpg'

Available media formats:


>>> yt.streams.all()
__main__:1: DeprecationWarning: Call to deprecated function all (This object can be treated as a list, all() is useless).
[<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" type="video">, <Stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2" progressive="True" type="video">, <Stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001f" progressive="False" type="video">, <Stream: itag="247" mime_type="video/webm" res="720p" fps="30fps" vcodec="vp9" progressive="False" type="video">, <Stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d4014" progressive="False" type="video">, <Stream: itag="244" mime_type="video/webm" res="480p" fps="30fps" vcodec="vp9" progressive="False" type="video">, <Stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e" progressive="False" type="video">, <Stream: itag="243" mime_type="video/webm" res="360p" fps="30fps" vcodec="vp9" progressive="False" type="video">, <Stream: itag="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d400c" progressive="False" type="video">, <Stream: itag="242" mime_type="video/webm" res="240p" fps="30fps" vcodec="vp9" progressive="False" type="video">, <Stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400b" progressive="False" type="video">, <Stream: itag="278" mime_type="video/webm" res="144p" fps="30fps" vcodec="vp9" progressive="False" type="video">, <Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2" progressive="False" type="audio">, <Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus" progressive="False" type="audio">]

Downloading the video:


>>> yt.streams.first().download()

Note: Your video will download it to the current working directory.

Setting a destination path:


>>> yt.streams.first().download("media")

Filtering & Downloading:


>>>yt.streams.filter(res="720p").first().download("media")

For more information, visit Pytube documentation.

Django Youtube Downloader

In the next section, we'll start to build our Django app by two Examples:
In the first example, we'll understand how to use Pytube with Django.
In the second example, we'll create our app with Django + Vuejs (single-page application)

1. Understanding How to use Pytube with Django

In core/views.py:


from django.shortcuts import render
from django.http import JsonResponse
from django.views.generic import TemplateView
#modules
import re 
import json
# Pytube library
from pytube import YouTube




class Index(TemplateView):
    template_name = "index.html"


def youtube_downloader(request):

    if request.method == "POST":
        #Video Url
        url = request.POST.get("url", None)
        try:
            # Download
            yt = YouTube(url).streams.first().download("media")
            # initialize the link
            down_link = re.findall('(/media/.+)', yt)[0]

            return render(request, "index.html", {"down_link":down_link})
        
        except:
            return render(request, "index.html", {"msg":"Something Wong"})

    return render(request, "index.html")

Index class: represents the form.
youtube_downloader function: handles the form.

let me explain the youtube_downloader function:

After submitting the form:
1. Getting video URL from the form.
2. Downloading video from URL using Pytube
3. Using REGEX to extract the available link of the video because Pyube returns the full path.
4. returning video's path as a link.

Now, we need to add paths for the Index class and the youtube_downloader function.

In urls.py:


path('index/', Index.as_view()),
path('download1/', youtube_downloader, name="download1"),

In the TEMPLATES directory, create index.html and put the following line:

TEMPLATES/index.html:


<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <style>
      @import url('https://fonts.googleapis.com/css?family=Merriweather:300|Oswald');
      h1{
      text-align: center;
      margin-top: 50px;
      margin-bottom: 100px;
      }
      /* Form fields */ 
      #booking-form {
      font-family: 'Merriweather', serif;
      max-width: 540px;
      margin: 40px auto;
      }
      #booking-form ul {
      padding-left: 0;
      }
      #booking-form li {
      list-style: none;
      }
      #booking-form input, 
      #booking-form textarea,  
      #booking-form select {
      font-size: 18px;
      padding: 0 28px;
      width: 100%;
      box-sizing: border-box;
      border: 1px solid rgb(233,233,233);
      text-align: left;
      border-radius: 35px;
      color: #000000;
      letter-spacing: normal;
      height: 50px;
      line-height: 50px;
      font-family: 'Merriweather', serif;
      background: #fff;
      margin-bottom: 20px;
      }
      #booking-form select {
      background: url('https://seashineadventures.com/wp-content/uploads/2017/08/grey-chevron.png') no-repeat;
      -webkit-appearance: none;
      appearance: none;
      background-size: 15px;
      background-position: 97% 50%;
      }
      #booking-form textarea {
      height: 120px;
      line-height: 1.5;
      padding: 15px 28px;
      }
      #booking-form input:focus, 
      #booking-form textarea:focus, 
      #booking-form select:focus {
      outline: 0;
      border: 1px solid rgb(211,211,211);
      }
      #booking-form label {
      font-size: 18px;
      font-weight: 300;
      display: block;
      margin-bottom: 10px;
      }
      /* Buttons */
      #booking-form .next-btn,
      #booking-form .submit-btn {
      background: #82CCC8;
      padding: 13px 20px;
      color: #fff;
      font-family: oswald, sans-serif;
      text-transform: uppercase;
      font-size: 18px;
      line-height: 24px;
      letter-spacing: 1px;
      min-width: 400px;
      display: inline-block;
      text-align: center;
      font-weight: 400;
      border-radius: 0;
      border: 0;
      transition-property: background-color;
      transition-duration: 0.5s;
      width: 100%;
      cursor: pointer;
      }
      #booking-form .next-btn:hover,
      #booking-form .submit-btn:hover {
      background-color: #444b5d;
      color: #fff;
      }
      /* Progress Bar */
      .progress-wrap {
      margin: auto;
      display: table;
      }
      .line-progress-bar {
      display: flex;
      margin: auto;
      width: 100%;
      }
      .line {
      height: 1px;
      width: 250px;
      border-bottom-style: solid;
      border-bottom-width: 1px;
      border-bottom-color: rgb(217, 217, 217);
      position: absolute;
      margin-top: 8px;
      }
      .progress-wrap div ul {
      display: flex;
      width: 250px;
      list-style: none;
      padding: 0px;
      margin: initial;
      justify-content: space-between;
      z-index: 1;
      }
      .progressbar-dots {
      display: inline-flex;
      border: #949494 solid 4px;
      background: #333333;
      height: 20px;
      width: 20px;
      border-radius: 50%;
      text-align: center;
      justify-content: center;
      align-items: center;
      font-weight: bold;
      color: #d4d4d4;
      font-size: 20px;
      margin-left: 0px;
      color: #d4d4d4;
      border: 0px solid rgb(217,217,217);
      background: rgb(217, 217, 217);
      }
      .progressbar-dots span {
      font-size: 12px;
      line-height: 12px;
      position: absolute;
      margin-top: 60px;
      /* width: 75px; */
      float: left;
      margin-left: -30px;
      display: none;
      }
      .progressbar-dots.active {
      color: #fff;
      border: 0px solid rgb(38,163,134);
      background: #82CCC8;
      }
      /*  Tab */
      .tab-pane {
      display: none;
      }
      .tab-pane:first-child {
      display: block;
      }
      /* Error */
      span.error {
      font-size: 12px;
      font-family: "helvetica neue", arial, sans-serif;
      color: #D6041D;
      text-transform: uppercase;
      display: block;
      margin-bottom: 10px;
      }
    </style>
  </head>
  <body>
    <h1>Youtube Downloader</h1>
    <form method="POST" action="{% url 'download1' %}" id="booking-form">
      {% csrf_token %}
      <div class="tab-content">
      <div class="tab-pane" id="step1">
        <ul>
          <input name="url" type="url" placeholder="video URL" required>
          <li style="list-style: none; display: inline">
            <button type="submit" class="next-btn next-btn1" type="button">Download Video</button>
          </li>
        </ul>
      </div>
    </form>
    {% if down_link %}
    <div style="text-align: center; margin-top: 80px">
      <h2>Downloading Link:</h2>
      <a style="background: #8d318a; width: 100px" class="next-btn next-btn1" href="{{down_link}}" download>link</a>
    </div>
    {% endif %}
  </body>
</html>

Let's see the result by going to http://127.0.0.1:8000/index

Django Youtube Downloader First example

2. Django Youtube Downloader + Vuejs

In this example, we'll make our app modern by using vuejs we'll also add some features like loading, video's info and video qualities.

In core/views.py:


#index page
class Index2(TemplateView):
    template_name = "index2.html"



# Get info of video
def get_video_info(request):
    if request.method == "POST":
        res = {}
        data = json.loads(request.body)
        try:
            yt = YouTube(data["url"])
            res['title'] = yt.title
            res['img'] = yt.thumbnail_url
            qualities = re.findall('res="(.+?)"', str(yt.streams))
            res['qualities'] = list(set(qualities)) 
            return JsonResponse(res, status=200)
        except:
            return JsonResponse({"msg": "Something went wrong"}, status=500)

    return JsonResponse({"response": "Page Not Allowed"}, status=405)       

            
#Youtube Downloader
def youtube_Downloader2(request):

    if request.method == "POST":
        data = json.loads(request.body)
        url = data["url"]
        quality = data["q"]
        youtube = YouTube(url).streams.filter(res=quality).first().download("media")

        return JsonResponse({"down_link":re.findall('(/media/.+)', youtube)[0]})

    return JsonResponse({"response": "Page Not Allowed"}, status=405)

Index2 class: represents the form.
get_video_info function: returns info of video
youtube_Downloader2 function: Downloads the video

We're not able to return video qualities with Pytube.
So, I use REGEX to extract the qualities from the yt.streams object.

In urls.py:


path('index2/', Index2.as_view()),
path('get-info/', get_video_info, name="get_info"),
path('download2/', youtube_Downloader2, name="download2")

Inside of TEMPLATES directory, create index2.html and add the following code:


<!DOCTYPE html>
<html>
  <head>
    <title>Youtube Downloader</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <style>
      @import url('https://fonts.googleapis.com/css?family=Merriweather:300|Oswald');
      h1{
      margin-top: 50px;
      margin-bottom: 100px;
      }
      /* Form fields */ 
      #booking-form {
      font-family: 'Merriweather', serif;
      max-width: 540px;
      margin: 40px auto;
      }
      #booking-form ul {
      padding-left: 0;
      }
      #booking-form li {
      list-style: none;
      }
      #booking-form input, 
      #booking-form textarea,  
      #booking-form select {
      font-size: 18px;
      padding: 0 28px;
      width: 100%;
      box-sizing: border-box;
      border: 1px solid rgb(233,233,233);
      text-align: left;
      border-radius: 35px;
      color: #000000;
      letter-spacing: normal;
      height: 50px;
      line-height: 50px;
      font-family: 'Merriweather', serif;
      background: #fff;
      margin-bottom: 20px;
      }
      #booking-form select {
      background: url('https://seashineadventures.com/wp-content/uploads/2017/08/grey-chevron.png') no-repeat;
      -webkit-appearance: none;
      appearance: none;
      background-size: 15px;
      background-position: 97% 50%;
      }
      #booking-form textarea {
      height: 120px;
      line-height: 1.5;
      padding: 15px 28px;
      }
      #booking-form input:focus, 
      #booking-form textarea:focus, 
      #booking-form select:focus {
      outline: 0;
      border: 1px solid rgb(211,211,211);
      }
      #booking-form label {
      font-size: 18px;
      font-weight: 300;
      display: block;
      margin-bottom: 10px;
      }
      /* Buttons */
      #booking-form .next-btn,
      #booking-form .submit-btn {
      background: #82CCC8;
      padding: 13px 20px;
      color: #fff;
      font-family: oswald, sans-serif;
      text-transform: uppercase;
      font-size: 18px;
      line-height: 24px;
      letter-spacing: 1px;
      min-width: 400px;
      display: inline-block;
      text-align: center;
      font-weight: 400;
      border-radius: 0;
      border: 0;
      transition-property: background-color;
      transition-duration: 0.5s;
      width: 100%;
      cursor: pointer;
      }
      #booking-form .next-btn:hover,
      #booking-form .submit-btn:hover {
      background-color: #444b5d;
      color: #fff;
      }
      /* Progress Bar */
      .progress-wrap {
      margin: auto;
      display: table;
      }
      .line-progress-bar {
      display: flex;
      margin: auto;
      width: 100%;
      }
      .line {
      height: 1px;
      width: 250px;
      border-bottom-style: solid;
      border-bottom-width: 1px;
      border-bottom-color: rgb(217, 217, 217);
      position: absolute;
      margin-top: 8px;
      }
      .progress-wrap div ul {
      display: flex;
      width: 250px;
      list-style: none;
      padding: 0px;
      margin: initial;
      justify-content: space-between;
      z-index: 1;
      }
      .progressbar-dots {
      display: inline-flex;
      border: #949494 solid 4px;
      background: #333333;
      height: 20px;
      width: 20px;
      border-radius: 50%;
      text-align: center;
      justify-content: center;
      align-items: center;
      font-weight: bold;
      color: #d4d4d4;
      font-size: 20px;
      margin-left: 0px;
      color: #d4d4d4;
      border: 0px solid rgb(217,217,217);
      background: rgb(217, 217, 217);
      }
      .progressbar-dots span {
      font-size: 12px;
      line-height: 12px;
      position: absolute;
      margin-top: 60px;
      /* width: 75px; */
      float: left;
      margin-left: -30px;
      display: none;
      }
      .progressbar-dots.active {
      color: #fff;
      border: 0px solid rgb(38,163,134);
      background: #82CCC8;
      }
      /*  Tab */
      .tab-pane {
      display: none;
      }
      .tab-pane:first-child {
      display: block;
      }
      /* Error */
      span.error {
      font-size: 12px;
      font-family: "helvetica neue", arial, sans-serif;
      color: #D6041D;
      text-transform: uppercase;
      display: block;
      margin-bottom: 10px;
      }
      /* loading */
      .lds-ellipsis {
      display: inline-block;
      position: relative;
      width: 80px;
      height: 80px;
      }
      .lds-ellipsis div {
      position: absolute;
      top: 33px;
      width: 13px;
      height: 13px;
      border-radius: 50%;
      background: #8d318a;
      animation-timing-function: cubic-bezier(0, 1, 1, 0);
      }
      .lds-ellipsis div:nth-child(1) {
      left: 8px;
      animation: lds-ellipsis1 0.6s infinite;
      }
      .lds-ellipsis div:nth-child(2) {
      left: 8px;
      animation: lds-ellipsis2 0.6s infinite;
      }
      .lds-ellipsis div:nth-child(3) {
      left: 32px;
      animation: lds-ellipsis2 0.6s infinite;
      }
      .lds-ellipsis div:nth-child(4) {
      left: 56px;
      animation: lds-ellipsis3 0.6s infinite;
      }
      @keyframes lds-ellipsis1 {
      0% {
      transform: scale(0);
      }
      100% {
      transform: scale(1);
      }
      }
      @keyframes lds-ellipsis3 {
      0% {
      transform: scale(1);
      }
      100% {
      transform: scale(0);
      }
      }
      @keyframes lds-ellipsis2 {
      0% {
      transform: translate(0, 0);
      }
      100% {
      transform: translate(24px, 0);
      }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div id="app" class="text-center mb-5">
        <h1>Youtube Downloader</h1>
        <form method="POST" id="booking-form">
          <div class="tab-content">
            <div class="tab-pane" id="step1">
              <ul>
                <input v-model="url" type="text" placeholder="video URL" required>
                <li style="list-style: none; display: inline">
                  <button class="next-btn next-btn1" type="button" v-on:click="getInfo()">Download Video</button>
                </li>
              </ul>
            </div>
          </div>
        </form>
        <div v-if="loading == true" style="">
          <div class="lds-ellipsis">
            <div></div>
            <div></div>
            <div></div>
            <div></div>
          </div>
        </div>
        <div class="video-info" v-if="video_info">
          <p>Title: <strong>[[video_info.title]]</strong></p>
          <div class="img">
            <img class="img-fluid" v-bind:src="video_info.img" v-bind:alt="video_info.title" width="500" height="500">
          </div>
          <p class="mt-5">Choose quality:</p>
          <button type="button" class="btn btn-info ml-4" v-for="q in video_info.qualities" v-on:click="Download(q)">[[q]]</button>
        </div>
        <div v-if="loading2 == true" style="">
          <div class="lds-ellipsis">
            <div></div>
            <div></div>
            <div></div>
            <div></div>
          </div>
        </div>
        <div v-if="down_link" style=" margin-top: 80px">
          <h2>Downloading Link:</h2>
          <a style="background: #8d318a; width: 100px; padding:10px 60px; color: #fff" class="next-btn next-btn1" v-bind:href="down_link" download>Click Here</a>
        </div>
      </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js" integrity="sha512-bZS47S7sPOxkjU/4Bt0zrhEtWx0y0CRkhEp8IckzK+ltifIIE9EMIMTuT/mEzoIMewUINruDBIR/jJnbguonqQ==" crossorigin="anonymous"></script>
    <script>
      const vms = new Vue({
          delimiters: ['[[', ']]'],
          el: '#app',
      
          data: {
              loading: false,
              loading2: false,
              url: null,
              video_info: null,
              down_link: null,
          },
      
          methods: {
      
              getInfo: function(){
                  this.down_link = null;
                  this.loading = true;
      
                  axios({
                          method : 'POST',
                          url:'{% url "get_info" %}', 
                          headers: {'X-CSRFTOKEN': '{{ csrf_token }}',},
                          data:{"url":this.url}
                          },
      
                          ).then(response => {
                              this.video_info = response.data;
                              this.loading = false;
                              
                             
                          }).catch(err => {
                              this.loading = false;
                             
                      });
      
      
      
      
              },
      
              Download: function(q){
                  this.down_link = null;
                  this.loading2 = true;
      
                  axios({
                          method : 'POST',
                          url:'{% url "download2" %}', 
                          headers: {'X-CSRFTOKEN': '{{ csrf_token }}',},
                          data:{"url":this.url, "q":q}
                          },
      
                          ).then(response => {
                              this.loading2 = false;
                              this.down_link = response.data['down_link']
      
                             
                          }).catch(err => {
                             
                          });
      
              },
      
          },
      
      
      });  
      
      
    </script>
  </body>
</html>

Let's going to http://127.0.0.1:8000/index2:

Django Youtube Downloader second example

Project on GitHub

The project is available on Github so, You can download it by clicking the link below:
Django Youtube Downloader App

I hope that this tutorial can offer some help.

English today is not an art to be mastered it's just a tool to use to get a result