Integrating the YouTube API to upload videos using Django

Not too long ago, I was helping a client embed video reviews on their website. Like any enthusiastic developer facing a new challenge, I turned to Google for answers. Unfortunately, I was met with a flood of useless or inaccurate advice, suggesting solutions for unrelated problems or pointing me towards outdated and unsupported Python libraries. In the end, my team and I decided to tackle the problem head-on: we built everything from the ground up, creating the necessary views, diving into Google’s API documentation, building a custom API client, and ultimately figuring out how to upload videos programmatically from our Django application.

This blog post will serve as a step-by-step guide on how to post YouTube videos directly from your Django app. Be prepared for some hands-on work with Google API credentials – first through their web interface and then within your code. The YouTube integration itself is surprisingly straightforward. However, understanding the ins and outs of Google’s ecosystem can be tricky, as the information is scattered across multiple resources.

Prerequisites

Before we dive in, I highly recommend familiarizing yourself with the following:

A particularly interesting piece of code from the Google YouTube API Docs is shown in the following Python snippet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 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')

Let’s Get Started

With the prerequisites out of the way, let’s gather the tools we’ll need.

Setting Up Our Toolkit

First, we’ll establish a virtual environment. I’m partial to pyenv, but feel free to use your preferred method. Setting up virtual environments is outside the scope of this post. I’ll be using pyenv commands below. If you prefer virtualenv, feel free to adapt the commands accordingly.

For this project, I’ll be using Python 3.7 and Django 2.1.

1
2
3
4
5
➜  ~/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 add the following to a .python-version file (only necessary if you’re using pyenv) to ensure our environment is activated automatically upon entering the project folder:

1
        djangoyt

Next, we’ll install the required dependencies:

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

Now we can create our Django project:

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

A Brief Interlude for Google Configuration

Before we go any further, we need to set up our project credentials to use the Google APIs.

Step 1. Navigate to the Google Cloud Console:

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

Step 2. Create a new project.

Create a new project

Step 3. Select “Enable APIs and Services.”

Enable APIs and Services.

Step 4. Locate and enable the “YouTube Data API v3.”

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

Step 5. You should receive a message about credentials.

a message about credentials

Step 6. Click the blue “Create credentials” button on the right. You’ll be directed to the following screen:

Click on the "Create credentials" blue button

Step 7. Choose “Web server” for the credential type and “User Data” for the data access:

Choose Web server, User Data

Step 8. Add any required authorized JavaScript origins and redirect URIs. Then, continue to the final step:

Add authorized JS origins and redirect URIs.

With that, our credentials are set! You can either download the credentials as a JSON file or copy the Client ID and Client Secret for later use.

Back to Django

Let’s create our first Django app. I usually call mine “core”:

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

In our main urls.py file, let’s add the following to route homepage requests to our “core” app:

1
2
3
4
5
6
# <root>/urls.py

from django.urls import path, include


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

Within the “core” app, we’ll create another urls.py file with some initial configurations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 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)

As you can see, we have an empty path pointing to HomePageView. Let’s create that view now.

For now, we’ll use a simple TemplateView to get things up and running.

1
2
3
4
5
6
7
# core/views.py
from django.shortcuts import render
from django.views.generic import TemplateView


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

Of course, we’ll need a basic template as well:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# core/templates/core/home.html
<!DOCTYPE html>
<html>
<body>

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

</body>
</html>

Let’s make some necessary tweaks to our settings:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 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/'

Now, we’ll create a YoutubeForm and assign it as the form_class for our view:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 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

If you try running your application at this point, you should see a simple form:

Page preview

A Pause for Authorization

Before we can upload videos, we need a way to store our Google API credentials. While we could use a file, cache system, or other storage solutions, a database feels most appropriate for scalability. Plus, it allows us to easily store credentials for individual users if needed.

However, we need to make a slight adjustment first. We’ll be using a fork of the oauth2client library that supports Django 2.1. Official support is expected soon, but for now, we’ll utilize this fork. The changes are minimal and easy to inspect.

1
2
pip install -e git://github.com/Schweigi/oauth2client.git@v4.1.3#egg=oauth2client
Because of compatibility with Django 2.1

In your settings.py file, paste the Client ID and Client Secret you obtained from Google earlier.

1
2
3
4
# settings.py

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

Important: Hardcoding secrets directly in your code is strongly discouraged. For demonstration purposes, we’re taking this shortcut. However, in a production environment, leverage environment variables or other secure methods to manage your sensitive information. Alternatively, if you downloaded the JSON file containing your credentials, you can specify its path here instead of hardcoding the values:

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

Fortunately, the oauth2client package provides much of the functionality we need, including a pre-built CredentialsField. We could enhance this further with additional fields like a foreign key or created/modified timestamps, but for now, let’s keep things simple.

Here’s a basic model to store our credentials:

1
2
3
4
5
6
7
# core/models.py
from django.db import models
from oauth2client.contrib.django_util.models import CredentialsField


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

Let’s create and apply migrations for this new model:

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

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

Now, let’s modify our API views to handle the authorization process:

In core/urls.py, we’ll add another entry for our authorization view:

1
2
3
4
5
6
7
8
# core/urls.py
from .views import AuthorizeView, HomePageView


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

The initial part of our AuthorizeView will look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 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/')'''

Followed by:

1
2
3
4
5
6
        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('/')

In essence, if no credentials exist or the existing credentials are invalid, we generate new ones and redirect the user to the Google authorization URL. Otherwise, we redirect them directly to the homepage where they can proceed with uploading a video.

Let’s see what happens when we access this view:

Authorization error

Before proceeding, we need to create a user.

1
2
3
4
5
6
7
8
9
(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.

After creating the user, log in through the admin interface (/admin) and then revisit the authorization view (/authorize/).

Let's login
Authorization

Finally, you should see:

404 Error

As you can see, the application attempts to redirect to the callback URL we specified earlier in the Google Cloud Console. Now, we need to implement this callback view.

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

1
2
3
4
5
6
7
8
9
# core/urls.py

from .views import AuthorizeView, HomePageView, Oauth2CallbackView

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

And the corresponding view:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 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: We’ve moved the flow variable outside the AuthorizeView, making it globally accessible. Ideally, this should be stored in a cache (perhaps using the user ID or session) after being generated within the AuthorizeView and then retrieved in the callback. However, this is beyond the scope of our current implementation.

With that in place, the get method of our AuthorizeView should now look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    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 find similar implementations for reference. While the oauth2client package offers built-in views, I prefer implementing custom OAuth flows for greater control.

Now, try accessing the /authorize/ URL again. The OAuth flow should work as expected. It’s time to put our hard work to the test and upload a video! Our updated HomePageView will check for valid credentials, and if everything checks out, we’re ready to upload.

Here’s how the updated HomePageView will look:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 modified template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{# 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 our YouTubeForm:

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

Here we go!

Upload form

If you check your YouTube Studio page (ensure you have a channel created), you should see:

Uploaded video

Success!

Some Final Thoughts

While our code could use some further refinement, it serves as a solid starting point for integrating with Google’s YouTube API. Hopefully, this guide helped clarify some of the common hurdles. Here are a few additional considerations:

  • Security: For production environments, always require user authentication and implement proper authorization checks before granting access to upload videos.
  • Global Variables: Avoid using global variables like our flow variable in production. Consider using a cache or other mechanisms to store and retrieve this data securely.
  • Token Refreshing: Google provides a refresh token during the initial authorization process. However, this token typically expires after a certain duration (usually around one hour). If you haven’t interacted with their API within that timeframe, you’ll start encountering invalid_grant responses. Reauthorizing the same user might not always provide a new refresh token. You might need to revoke the application’s access through your Google Account settings and re-authorize it. In some cases, implementing a task to periodically refresh the token might be necessary.
  • Login Requirements: Since we’re working with user-specific credentials, ensure that users are required to log in before accessing any views that interact with the YouTube API.
FlowExchange Error

Finally, keep in mind that video uploads can be time-consuming. Performing them directly within your main application process might lead to performance issues or even block the entire application. A more robust solution would involve offloading uploads to a separate process, allowing them to run asynchronously in the background.

Licensed under CC BY-NC-SA 4.0