Ivan Neto
Posted on September 30, 2018
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:
- YouTube Data API: Python Quickstart
- YouTube Data API: API Reference
- YouTube Data API: Code Samples
- Google Python API Client Library
- Google Python API Client Library: Reference Doc
- Google Python API Client Library: Code Samples
- YouTube API: Python Code Samples
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
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.
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
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')),
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 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
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
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 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 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:
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.
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')
]
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 oauth2clien
package itself provides views but I particularly prefer to implement my custom Oauth view.
- https://github.com/google/google-api-python-client/blob/master/samples/django_sample/plus/views.py
- https://github.com/google/oauth2client/blob/master/oauth2client/contrib/django_util/views.py
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 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!
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!
Posted on September 30, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.