Monday, 24 September 2012

Programmatically uploading videos to YouTube using C#

Recently I spent some time investigating the Google Data .NET Client library. Specifically, I was interested in the YouTube Data API. What I wanted to do was programmatically upload a video file to my YouTube account. I ran into a couple of (minor) speed bumps along the way, and noticed there were a few things that weren't as clear as they should have been. Hopefully I can clarify the problems I encountered, in case future developers run into the same trip ups. Let's get started.

For the context of this post, I should explain my development environment. I am using the following:

  • Windows 7 Professional (32-bit)

  • Visual Studio 2008 Professional SP1

  • .NET 3.5 SP1

  • ASP.NET MVC 1.0

OK, so we're going to set up a very basic ASP.NET MVC web site that will basically do two things: 

  • Provide a link to use for authenticating a Google Account

  • Provide a form to direct upload a video (including metadata)

First things first: download the most up to date version of the Google Data .NET Client library and follow the instructions for installing and setting it up.

Next, ensure you have a YouTube developer API key attached to your YouTube account. If you have not done this yet, go here and associate a Developer ID with your account. Take note of this ID (it's pretty long).

Now, let's set up a new ASP.NET MVC project. We're going to use the Visual Studio defaults here, and just name our project "YouTubeUploader". 



Next, we need to add some references to the Google APIs. When you install the Google Data API, there should be a solution at All Programs -> Google Data API SDK -> Google Data API SDK.sln that the setup guide tells you to open and build. Once you have done this, you can select these binaries as a reference in your current project, which is what we do here.



Next, we're going to create a ViewModel to encapsulate all the inputs required to pass to our video uploader. This is going to be a very basic ViewModel, with nothing more than properties for retrieving inputs for our video. Here's what the code looks like:


  1. namespace YouTubeUploader.Models  
  2. {  
  3.     public class UploadViewModel  
  4.     {  
  5.         public string Title { getset; }  
  6.         public string Keywords { getset; }  
  7.         public string Description { getset; }  
  8.         public bool Private { getset; }  
  9.         public string VideoTags { getset; }  
  10.         public double Latitude { getset; }  
  11.         public double Longitude { getset; }  
  12.         public string Path { getset; }  
  13.         public string Type { getset; }  
  14.     }  
  15. }  

Like I said, very basic ViewModel here.

Next, we need to add a controller method to handle our Login logic. For simplicity sake, we're going to use the HomeController for all of our methods here. In your production situation, however, this logic might be split apart into different modules. We're just going for the basic "Hello, World" functionality here. In order to successfully make YouTube API calls (or any Google Data API, for that matter) you must retrieve an authenticated session token from the Google servers. This can be accomplished a number of different ways. Since we're trying to make a web site here, we're going to go with the AuthSub method of Google authentication. Here, we're going to provide a link to our user where they can go and authenticate themselves with the Google servers, send back a session token, and then finally re-direct the user back to a page of our choosing. This token is returned as part of the request query string, which we can handle in a number of different ways. For our purposes, we are going to use a string parameter on one of our controller methods to take the parameter and use it to create an authenticated session token in memory. The method will look like this: 
(NOTE: throughout the post, I reference "http://localhost:50555/" as my development server. I am just running my site through Visual Studio 2008 and am taking the default server address provided. This may vary in your environment, so please replace this address for what your environment requires.)
  1. public ActionResult Login()  
  2.        {  
  3.            Session["authSubUrl"] = AuthSubUtil.getRequestUrl("http://localhost:50555/Home/Upload""http://gdata.youtube.com"falsetrue);  
  4.   
  5.            return View();  
  6.        }  

What we're doing here is using a Google utility (AuthSubUtil.getRequestUrl) to generate the text for our link to provide to our users. getRequestUrl takes the following parameters:

  • continueUrl: Where the user will be redirected after authenticating. For our example, I used my local development server (http://localhost:50555/Home/Upload) since I want to pass my authenticated session token into my Upload GET method...more on that next.

  • scope: for YouTube API calls we use http://gdata.youtube.com

  • secure: If you have registered your app with Google with the appropriate security credentials, you can set this to true to ensure that your API requests do not show the "Warning: Access Consent" verbiage after authenticating. Also, some API calls are not allowed unless your app is registered. For our testing, we send in false.

  • session: Whether the authenticated token should persist over multiple API calls or just be a "one-time-only" shot. This becomes very clear when we actually create our YouTubeRequest object.

Next we add a view for our Login page. It's going to be a very generic view, with only one link on the page. Here is the whole view:


  1. <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>  
  2.     <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">  
  3.      Login  
  4.     </asp:Content>  
  5.     <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">  
  6.         <h2>Login</h2>  
  7.         <a href="<%= Session["authSubUrl"] %>">Click here to login</a>  
  8. </asp:Content>  

Notice how we are retrieving the URL text from Session["authSubUrl"], which we set in our Login() method. You could just as easily encapsulate this value into a ViewModel, however, I felt for the type of exercise we're performing here, this was sufficient.

Let's compile our project now and run our website. What you see when you navigate to http://localhost:/Home/Login is similar to the following:



The link brings us to a very familiar page to anyone with a Google account:



Once the user has entered their credentials, the following screen shows up:



This is the warning I mentioned previously about a secure application. If you secure your site with Google, the verbiage here (according to the documentation) is omitted. I have not yet secured a site with Google yet, so I have not experienced this difference.

After clicking on "Allow Access", we're presented with the following screen:



D'oh! We don't have an Upload view or controller method yet to handle this redirect! This is what we will create next. Take a look at the URL that Google navigated to post-login.http://localhost:50555/Home/Upload?token=CPvdxbuhGRDovLiXBw That looks awfully similar to what we specified in our Login() method, doesn't it? And you can see the authenticated token in the QueryString at the end of our URL. 

Next we add a controller method to handle GET requests to our Upload page. This is where we are going to handle binding our session token into a YouTubeRequestSettings object, and we'll use that to build a YouTubeRequest object, which is how we'll interact with the YouTube Data API. The method looks like this:
  1. public ActionResult Upload(string token)  
  2. {  
  3.     Session["token"] = AuthSubUtil.exchangeForSessionToken(token, null);  
  4.   
  5.     return View();  
  6. }  

Ok, what we're doing here is handling the QueryString token we get back from Google as a part of the GET request by making sure our method has a string parameter (which we call token). The method then uses a method on AuthSubUtil called exchangeForSessionToken which takes a string and an AsymmetricAlgorithm and returns a token good for an entire user session. This way we only have to authenticate the user once per session and they can make as many API calls as the system allows. Since we are not using a secured certificate for authentication we are leaving this as a null parameter. However, if you choose to use this functionality in a production environment I highly suggest taking a look at the documentation on registering your app with Google to take advantage of the heightened security. As this is a simple exercise, we are omitting this.

Next we add a strongly typed view (Create) for our Upload logic (UploadViewModel). We are going to choose "Create" template from the dropdown, and our view comes out like so:


  1. <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<YouTubeUploader.Models.UploadViewModel>" %>  
  2.     <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">  
  3.      Upload  
  4.     </asp:Content>  
  5.     <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">  
  6.         <h2>Upload</h2>  
  7.         <%= Html.ValidationSummary("Create was unsuccessful. Please correct the errors and try again.") %>  
  8.         <% using (Html.BeginForm()) {%>  
  9.             <fieldset>  
  10.                 <legend>Fields</legend>  
  11.                 <p>  
  12.                     <label for="Title">Title:</label>  
  13.                     <%= Html.TextBox("Title") %>  
  14.                     <%= Html.ValidationMessage("Title", "*") %>  
  15.                 </p>  
  16.                 <p>  
  17.                     <label for="Keywords">Keywords:</label>  
  18.                     <%= Html.TextBox("Keywords") %>  
  19.                     <%= Html.ValidationMessage("Keywords", "*") %>  
  20.                 </p>  
  21.                 <p>  
  22.                     <label for="Description">Description:</label>  
  23.                     <%= Html.TextBox("Description") %>  
  24.                     <%= Html.ValidationMessage("Description", "*") %>  
  25.                 </p>  
  26.                 <p>  
  27.                     <label for="Private">Private:</label>  
  28.                     <%= Html.TextBox("Private") %>  
  29.                     <%= Html.ValidationMessage("Private", "*") %>  
  30.                 </p>  
  31.                 <p>  
  32.                     <label for="VideoTags">VideoTags:</label>  
  33.                     <%= Html.TextBox("VideoTags") %>  
  34.                     <%= Html.ValidationMessage("VideoTags", "*") %>  
  35.                 </p>  
  36.                 <p>  
  37.                     <label for="Latitude">Latitude:</label>  
  38.                     <%= Html.TextBox("Latitude") %>  
  39.                     <%= Html.ValidationMessage("Latitude", "*") %>  
  40.                 </p>  
  41.                 <p>  
  42.                     <label for="Longitude">Longitude:</label>  
  43.                     <%= Html.TextBox("Longitude") %>  
  44.                     <%= Html.ValidationMessage("Longitude", "*") %>  
  45.                 </p>  
  46.                 <p>  
  47.                     <label for="Path">Path:</label>  
  48.                     <%= Html.TextBox("Path") %>  
  49.                     <%= Html.ValidationMessage("Path", "*") %>  
  50.                 </p>  
  51.                 <p>  
  52.                     <label for="Type">Type:</label>  
  53.                     <%= Html.TextBox("Type") %>  
  54.                     <%= Html.ValidationMessage("Type", "*") %>  
  55.                 </p>  
  56.                 <p>  
  57.                     <input type="submit" value="Create" />  
  58.                 </p>  
  59.             </fieldset>  
  60.         <% } %>  
  61.  <div>  
  62.   <%=Html.ActionLink("Back to List", "Index") %>  
  63.  </div>  
  64.     </asp:Content>  

This is a very limited view. What we are doing is adding a field for every property on our UpdateViewModel. This allows the user to specify what kind of video they want to upload.

Next we add a controller method to handle the POST request for our Upload page (i.e. what happens when we click "Create"). This is where the bulk of our logic will reside. Here's what the code looks like:
  1. [AcceptVerbs(HttpVerbs.Post)]  
  2. public ActionResult Upload(UploadViewModel uploadViewModel)  
  3. {  
  4.     const string developerKey = "THIS_IS_WHERE_YOUR_REALLY_LONG_DEVELOPER_API_KEY_GOES";  
  5.     const string applicationName = "THIS_IS_WHERE_YOUR_APP_NAME_GOES";              
  6.   
  7.     _settings = new YouTubeRequestSettings(applicationName, "ThisCanSeriouslyBeAnyString_It'sBeenDeprecated", developerKey, (string) Session["token"]);  
  8.     _request = new YouTubeRequest(_settings);  
  9.   
  10.     var newVideo = new Video();  
  11.   
  12.     newVideo.Title = uploadViewModel.Title;  
  13.     newVideo.Keywords = uploadViewModel.Keywords;  
  14.     newVideo.Description = uploadViewModel.Description;  
  15.     newVideo.YouTubeEntry.Private = uploadViewModel.Private;  
  16.   
  17.     newVideo.YouTubeEntry.Location = new GeoRssWhere(uploadViewModel.Latitude, uploadViewModel.Longitude);  
  18.   
  19. wVideo.Tags.Add(new MediaCategory(uploadViewModel.VideoTags, YouTubeNameTable.DeveloperTagSchema));  
  20.   
  21.     newVideo.YouTubeEntry.MediaSource = new MediaFileSource(uploadViewModel.Path, uploadViewModel.Type);  
  22.     var createdVideo = _request.Upload(newVideo);  
  23.   
  24.     return View();  
  25. }  

Ok, so it was this method where I ran into the gotcha's that prompted me to write this post in the first place. Once again, in a production environment, you will probably have the developerKey and applicationName stored in some kind of configuration file / object or a database. For our example, we're just setting some hard-coded strings inside our method. These are used to create our YouTubeRequestSettings object. As you can see, the method takes 4 parameters, and this is the method call that was a pain to debug. The 4 parameters are:

  • applicationName: The name of our application, as specified in our YouTube Account screen, to the left of our developer api key.

  • client: If you look on your YouTube account screen (as of February 24th, 2010) you'll notice thereis not a client id on your screen. In fact, there is verbiage stating that they are no long required.Use any string you want here. Anything. I used "ThisIsMyRidiculouslyLongClientIdStringThatWillWorkJustBecause" and that is fine. It can be anything. I don't know why this hasn't been deprecated yet, but hopefully in the future it does to reduce confusion.

  • developerKey: This is your developer key from your YouTube account page. It's really long, so be sure when you copy / paste it in that you grabbed everything.

  • authSubToken: This is the string version of the AuthSub session token we created in our Login() method.

Once you understand the functionality in setting up your YouTubeRequestSettings object the rest is a walk in the park. The YouTubeRequest object itself takes a YouTubeRequestSettings object as a parameter, so you just new() up one of those with the YouTubeRequestSettings object we just created. Then, we create a new Video() object and set the properties on it equal to the values in our UploadViewModel. This is an ideal situation for AutoMapper in that all we're doing is basically mapping properties from one object to another. However, for this example we are just going to set them explicity ourselves. Then we create a new MediaFileSource object as a property on our Video object. Be sure to escape '\' in your path, if you are using a local path (i.e. instead of C:\MyCode\Project you need C:\\MyCode\\Project). Also, for the Type property, you need the MIME type of the video you are uploading. For example, for Windows Media Video files, you want to use "video/x-ms-wmv" as your type.

And that's it! Let's run the web site now and see our results.

To make this more robust (and actually usable) you'll want to provide some kind of feedback mechanism to notify your user whether the upload failed or was successful. For this example I chose to just prove how to upload the files. 

I hope this eases someone's pain and eliminates the 45 minutes - 1 hour I lost trying to figure out why my API calls weren't being correctly authenticated. Take some time and experiment with the rest of the APIs, which allows you to do pretty much anything you can do on the web site.

No comments:

Post a Comment

What should you required to learn machine learning

  To learn machine learning, you will need to acquire a combination of technical skills and domain knowledge. Here are some of the things yo...