DEV Community

Ivan Neto
Ivan Neto

Posted on • Originally published at toptal.com on

YouTube API Integration: Uploading Videos with Django

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')

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
        djangoyt
Enter fullscreen mode Exit fullscreen mode

Installing dependencies:

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

Now time to start our django project:

➜  ~/projects/django-youtube $ django-admin startproject django_youtube .
Enter fullscreen mode Exit fullscreen mode

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.

Step3. Click "Enable APIs and Services."

Step4. Look for Youtube Data API v3, click "Enable."

Step 5. You should get a message about credentials.

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

Step 7. Choose Web server, User Data

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

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

Enter fullscreen mode Exit fullscreen mode

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

# urls.py

from django.urls import path, include


    path('', include(('core.urls', 'core'), namespace='core')),
Enter fullscreen mode Exit fullscreen mode

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

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'
Enter fullscreen mode Exit fullscreen mode

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

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/'
Enter fullscreen mode Exit fullscreen mode

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

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

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

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/oauth2client.git@v4.1.3#egg=oauth2client
# Because of compatibility with Django 2.0
Enter fullscreen mode Exit fullscreen mode

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>'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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

Time to create migrations and migrate:

(djangoyt) ➜  ~/projects/django-youtube $ ./manage.py makemigrations core
(djangoyt) ➜  ~/projects/django-youtube $ ./manage.py migrate
Enter fullscreen mode Exit fullscreen mode

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'),
]
Enter fullscreen mode Exit fullscreen mode

So 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/')'''
Enter fullscreen mode Exit fullscreen mode

And then 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('/')
Enter fullscreen mode Exit fullscreen mode

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:

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.
Enter fullscreen mode Exit fullscreen mode

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

Then,

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')
]

Enter fullscreen mode Exit fullscreen mode

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

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

You can take a look at similar implementations here. The oauth2clien 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!')
Enter fullscreen mode Exit fullscreen mode

And the 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>
Enter fullscreen mode Exit fullscreen mode

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

class YouTubeForm(forms.Form):
    video = forms.FileField()
Enter fullscreen mode Exit fullscreen mode

Here we go!

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.
  • There is a work in progress based on this experience and this post. Of course there is a lot to be added, but it’s a good starting point: https://github.com/ivancrneto/youtube-django

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.

The code needs some improvement, but it's a good starting point. I hope it helped with most of the Google's API handling and of course uploading your video to YouTube!

Cheers!

Top comments (1)

Collapse
 
mrostgaard profile image
Mark Mortensen

I like it, great explanation. Do you know if anything exists to replace:

from oauth2client.contrib.django_util.models import CredentialsField

I get 'ImportError: cannot import name 'urlresolvers' from 'django.core'' when i try to import it, my understanding is that urlresolver is deprecated in Django v2 (which I don't understand since you are using 2.1?) at least according to this: stackoverflow.com/questions/431390...

Maybe it would be easiest to just skip the oauth2client library completely and just build CredentialsField from the ground up?