YouTube

Using the YouTube Data API with App Engine and the Python client library

Stephanie Liu & Jochen Hartmann, Google Data APIs Team
August 2008

Introduction

This article is based on a two-hour hands-on codelab presented at Google I/O earlier this year. It is written in the form or an interactive tutorial, and will walk you through the basics of using the Google Data Python Client Library with App Engine. It will cover everything from fetching feeds, to authentication and how to upload videos to YouTube. All of the code samples used here can be found on the hello-youtube project page.

Prerequisites

In order to get the most out of this tutorial, the following prerequisites are assumed:

For the upload portion of the codelab, you will also need:

Your YouTube test account should contain at least one private video so that authenticated feed retrieval can be tested.

Overview of the tools used in this tutorial

Since App Engine has been released for some time now, I will skip the longer introduction to App Engine. In a nutshell, Google App Engine enables you to build web applications on the same scalable systems that power Google applications. Some of the features that will be touched on in this tutorial include:

The YouTube Data API is a full read/write API that allows you to access most of the functionality that you get on the YouTube site. You can use it in read-only mode to retrieve various feeds and to perform complex queries. When authenticated you perform write operations such as uploading videos, posting comments, manipulating playlists, etc.

Using the API allows you to save yourself the hassle that is normally associated with video hosting. You don't need to worry about scaling or bandwidth since all of the content will come from YouTube's servers.

The YouTube Data API is one of the many 'Google Data' classes of APIs which exist for Blogger, Google Spreadsheets, etc. All of these APIs provide a simple standard protocol for reading and writing data to the web. The protocol is based upon the Atom 1.0 and RSS 2.0 syndication format, plus the Atom Publishing Protocol. Protocol requests can be made using simple HTTP verbs such as GET, POST, PUT and DELETE in a RESTful way.

The Python Client Library makes it easier to work with the API by abstracting the often verbose XML requests and responses into manageable objects. It also provides simple methods to interact with the YouTube Data API. The client library is open source and works for all of the Google Data APIs.

Writing a simple App Engine application

To get started quickly with App Engine, simply create two files in a folder named codelab. The first file needs to be called app.yaml and will contain your application's configuration:

application: codelab
version: 1
runtime: python
api_version: 1

handlers:
- url: /.*
  script: main.py

The options are setting name, version (of our application), runtime and API version. The handlers section is a little bit more interesting. Here is where you can define which Python scripts get executed when specific URLs are accessed. In this sample application, all URLs /.* are mapped to be handled by the main.py script. So this means that main.py gets executed when any URL in your application is accessed.

Now, let's create the main.py file:

import wsgiref.handlers
from google.appengine.ext import webapp

class MainPage(webapp.RequestHandler):
  def get(self):
    self.response.headers[[]'Content-Type'] = 'text/plain'
    self.response.out.write('Hello, App Engine!')

def main():
  application = webapp.WSGIApplication(
                                       [[]('/', MainPage)],
                                       debug=True)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
  main()

This file contains a few imports, a class, and a main method. This format will be used through the tutorial with some slight modifications as the example applications become a little bit more complicated. Therefore, it is a good idea to examine these basic building blocks more closely. As you can see, the required modules need to be imported first. The wsgiref.handlers and webapp modules are required to allow the application to handle CGI requests and map them to specific classes.

The MainPage class is the only request handler for the application. This class has one method, get(), which is executed on any HTTP GET request to any URL that maps to this file within our application. The body of the method sets the Content-Type header to 'text/plain', and writes 'Hello, App Engine!' to the HTTP response output.

The main() method sits outside of the MainPage class and is executed when the module is loaded.

Assuming that you have downloaded and correctly installed the App Engine SDK, start up your development webserver from the command line:

dev_appserver.py /path_to_project/codelab/

And then navigate to http://localhost:8080 to see your application.

Note: For Macintosh users, you can use the Google App Engine Launcher to test and deploy your applications.

If you run into trouble launching this application, please check out our source code and the the App Engine documentation.

Test driving the Python Client Library

Python's interactive interpreter will be used in this section. Assuming that you have installed the Python Client Library correctly, it should be pretty easy to follow along:

jochen@mybox:~$ python
Python 2.4.3 (#2, Mar  7 2008, 01:58:20) 
[[]GCC 4.0.3 (Ubuntu 4.0.3-1ubuntu5)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import gdata.youtube
>>> import gdata.youtube.service

First, the required gdata.youtube and gdata.youtube.service modules need to be imported. In the snippet below, an instance of a read-only YouTubeService object is created. I then call the GetRecentlyFeaturedVideoFeed() method to retrieve a feed from YouTube. The feed object contains a list of 25 YouTubeVideoEntry objects whose metadata can be extracted easily:

>>> client = gdata.youtube.service.YouTubeService()
>>> feed = client.GetRecentlyFeaturedVideoFeed()
>>> len(feed.entry)
25
>>> for entry in feed.entry:
...     print "%s %s | %s" % (entry.title.text, entry.rating.average, entry.id.text)
... 

Your results will vary, but at the time of writing this article, I received the following output:

little boots MEDDLE bedroom version - acoustic on piano, tenorion and stylophone oo la la  4.36 | http://gdata.youtube.com/feeds/api/videos/Zcc8gE54Md8
GroundReport Launches Beijing Olympics Video Contest 2.90 | http://gdata.youtube.com/feeds/api/videos/Gy7EQoSISEU
Telephone Piano 4.64 | http://gdata.youtube.com/feeds/api/videos/zV4ISDTnRLM
The Line on C-Spot - Episode 5 4.27 | http://gdata.youtube.com/feeds/api/videos/CM7lokXWyL0
...

One thing to notice is that the structure of a YouTubeVideoEntry closely mirrors the underlying XML. In the snippet, below, the pprint module is imported and then more details are printed about the first entry in the feed:

>>> entry = feed.entry[[]0]
>>> import pprint
>>> pprint.pprint(entry)
<gdata.youtube.YouTubeVideoEntry object at 0xf7cd0d4c>
>>> pprint.pprint(entry.__dict__)
{'_GDataEntry__id': <atom.Id object at 0xf7cd0eac>,
 'author': [[]<atom.Author object at 0xf7cd56ac>],
 'category': [[]<atom.Category object at 0xf7cd0f6c>,
              <atom.Category object at 0xf7cd0fac>,
              <atom.Category object at 0xf7cd502c>,
              <atom.Category object at 0xf7cd506c>,
              <atom.Category object at 0xf7cd50cc>,
              <atom.Category object at 0xf7cd512c>,
              <atom.Category object at 0xf7cd518c>,
              <atom.Category object at 0xf7cd51ec>,
              <atom.Category object at 0xf7cd524c>,
              <atom.Category object at 0xf7cd52ac>,
              <atom.Category object at 0xf7cd52ec>,
              <atom.Category object at 0xf7cd534c>,
              <atom.Category object at 0xf7cd53ac>,
              <atom.Category object at 0xf7cd540c>,
              <atom.Category object at 0xf7cd548c>],
 'comments': <gdata.youtube.Comments object at 0xf7cd5f2c>,
 'content': <atom.Content object at 0xf7cd552c>,
 'contributor': [[]],
 'control': None,
 'extension_attributes': {},
 'extension_elements': [[]],
 'geo': None,
 'link': [[]<atom.Link object at 0xf7cd558c>,
          <atom.Link object at 0xf7cd55ec>,
          <atom.Link object at 0xf7cd562c>,
          <atom.Link object at 0xf7cd566c>],
 'media': <gdata.media.Group object at 0xf7cd570c>,
 'noembed': None,
 'published': <atom.Published object at 0xf7cd0eec>,
 'racy': None,
 'rating': <gdata.youtube.Rating object at 0xf7cd580c>,
 'recorded': None,
 'rights': None,
 'source': None,
 'statistics': <gdata.youtube.Statistics object at 0xf7cd0d6c>,
 'summary': None,
 'text': None,
 'title': <atom.Title object at 0xf7cd54ec>,
 'updated': <atom.Updated object at 0xf7cd0f2c>}

Most of the actual metadata, such as the entry's description is contained in the gdata.media.Group object:

>>> pprint.pprint(entry.media.__dict__)
{'category': [[]<gdata.media.Category object at 0xf7cd596c>],
 'content': [[]<gdata.media.Content object at 0xf7cd5a2c>,
             <gdata.media.Content object at 0xf7cd5a6c>,
             <gdata.media.Content object at 0xf7cd5b2c>],
 'credit': None,
 'description': <gdata.media.Description object at 0xf7cd586c>,
 'duration': <gdata.media.Duration object at 0xf7cd592c>,
 'extension_attributes': {},
 'extension_elements': [[]],
 'keywords': <gdata.media.Keywords object at 0xf7cd58cc>,
 'name': None,
 'player': <gdata.media.Player object at 0xf7cd5bcc>,
 'private': None,
 'text': None,
 'thumbnail': [[]<gdata.media.Thumbnail object at 0xf7cd5c6c>,
               <gdata.media.Thumbnail object at 0xf7cd5cac>,
               <gdata.media.Thumbnail object at 0xf7cd5d4c>,
               <gdata.media.Thumbnail object at 0xf7cd5dec>],
 'title': <gdata.media.Title object at 0xf7cd582c>}

The next section will explain how to hook the Python Client Library into App Engine and display videos in your App Engine application.

Using the Python Client Library with App Engine

The next example will integrate the Python Client Library into a working App Engine application and display the 'Recently Featured' video feed that was examined in the previous section. This example application uses static files in addition to the standard main.yaml and main.py files that were used in the previous example.

The first thing to do is make another folder for this example application and call it client-library-example. You will need to copy the atom, gdata and stylesheets folders into it. The file organization should match the layout shown below. Note that the main.css file is also added into the stylesheets directory.

02_hello_python_client_library
|     app.yaml
|     main.py
|
----atom
|         service.py
|         __init__.py
|
----gdata
|     |     auth.py
|     |     service.py
|     |     urlfetch.py
|     |     __init__.py
|     |
|     |------geo
|     |         __init__.py
|     |
|     |------media
|     |         __init__.py
|     |
|     ----youtube
|             service.py
|             __init__.py
|
----stylesheets
       main.css

Note: If you want to make sure you have everything, you can download the files for this example from the hello-youtube project site.

Since all of the files in the directories are static, you don't need to worry about the code inside of them. The only thing that needs to be changed are the updated app.yaml and main.py files. Our new app.yaml file looks very similar to the one in the first example, with the exception of adding a static directory that references our stylesheet:

application: hello-python-client-library
version: 1
api_version: 1
runtime: python

handlers:
- url: /stylesheets
  static_dir: stylesheets

- url: .*
  script: main.py

Note: You do not need to make any special declarations here for the Python Client Library files since they are not accessed from a URL.

The handlers section now also specifies that any requests to the URL /stylesheets should be handled by whatever is in the stylesheets directory. Next, construct a new main.py file which will contain the bulk of the new application. Since this file contains a fair amount of code, it will be examined in sections. First, let us look at the updated imports:

import wsgiref.handlers
from google.appengine.ext import webapp

import gdata.youtube
import gdata.youtube.service
import gdata.alt.appengine

All that changed is that the Python Client Library imports are now added, which were used in the command line demonstration. In addition, the gdata.alt.appengine module is also imported which is used to modify the client object to use App Engine APIs when communicating with the YouTube servers.

Note: Please read the available articles on using Google Data APIs with App Engine: first, second for further details behind using gdata.alt.appengine.run_on_appengine.

The following snippets will describe the MainPage request handler:

class MainPage(webapp.RequestHandler):

  def get(self):
    client = gdata.youtube.service.YouTubeService()
    gdata.alt.appengine.run_on_appengine(client)
    feed = client.GetRecentlyFeaturedVideoFeed()

    self.response.out.write("""<html><head><title>
        hello_python_client_library: Retrieving a YouTube feed</title>
        <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" />
        </head><body><div id="video_listing">""")

    self.response.out.write('<span class="listing_title">Recently Featured '
        'Videos</span><br /><br />')

    self.response.out.write('<table border="0" cellpadding="2" '
        'cellspacing="0">')

The code in the snippet above should be familiar, since it is the same thing shown in the command line demonstration section. The following snippet shows how to iterate through the feed and display the results:

    for entry in feed.entry:
      self.response.out.write('<tr><td class="thumbnail">')
      self.response.out.write(
          '<img src="%s" /><br /><br />' % entry.media.thumbnail[[]0].url)
      self.response.out.write(
          '<span class="video_rating">Rating: %s of 5 stars<br/>%s Votes '
          '</span>' % (entry.rating.average, entry.rating.num_raters))
      self.response.out.write('</td>')

      self.response.out.write('<td valign="top">')
      self.response.out.write(
          '<span class="video_title">%s</span><br />' % entry.title.text)
      self.response.out.write(
          '<span class="video_description">%s</span>'
          '<br /><br />' % entry.media.description)

The embeddable player section has been commented out in this example. Feel free to uncomment the below to show the embeddable player in your demo application.

    # uncomment this section to show the embeddable player
    # if entry.GetSwfUrl():
    #   self.response.out.write('<object width="425" height="350">'
    #      '<param name="movie" value="' + entry.GetSwfUrl() + '"></param>'
    #       '<embed src="' + entry.GetSwfUrl() + 
    #       '" type="application/x-shockwave-flash" '
    #       'width="425" height="350"></embed></object>')

      self.response.out.write(
          '<span class="video_category"><strong>%s</strong></span>' % 
          entry.media.category[[]0])
      self.response.out.write('<span class="video_published"> | published '
          'on %s</span><br />' % (entry.published.text.split('T')[[]0] + ' at ' +
          entry.published.text.split('T')[[]1][[]:5] + ' PST'))
      self.response.out.write('<span class="video_keywords"><strong>Keywords:'
          '</strong> %s </span><br />' % entry.media.keywords)
      self.response.out.write('</td></tr>')
      self.response.out.write(
          '<tr><td height="20" colspan="2"><hr class="slight"/></tr>')

After iterating through the feed, simply end the HTML output:

    self.response.out.write('</table></div>')
    self.response.out.write('</body></html>')

Note: Please keep in mind that self.response.out.write is used primarily for the sake of simplicity in these demo applications. For production applications, you should use templates.

The remaining code is the same as in the first example:

def main():
  application = webapp.WSGIApplication([[]('/.*', MainPage),], debug=True)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
  main()

Assuming everything worked, your application should display the recently featured video feed and look like this:



The next section builds upon this example and shows how to perform a video search.

The following snippet shows how to perform a basic video query using the Python Client Library by creating a gdata.youtube.YouTubeVideoQuery object, setting a few search parameters, and retrieving a feed of video results:

client = gdata.youtube.service.YouTubeService()
gdata.alt.appengine.run_on_appengine(client)
query = gdata.youtube.service.YouTubeVideoQuery()

query.vq = 'bicycle dalmatian' # the term(s) that you are searching for
query.orderby = 'viewCount'    # how to display the results
query.max_results = '5'        # number of results to retrieve

self.response.out.write('<span class="listing_title">Searching for "' +
  query.vq + '"</span><br /><br />')

feed = client.YouTubeQuery(query)

Again you end up with a feed object that can be iterated through as described in the last example. For more information about other query parameters, please refer to the Query parameter defintions section of the YouTube API reference guide.

The last concept to introduce before writing more code for this application is form handling within App Engine. The previous examples showed how to handle HTTP GET methods. This time, however, information is submitted using HTTP POST, so a post() method needs to be added. First, here are the imports. This time, the cgi module is added:

import cgi
import wsgiref.handlers
from google.appengine.ext import webapp

import gdata.youtube
import gdata.youtube.service
import gdata.alt.appengine

The new request handler, SearchPage is defined below with its get() printing out a simple HTML form:

class SearchPage(webapp.RequestHandler):
  def get(self):
    self.response.out.write("""
      <html>
        <body><br/>Please enter a search term:<br/>
          <form action="/" method="post">
            <div><input type="text" name="content" /></div>
            <div><input type="submit" value="Search YouTube"></div>
          </form>
        </body>
      </html>""")

Next, the post() method is defined to escape and handle input from the form. The search term is then passed to a simple YouTubeVideoQuery.

  def post(self):
    search_term = cgi.escape(self.request.get('content')).encode('UTF-8')
    if not search_term:
        self.redirect('/')
        return

    self.response.out.write("""<html><head><title>
        hello_youtube_search_query: Searching YouTube</title>
        <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" />
        </head><body><div id="video_listing">""")

    self.response.out.write("""
        <span class="listing_title">
        Searching for '%s'</span><br /><br />""" % search_term)

    self.response.out.write('<table border="0" cellpadding="2" '
        'cellspacing="0">')

    client = gdata.youtube.service.YouTubeService()
    gdata.alt.appengine.run_on_appengine(client)
    query = gdata.youtube.service.YouTubeVideoQuery()

    query.vq = search_term
    query.max_results = '5'
    feed = client.YouTubeQuery(query)

Iterating through the result feed should be familiar. This time, the Flash player is also displayed:

    for entry in feed.entry:
      if entry.GetSwfUrl():
        swf_url = entry.GetSwfUrl()
        self.response.out.write(
            '<tr><td><span class="video_title">%s</span><br /><br />' % 
            entry.title.text)
        self.response.out.write("""<object width="425" height="355">
            <param name="movie" value="%s"></param>
            <param name="wmode" value="transparent"></param>
            <embed src="%s" type="application/x-shockwave-flash" 
            wmode="transparent" width="425" height="355"></embed></object>
            <br />""" % (swf_url, swf_url))
        self.response.out.write(
            '<span class="video_description">%s</span>'
            '<br />' % entry.media.description)
        if entry.rating:
          self.response.out.write(
              '<span class="video_rating">Rating: %s of 5 stars<br/>%s Votes '
              '</span></td></tr>' % 
              (entry.rating.average, entry.rating.num_raters))
        self.response.out.write(
            '<tr><td height="20"><hr class="slight"/></tr>')
    self.response.out.write('</table><br />')
    self.response.out.write("""Search:<br />
        <form action="/" method="post">
        <div><input type="text" name="content" /></div>
        <div><input type="submit" value="Search YouTube"></div>
        </form>
        """)

    self.response.out.write('</body></html>')

The remaining code is the same as before:

def main():
  application = webapp.WSGIApplication(
                                       [[]('/', SearchPage)],
                                        debug=True)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
  main()

This simple application should now be able to execute YouTube Data API queries. For a more complex example, please see the YouTube Data API search application running on App Engine. It follows the example set up here, except that it handles many more search parameters. Have a look at the source code on the project page under the 04_hello_user_input folder.

Note: For more details on how to handle form input with App Engine, please check out the App Engine documentation.

Authentication and performing a video upload

And now for the last section of the tutorial: video uploads. As mentioned before, any operations that write data require authentication. There are two methods that can be used to authenticate your client object to YouTube. These will work on all of the Google Data APIs and they are either ClientLogin for Desktop and installed application or AuthSub for multi-user web applications. The AuthSub authorization process is based around two tokens that need to exchanged between your application and YouTube's servers. Conceptually the process consists of the following steps:

  1. Your application creates a link that the user can click on to log in to their YouTube account.
  2. Your user visits this link and gets directed to the YouTube sign in page.
    • User signs in and is presented with a screen that asks them to allow your application access to their YouTube account.
    • If the user agrees, they get redirected back to your application with a one-time use token appended to the URL.
  3. Your application grabs the one-time token from the URL and makes a simple call back to YouTube's servers, asking to exchange this one-time use token for a permanent session token.
  4. Your application then stores the session token in App Engine's datastore.

The first application will simply retrieve a user's uploads feed. You may want to sign in to YouTube now and mark a video in the feed as private. This video should no longer show up on the YouTube web site, but will show up when making an authenticated request.

The first step, as before is to import the required modules. Our list now includes urllib for handling URL parameters, as well as the db datastore module and the users module to handle user login.

import wsgiref.handlers
import urllib
import cgi
from google.appengine.ext import webapp
from google.appengine.api import users
from google.appengine.ext import db

import gdata.youtube
import gdata.youtube.service
import gdata.media
import gdata.geo
import gdata.alt.appengine

To use App Engines scalable datastore to store our session tokens, a simple data model needs to be defined first. It will contain two fields: the user's email and the session token:

class StoredToken(db.Model):
  user_email = db.StringProperty(required=True)
  session_token = db.StringProperty(required=True)

The next step is to write the AuthSub request handler. This time an __init__ method is used to prepare a few member variables:

class AuthSub(webapp.RequestHandler):

  def __init__(self):
    self.current_user = None
    self.token = None
    self.feed_url = 'http://gdata.youtube.com/feeds/api/users/default/uploads'
    self.youtube_scope = 'http://gdata.youtube.com'
    self.developer_key = None
    self.client = gdata.youtube.service.YouTubeService()
    gdata.alt.appengine.run_on_appengine(self.client)

The get method is slightly more complicated this time around. Conceptually, the following steps need to be performed:

  • Get the user that is currently logged into our application with a call to users.GetCurrentUser().
  • Interpret any URL parameters (only present after the has signed in to YouTube)
  • If a current user is found, then call to LookupToken() to see whether there is a matching session token for the user, matching on email address.
    • If yes, print out their video feed and return.
    • If no, check whether a one-time token was returned in the URL parameters. If yes, the user has just signed in to YouTube and allowed this application access. A call to UpgradeAndStoreToken() is made to upgrade and store the session token.
  • If there is no current user, display a log in page using users.CreateLoginURL()

The next code snippet shows how this process is implemented in the get method:

  def get(self):
    self.my_app_domain = 'http://' + self.request._environ[[]'HTTP_HOST']
    self.response.out.write("""<html><head><title>
        hello_authsub: AuthSub demo</title>
        <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" />
        """)

    self.current_user = users.GetCurrentUser()
    self.response.out.write('</head><body>')

    # Split URL parameters if found
    for param in self.request.query.split('&'):
      if param.startswith('token'):
        self.token = param.split('=')[[]1]
      elif param.startswith('feed_url'):
        self.feed_url = urllib.unquote_plus(param.split('=')[[]1])

    if self.current_user:
      self.response.out.write('<a href="%s">Sign Out</a><br /><br />' % (
          users.CreateLogoutURL(self.request.uri)))

      if self.LookupToken():
        self.response.out.write('<div id="video_listing">')
        self.FetchFeed()
        self.response.out.write('</div>')

      else:
        # Check if a one-time use token was passed in the URL parameters
        if self.token:
          self.UpgradeAndStoreToken()
          self.redirect('/')

If no one-time use token was found either, but a current user is found, then create the link to the YouTube sign in page from where they can authorize our application:

        else:
          self.response.out.write('<div id="sidebar"> '
              '<div id="scopes"><h4>Request a token</h4><ul>')
          self.response.out.write('<li><a href="%s">YouTube API</a></li>' % (
              self.client.GenerateAuthSubURL(
              self.my_app_domain, self.youtube_scope, secure=False, session=True))
              )

This is accomplished using a call to GenerateAuthSubURL, a method of the gdata.youtube.service.YouTubeService() class, which is actually inherited from the gdata.service.GDataService base class. Four parameters need to be passed to this method:

  • self.my_app_domain — The URL of the application that initiated the first token request. The user is redirected to this URL after they have allowed access.
  • self.youtube_scope — The scope of the authorization request. This variable controls the scope that the returned token will be applicable to. Set to 'http://gdata.youtube.com'.
  • secure — This parameter requests a 'secure' token and is only used in signed requests for registered applications.
  • session — The session parameter needs to be set to True in order to receive a one-time token that is upgradable to a session token.

Note: For more information about the AuthSub registration process, please refer to the documentation.

Lastly, if no current user is found, display a log-in page:

    else:
      self.response.out.write('<a href="%s">Sign In</a><br />' % (
          users.CreateLoginURL(self.request.uri)))

The remaining sections cover the FetchFeed, UpgradeAndStoreToken and LookupToken helper methods. The FetchFeed method retrieves an authenticated user uploads feed and iterates through it, using code from the previous examples:

  def FetchFeed(self):
    # Get users video feed
    if not self.client:
      self.client = gdata.youtube.service.YouTubeService()
      gdata.alt.appengine.run_on_appengine(self.client)

One thing to note is that this application contains basic error handling around the request:

    try:
      feed = self.client.GetYouTubeVideoFeed(self.feed_url)

      self.response.out.write(
          '<span class="listing_title">My Videos</span><br /><br />')
      self.response.out.write(
          '<table border="0" cellpadding="2" cellspacing="0">')

      for entry in feed.entry:
        self.response.out.write('<tr><td class="thumbnail">')
        if len(entry.media.thumbnail) > 0:
          self.response.out.write(
              '<img src="%s"></p>' % entry.media.thumbnail[[]0].url)
        else:
          self.response.out.write(' ')
        self.response.out.write('</td>')

        self.response.out.write('<td valign="top">')
        self.response.out.write(
            '<span class="video_title">%s </span><br />' % entry.title.text)
        self.response.out.write(
            '<span class="video_description">%s </span><br />' % 
            entry.media.description)
        self.response.out.write(
            '<span class="video_category"><strong>%s</strong></span>' % 
            entry.media.category[[]0])
        self.response.out.write('<span class="video_published"> | published '
            'on %s</span><br />' % (entry.published.text.split('T')[[]0] + ' at ' +
            entry.published.text.split('T')[[]1][[]:5] + ' PST'))
        self.response.out.write('<span class="video_keywords"><strong>Keywords:'
            '</strong> %s </span><br />' % entry.media.keywords)

        self.response.out.write('</td></tr>')
        self.response.out.write(
          '<tr><td height="20" colspan="2"><hr class="slight"/></tr>')

      self.response.out.write('</table><br />')

If there is an error, check the HTTP status code. If it is a 401, display a link for the user to sign in to YouTube:

    except gdata.service.RequestError, request_error:
      if request_error[[]0][[]'status'] == 401:
        self.response.out.write(
            '<div id="sidebar"><div id="scopes"><h4>Request a token</h4><ul>')
        self.response.out.write(
            '<li><a href="%s">YouTube API</a></li>' % (
            self.client.GenerateAuthSubURL(
            self.my_app_domain, self.youtube_scope, secure=False, session=True))
        )
      else:
        self.response.out.write(
            'Something else went wrong, here is the error object: %s ' % (
                str(request_error[[]0])))

The UpgradeAndStoreToken method upgrades the one-time token to a session token and stores that in the application's datastore. To store the upgraded token, instantiate a new StoredToken object and call its put method to persist it.

  def UpgradeAndStoreToken(self):
    self.client.SetAuthSubToken(self.token)
    self.client.UpgradeToSessionToken()
    if self.current_user:
      new_token = StoredToken(user_email=self.current_user.email(), 
          session_token=self.client.GetAuthSubToken())
      new_token.put()

The LookupToken method performs a lookup operation using a simple GQL query:

  def LookupToken(self):
    if self.current_user:
      stored_tokens = StoredToken.gql('WHERE user_email = :1',
          self.current_user.email())
      for token in stored_tokens:
        self.client.SetAuthSubToken(token.session_token)
        return True

The remaining code is the same as from before:

def main():
  application = webapp.WSGIApplication([[]('/.*', AuthSub),], debug=True)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == '__main__':
  main()

The last example in this tutorial builds upon the AuthSub application and demonstrates how to implement video uploads. If the example detailed above does not work for you, please take a look at the source code in the 05_hello_authsub folder.

Note: For a more detailed article on how to implement AuthSub within Google App Engine, please refer to the article by Jeff Scudder.

Uploading videos

There are two ways to upload videos to the YouTube Data API. One way involves sending video metadata along with a binary of the actual video file in one request. This method is referred to as direct uploading. For web applications, it is sometimes easier to implement the other method, referred to as browser-based uploading. Here, you only submit the video metadata to YouTube. An upload token and a URL are returned which your application can use to build a HTML form that receives the video from the user. In this case you don't have to store the binary at all. The user uploads the file directly to YouTube.

The new application's app.yaml file now also includes a second Python script, upload.py:

application: hello-upload
version: 1
api_version: 1
runtime: python

handlers:
- url: /
  script: main.py

- url: /upload
  script: upload.py

- url: /stylesheets
  static_dir: stylesheets

The imports are the same for this application:

import wsgiref.handlers
import urllib
import cgi
from google.appengine.ext import webapp
from google.appengine.api import users
from google.appengine.ext import db
import gdata.youtube
import gdata.youtube.service
import gdata.media
import gdata.geo
import gdata.alt.appengine

As does our StoredToken data model:

class StoredToken(db.Model):
  user_email = db.StringProperty(required=True)
  session_token = db.StringProperty(required=True)

Then, define the request handler, also called AuthSub:

class AuthSub(webapp.RequestHandler):

  def __init__(self):
    self.current_user = None
    self.token_scope = None
    self.client = None
    self.token = None
    self.feed_url = 'http://gdata.youtube.com/feeds/api/users/default/uploads'
    self.youtube_scope = 'http://gdata.youtube.com'
    self.developer_key = 'ADD YOUR DEVELOPER KEY HERE'

An additional member variable has been added, whose value you will need to replace with your Developer Key.

Note: The final example application's source code actually allows the user to store their developer key in the datastore. For the sake of simplicity it is hard coded here. Please also note that for production applications you would probably want to store the developer key in a configuration file.

The get method performs largely the same thing as before. This time, in addition to calling self.FetchFeed() it also creates a video upload form:

  def get(self):
    self.client = gdata.youtube.service.YouTubeService()
    gdata.alt.appengine.run_on_appengine(self.client)
    self.my_app_domain = 'http://' + self.request._environ[[]'HTTP_HOST']
    self.response.out.write("""<html><head><title>
        hello_browser_based_upload: Browser based video upload demo</title>
        <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" />
        """)

    self.current_user = users.GetCurrentUser()
    self.response.out.write('</head><body>')

    for param in self.request.query.split('&'):
      if param.startswith('token_scope'):
        self.token_scope = urllib.unquote_plus(param.split('=')[[]1])
      elif param.startswith('token'):
        self.token = param.split('=')[[]1]
      elif param.startswith('feed_url'):
        self.feed_url = urllib.unquote_plus(param.split('=')[[]1])

    if self.token and self.feed_url and not self.token_scope:
      self.token_scope = self.feed_url

    if self.current_user:
      self.response.out.write('<a href="%s">Sign Out</a><br /><br />' % (
          users.CreateLogoutURL(self.request.uri)))

      if self.LookupToken():
        self.response.out.write('<div id="video_listing">')
        self.FetchFeed()
        self.DisplayUploadForm()
        self.response.out.write('</div>')

      # not authenticated...
      else:
        if self.token:
          self.UpgradeAndStoreToken()
        else:
          self.response.out.write('<div id="sidebar"> '
              '<div id="scopes"><h4>Request a token</h4><ul>')
          self.response.out.write('<li><a href="%s">YouTube API</a></li>' % (
              self.client.GenerateAuthSubURL(
              self.my_app_domain + '/' + '?token_scope=' + self.youtube_scope, 
              self.youtube_scope, secure=False, session=True))
              )
    else:
      self.response.out.write('<a href="%s">Sign In</a><br />' % (
          users.CreateLoginURL(self.request.uri)))

The FetchFeed, UpgradeAndStoreToken as well as LookupToken methods are the same as from the last example, so they will be omitted from this section.

And finally, build the form that is used to perform the browser-based upload. Note that the POST action redirects the user to the /upload URL, which gets handled by the upload.py script. Other than that detail, there is nothing special about this form:

  def DisplayUploadForm(self):
    self.response.out.write("""<br /><div id="upload_form">
      <strong>Upload a new Video</strong><br /><br />
      <form action="/upload" method="post" >
      Video Title<br /><input type="text" name="video_title" /><br /><br />
      Video Description<br /><textarea cols="50" name="video_description">
      </textarea><br /><br />
      Select a Category <select name="video_category">
        <option value="Autos">Autos & Vehicles</option>
        <option value="Music">Music</option>
        <option value="Animals">Pets & Animals</option>
        <option value="Sports">Sports</option>
        <option value="Travel">Travel & Events</option>
        <option value="Games">Gadgets & Games</option>
        <option value="Comedy">Comedy</option>
        <option value="People">People & Blogs</option>
        <option value="News">News & Politics</option>
        <option value="Entertainment">Entertainment</option>
        <option value="Education">Education</option>
        <option value="Howto">Howto & Style</option>
        <option value="Nonprofit">Nonprofit & Activism</option>
        <option value="Tech">Science & Technology</option>',
      </select><br /><br />
      Enter some tags to describe your video <em>(separated by spaces)</em>
      <br /><br />
      <input type="text" name="video_tags" />
      <input type="submit" value="Go"/>
    </form></div>
    """)

The rest of the file is the same as all the other examples so far:

def main():
  application = webapp.WSGIApplication([[]('/.*', AuthSub),], debug=True)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == '__main__':
  main()

The final piece of the upload application is the upload.py script:

import wsgiref.handlers
import urllib
import cgi
from google.appengine.ext import webapp
from google.appengine.api import users
from google.appengine.ext import db
import gdata.service
import gdata.youtube
import gdata.youtube.service
import gdata.media
import gdata.geo
import gdata.alt.appengine

The data model from before:

class StoredToken(db.Model):
  user_email = db.StringProperty(required=True)
  session_token = db.StringProperty(required=True)

Our request handler, this time called Upload. Please note that, as mentioned earlier, for production applications you would probably want to store all instances of the Developer Key in a single configuration file.

class Upload(webapp.RequestHandler):

  def __init__(self):
    self.current_user = None
    self.token_scope = None
    self.client = None
    self.token = None
    self.feed_url = 'http://gdata.youtube.com/feeds/api/users/default/uploads'
    self.youtube_scope = 'http://gdata.youtube.com'
    self.server_response = None
    self.developer_key = 'ADD YOUR DEVELOPER KEY HERE'

In the post() method, the application checks whether there still is a current user and a session token. Once this is verified, the input from the upload form is validated:

  def post(self):
    self.my_app_domain = 'http://' + self.request._environ[[]'HTTP_HOST']
    self.response.out.write("""<html><head><title>
        hello_browser_based_upload: Browser based upload demo</title>
        <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" />
        </head>""")

    self.current_user = users.GetCurrentUser()
    self.client = gdata.youtube.service.YouTubeService()
    gdata.alt.appengine.run_on_appengine(self.client)
    self.response.out.write('<body>')

    if self.LookupToken():
      video_title = cgi.escape(self.request.get('video_title'))
      video_description = cgi.escape(self.request.get('video_description'))
      video_category = cgi.escape(self.request.get('video_category'))
      video_tags = cgi.escape(self.request.get('video_tags'))

Once the user input is properly escaped, a gdata.media.Group object is created to store the video metadata:

      my_media_group = gdata.media.Group(
        title = gdata.media.Title(text=video_title),
        description = gdata.media.Description(description_type='plain',
                                            text=video_description),
        keywords = gdata.media.Keywords(text=video_tags),
        category = gdata.media.Category(
          text=video_category,
          scheme='http://gdata.youtube.com/schemas/2007/categories.cat',
          label=video_category),
      player=None
      )

Next, a gdata.youtube.YouTubeVideoEntry object is created by passing it the gdata.media.Group:

      video_entry = gdata.youtube.YouTubeVideoEntry(media=my_media_group)

The GetFormUploadToken method of the authenticated gdata.youtube.service.YouTubeService object submits the video entry, containing metadata only, to YouTube:

      try:
        self.server_response = self.client.GetFormUploadToken(video_entry)
      except gdata.service.RequestError, request_error:
        self.response.out.write('<div id="error">')
        self.response.out.write(request_error[[]0][[]'status'])
        self.response.out.write(request_error[[]0][[]'body'])
        if request_error[[]0][[]'reason']:
          self.response.out.write(request_error[[]0][[]'reason'])
        self.response.out.write(
            '<br /><a href="/" style="color: #000">'
            'click here to return</a></div>')
        return

The response is a tuple with two elements, the post_url and our youtube_token which are used to build and return a simple HTML upload form:

      post_url = self.server_response[[]0]
      youtube_token = self.server_response[[]1]
      self.response.out.write("""<div id="file_upload">Upload your video file
          <br /><br />
          <form action="%s?nexturl=%s" method="post" 
          enctype="multipart/form-data">
          <input name="file" type="file" size="50"/>
          <input name="token" type="hidden" value="%s"/>
          <br /><input value="Go" type="submit" />
          </form></div>""" % (post_url, self.my_app_domain, youtube_token))

If the check for a token earlier failed, the application redirects to the start page. A professional application would probably want to do something more intelligent here:

    else:
      self.redirect("/")

The remaining piece of upload.py is the same as before:

def main():
  application = webapp.WSGIApplication([[]('/.*', Upload),], debug=True)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == '__main__':
  main()

Conclusion

Using the Python Client Library within Google App Engine is pretty easy once you know how to hook it up. For more information, check out the YouTube Data API Python Developer's Guide.

Please join us in the discussion groups if you have any questions about using these tools with your favorite Google Data API. For App Engine specific questions, please refer to the App Engine discussion group.

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.