A short while ago, I was working for a client, integrating video reviews in their website. Like any motivated developer solving a novel problem, the first thing I did was Google it, and I found a plethora of unhelpful or misguided answers on how to achieve something entirely different, or outdated and unmaintained Python packages. Eventually, I bit the bullet and the team and I built everything from scratch: we created the views, learned about Google’s API, created the API client, and eventually succeeded in programmatically uploading videos from Django.

In this post, I’ll try to guide you step by step in how to post YouTube videos from your Django app. This will require a bit of playing around with Google API credentials—first with the web interface, then with the code. The YouTube part itself is very straightforward. We need to understand how Google stuff works because sometimes it’s tricky and the information is spread through many places.

Prerequisites

I recommend familiarizing yourself with the following before we begin working:

An interesting bit of code to note is the following Python Snippet from the Google YouTube API Docs:

# Sample python code for videos.insert
def videos_insert(client, properties, media_file, **kwargs):
  resource = build_resource(properties) # See full sample for function
  kwargs = remove_empty_kwargs(**kwargs) # See full sample for function
  request = client.videos().insert(
    body=resource,
    media_body=MediaFileUpload(media_file, chunksize=-1,
                               resumable=True),
    **kwargs
  )

  # See full sample for function
  return resumable_upload(request, 'video', 'insert')

media_file = 'sample_video.flv'
  if not os.path.exists(media_file):
    exit('Please specify a valid file location.')
videos_insert(client, 
    {'snippet.categoryId': '22',
     'snippet.defaultLanguage': '',
     'snippet.description': 'Description of uploaded video.',
     'snippet.tags[]': '',
     'snippet.title': 'Test video upload',
     'status.embeddable': '',
     'status.license': '',
     'status.privacyStatus': 'private',
     'status.publicStatsViewable': ''},
    media_file,
    part='snippet,status')

Getting Started

After you’ve read the prerequisites, it’s time to get started. Let’s see what we need.

Toolbelt

Basically, let’s create a virtual environment. I personally prefer pyenv. Setting up both is out of the scope of this post, so I’m going to post some pyenv commands below and, if your preference is virtualenv, feel free to replace the commands accordingly.

I’m going to use Python 3.7 and Django 2.1 in this post.

➜  ~/projects $ mkdir django-youtube
➜  ~/projects $ cd django-youtube
➜  ~/projects/django-youtube $ pyenv virtualenv 3.7.0 djangoyt

➜  ~/projects/django-youtube $ vim .python-version

Let’s put this in the contents (just if you use pyenv, so it activates automatically when you enter the folder):

        djangoyt

Installing dependencies:

➜  ~/projects/django-youtube $ pip install google-api-python-client google-auth\
 google-auth-oauthlib google-auth-httplib2 oauth2client Django unipath jsonpickle

Now time to start our django project:

➜  ~/projects/django-youtube $ django-admin startproject django_youtube .

Pause for some Google Config

Let’s config our project credentials now so we are able to use the Google APIs.

Step 1. Go to the following URL:

https://console.developers.google.com/apis/library/youtube.googleapis.com

Step 2. Create a new project.

Create a new project

Step 3. Click “Enable APIs and Services.”

Enable APIs and Services.

Step 4. Look for YouTube Data API v3, and click “Enable.”

Look for YouTube Data API v3, and click "Enable."

Step 5. You should get a message about credentials.

a message about credentials

Step 6. Click on the “Create credentials” blue button on the right side, and you should get the following screen:

Click on the "Create credentials" blue button

Step 7. Choose Web server, User Data:

Choose Web server, User Data

Step 8. Add authorized JS origins and redirect URIs. Continue to the end:

Add authorized JS origins and redirect URIs.

OK we are done with our credentials set up. You can either download the credentials in a JSON format or copy the Client ID and Client Secret.

Back to Django

Let’s start our very first Django app. I usually name it “core”:

(djangoyt) ➜  ~/projects/django-youtube $ python manage.py startapp core

Now, let’s add the following to our root urls.py file to route the homepage requests to our core app:

# <root>/urls.py

from django.urls import path, include


    path('', include(('core.urls', 'core'), namespace='core')),

In the core app, let’s have another urls.py file, with some config also:

# core/urls.py

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

from .views import HomePageView

urlpatterns = [
    path('', HomePageView.as_view(), name='home')
]

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

See there is an empty path pointing to HomePageView. Time to add some code.

Let’s do now a simple TemplateView just to see it running.

# core/views.py
from django.shortcuts import render
from django.views.generic import TemplateView


class HomePageView(TemplateView):
    template_name = 'core/home.html'

And of course we need a basic template:

# core/templates/core/home.html
<!DOCTYPE html>
<html>
<body>

<h1>My First Heading</h1>
<p>My first paragraph.</p>

</body>
</html>

We need to do some settings tweaks:

# settings.py
from unipath import Path

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = Path(__file__).parent


INSTALLED_APPS
    'core',



STATIC_ROOT = BASE_DIR.parent.child('staticfiles')

STATIC_URL = '/static/'


MEDIA_ROOT = BASE_DIR.parent.child('uploads')

MEDIA_URL = '/media/'

Let’s create now a YoutubeForm and add it as form_class to the view:

# core/views.py
from django import forms
from django.views.generic.edit import FormView


class YouTubeForm(forms.Form):
    pass


class HomePageView(FormView):
    template_name = 'core/home.html'
    form_class = YouTubeForm

Try to run your application now, and the page will look like this:

Page preview

Pause to Do Authorization

First of all, you have to create a model to store your credentials. You could to through a file, cache system, or any other storage solution, but a database seems reasonable and scalable, and also you can store credentials per users if you want.

Before proceeding, an adjustment needs to be made—there is fork of oauth2client that supports Django 2.1 that we have to use. Soon, we’ll have official support, but in the meantime, you can inspect the fork changes. They are very simple.

pip install -e git://github.com/Schweigi/[email protected]#egg=oauth2client
Because of compatibility with Django 2.1

Go to your settings.py and place the Client ID and Client Secret you got from Google in previous steps.

# settings.py

GOOGLE_OAUTH2_CLIENT_ID = '<your client id>'
GOOGLE_OAUTH2_CLIENT_SECRET = '<your client secret>'

Caution: storing secrets in your code is not recommended. I’m doing this simply as a demonstration. I recommend using environment variables in your production app, and not hardcoding secrets in application files. Alternatively, if you downloaded the JSON from Google, you can also specify its path instead of the settings above:

GOOGLE_OAUTH2_CLIENT_SECRETS_JSON = '/path/to/client_id.json'

The oauth2client package already provides plenty of functionality, with a CredentialsField already done that we can use. It’s possible to add more fields, like a foreign key and created/modified dates so we get more robust, but let’s stay simple.

Simple model to store credentials:

# core/models.py
from django.db import models
from oauth2client.contrib.django_util.models import CredentialsField


class CredentialsModel(models.Model):
    credential = CredentialsField()

Time to create migrations and migrate:

(djangoyt) ➜  ~/projects/django-youtube $ ./manage.py makemigrations core

(djangoyt) ➜  ~/projects/django-youtube $ ./manage.py migrate

Now let’s change our API views to be able to authorize our application:

In our core/urls.py file, let’s add another entry for the first authorization view:

# core/urls.py
from .views import AuthorizeView, HomePageView


urlpatterns = [
    # [...]
    path('authorize/', AuthorizeView.as_view(), name='authorize'),
]

So, the first part of the AuthorizeView will be:

# core/views.py

from django.conf import settings
from django.shortcuts import render, redirect
from django.views.generic.base import View

from oauth2client.client import flow_from_clientsecrets, OAuth2WebServerFlow
from oauth2client.contrib import xsrfutil
from oauth2client.contrib.django_util.storage import DjangoORMStorage
from .models import CredentialsModel

# [...]

class AuthorizeView(View):

    def get(self, request, *args, **kwargs):
        storage = DjangoORMStorage(
            CredentialsModel, 'id', request.user.id, 'credential')
        credential = storage.get()
        flow = OAuth2WebServerFlow(
            client_id=settings.GOOGLE_OAUTH2_CLIENT_ID,
            client_secret=settings.GOOGLE_OAUTH2_CLIENT_SECRET,
            scope='https://www.googleapis.com/auth/youtube',
            redirect_uri='http://localhost:8888/oauth2callback/')

        # or if you downloaded the client_secrets file
        '''flow = flow_from_clientsecrets(
            settings.GOOGLE_OAUTH2_CLIENT_SECRETS_JSON,
            scope='https://www.googleapis.com/auth/youtube',
            redirect_uri='http://localhost:8888/oauth2callback/')'''

And then the second part:

        if credential is None or credential.invalid == True:
            flow.params['state'] = xsrfutil.generate_token(
                settings.SECRET_KEY, request.user)
            authorize_url = flow.step1_get_authorize_url()
            return redirect(authorize_url)
        return redirect('/')

So if there is no credential or the credential is invalid, generate one and then redirect it to the authorize URL. Otherwise, just go to the homepage so we can upload a video!

Let’s access the view now and see what happens:

Authorization error

Let’s create a user then, before going to that page.

(djangoyt) ➜  ~/projects/django-youtube $ python manage.py createsuperuser

Username (leave blank to use 'ivan'): ivan
Email address: ivan***@mail.com
Password:
Password (again):
This password is too short. It must contain at least 8 characters.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

Let’s also log in with it via /admin. After, let’s access our /authorize/ view again.

Let's login

Authorization

Then,

404 Error

OK, it tried to redirect to the callback URL we configured long ago with Google. Now we need to implement the callback view.

Let’s add one more entry to our core/urls.py:

# core/urls.py

from .views import AuthorizeView, HomePageView, Oauth2CallbackView

urlpatterns = [
    # [...]
    path('oauth2callback/', Oauth2CallbackView.as_view(),
         name='oauth2callback')
]

And another view:

# core/views.py


# the following variable stays as global for now
flow = OAuth2WebServerFlow(
    client_id=settings.GOOGLE_OAUTH2_CLIENT_ID,
    client_secret=settings.GOOGLE_OAUTH2_CLIENT_SECRET,
    scope='https://www.googleapis.com/auth/youtube',
    redirect_uri='http://localhost:8888/oauth2callback/')
# or if you downloaded the client_secrets file
'''flow = flow_from_clientsecrets(
    settings.GOOGLE_OAUTH2_CLIENT_SECRETS_JSON,
    scope='https://www.googleapis.com/auth/youtube',
    redirect_uri='http://localhost:8888/oauth2callback/')'''


# [...]


class Oauth2CallbackView(View):

    def get(self, request, *args, **kwargs):
        if not xsrfutil.validate_token(
            settings.SECRET_KEY, request.GET.get('state').encode(),
            request.user):
                return HttpResponseBadRequest()
        credential = flow.step2_exchange(request.GET)
        storage = DjangoORMStorage(
            CredentialsModel, 'id', request.user.id, 'credential')
        storage.put(credential)
        return redirect('/')

Note: The flow was moved to outside of the AuthorizeView, becoming global. Ideally, you should create it under the AuthorizeView and save in a cache, then retrieve it in the callback. But that is out of the scope of this post.

The get method of AuthorizeView is now:

    def get(self, request, *args, **kwargs):
        storage = DjangoORMStorage(
            CredentialsModel, 'id', request.user.id, 'credential')
        credential = storage.get()

        if credential is None or credential.invalid == True:
            flow.params['state'] = xsrfutil.generate_token(
                settings.SECRET_KEY, request.user)
            authorize_url = flow.step1_get_authorize_url()
            return redirect(authorize_url)
        return redirect('/')

You can take a look at similar implementations here. The oauth2client package itself provides views but I particularly prefer to implement my custom Oauth view.

Now if you try the /authorize/ URL again, the OAuth flow should work. Time to see if this work is worth it and upload our video! The HomePageView will first check for credentials and if it’s all good, we are ready for uploading our video.

Let’s check how our new code for the HomePageView will look:

import tempfile
from django.http import HttpResponse, HttpResponseBadRequest
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload


class HomePageView(FormView):
    template_name = 'core/home.html'
    form_class = YouTubeForm

    def form_valid(self, form):
        fname = form.cleaned_data['video'].temporary_file_path()

        storage = DjangoORMStorage(
            CredentialsModel, 'id', self.request.user.id, 'credential')
        credentials = storage.get()

        client = build('youtube', 'v3', credentials=credentials)

        body = {
            'snippet': {
                'title': 'My Django Youtube Video',
                'description': 'My Django Youtube Video Description',
                'tags': 'django,howto,video,api',
                'categoryId': '27'
            },
            'status': {
                'privacyStatus': 'unlisted'
            }
        }

        with tempfile.NamedTemporaryFile('wb', suffix='yt-django') as tmpfile:
            with open(fname, 'rb') as fileobj:
                tmpfile.write(fileobj.read())
                insert_request = client.videos().insert(
                    part=','.join(body.keys()),
                    body=body,
                    media_body=MediaFileUpload(
                        tmpfile.name, chunksize=-1, resumable=True)
                )
                insert_request.execute()

        return HttpResponse('It worked!')

And the new template:

{# core/templates/core/home.html #}

        <!DOCTYPE html>
        <html>
        <body>

        <h1>Upload your video</h1>
        <p>Here is the form:</p>
        <form action="." method="post" enctype="multipart/form-data">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Submit">
        </form>

        </body>
        </html>

Don’t forget to add the video field to YouTubeForm:

class YouTubeForm(forms.Form):
    video = forms.FileField()

Here we go!

Upload form

And then, checking at your YouTube account Studio page (it’s important to have a channel):

Uploaded video

Voila!

Closing Notes

The code needs some improvement, but it’s a good starting point. I hope it helped with most of the Google’s YouTube API Integration problems. Here are a few more important things to note:

  • For authorization, it’s important to require login and extra permissions for the user that will authorize your application to be uploading videos.
  • The flow variable needs to be moved out from being global. It isn’t safe in a production environment. It’s better to cache based on the user ID or session who accessed the first view, for instance.
  • Google only provides a refresh token when you do the first authorization. So after some time, mostly one hour, your token will expire and if you didn’t interact with their API you will start receiving invalid_grant responses. Reauthorizing the same user who already authorized a client will not guarantee your refresh token. You have to revoke the application in your Google Accounts page and then do the authorization process again. In some cases, you might need to run a task to keep refreshing the token.
  • We need to require login in our view since we are using a user credential directly related to the request.

FlowExchange Error

Uploading takes a lot of time, and doing it in your main application process can cause the entire application to block while the upload happens. The right way would be to move it into its own process and handle uploads asynchronously.

Confused? Don’t be, read more in Orchestrating a Background Job Workflow in Celery for Python.

Understanding the Basics

What is Django and why it is used?

From the official website: “Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design”. It’s the most biggest Python Web Framework and it’s widely used because it offers tools for fast web development, including templating, database handling and security.

About the author

Ivan Neto, Brazil
member since March 7, 2014
Ivan is an engineer with eight years of experience in software and web development. Largely focused on the web, he has solved code optimization problems arising from growing systems, as well as the migrated production apps and services on-the-fly with zero downtime. While a major part of his experience has been on the back-end, he is also able to work with the front-end, databases, networking, and infrastructure. [click to continue...]
Hiring? Meet the Top 10 Freelance Django Developers for Hire in December 2018

Comments

Lokesh Sanapalli
Interesting post... Thanks :)
Muhammad Taqi Hassan
Nice one, please also write about rest api with django.
MHmasuk
when try to upload video why i find this error, can you please give the correction. 'InMemoryUploadedFile' object has no attribute 'temporary_file_path'
B Mike
Thanks for the great post. Do you know how to query youtube data api with requests library ?
comments powered by Disqus
Subscribe
Free email updates
Get the latest content first.
No spam. Just great articles & insights.
Free email updates
Get the latest content first.
Thank you for subscribing!
Check your inbox to confirm subscription. You'll start receiving posts after you confirm.
Trending articles
Relevant Technologies
About the author
Ivan Neto
Python Developer
Ivan is an engineer with eight years of experience in software and web development. Largely focused on the web, he has solved code optimization problems arising from growing systems, as well as the migrated production apps and services on-the-fly with zero downtime. While a major part of his experience has been on the back-end, he is also able to work with the front-end, databases, networking, and infrastructure.