O'Reilly logo

Coding4Fun by Dan Fernandez, Brian Peek

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. InnerTube: Download, Convert, and Sync YouTube Videos

AUTHOR: Dan Fernandez

DIFFICULTY: Advanced

TIME REQUIRED: 20+ hours

COST: Free

SOFTWARE: Visual C# 2008 Express Edition with Service Pack 1, Visual Basic 2008 Express Edition with Service Pack 1, iTunes version 7.0 or higher

HARDWARE: Optional iPod, iPhone, or Zune device for syncing

DOWNLOAD: http://www.codeplex.com/InnerTube/ and http://www.c4fbook.com/InnerTube

OK, I’ll admit it. I love YouTube. It’s the only site on the Web where you can be endlessly entertained with everything from classic movie clips from Pulp Fiction or The Big Lebowski to Internet celebrities like Tron Guy, Kimbo Slice, or Tay Zonday (aka Mr. Chocolate Rain).

In short, YouTube has it all, except for portability, as you can watch YouTube videos only if you have an Internet connection. The goal of InnerTube is to work around that pesky limitation by enabling you to download videos automatically from YouTube, convert them to a more user-friendly file format, and even synchronize your downloaded videos with an iPod or Zune device.

Overview

To see InnerTube in action, let’s take a quick tour of some of its key features.

InnerTube’s Main UI

As you can see in Figure 4-1, InnerTube’s main UI uses a three-pane design similar to Outlook, with the left pane holding the list of video feeds, the center pane listing the videos from the selected feed, and the rightmost pane showing a detailed view of the selected video.

A screenshot of InnerTube
Figure 4-1. A screenshot of InnerTube

Running InnerTube for the First Time

When InnerTube first runs, it detects if you have iTunes and/or the Zune client software installed and automatically suggests where your downloaded videos will be stored. If iTunes and Zune are not detected, the option to sync to them will be disabled (see Figure 4-2).

InnerTube configuring itself for your PC
Figure 4-2. InnerTube configuring itself for your PC

Adding Videos

Clicking Add Videos from the main menu enables you to easily add different types of YouTube feeds (see Figure 4-3). When you add a feed you can also set properties, such as which user’s favorites to download or a time interval for the Top Rated Over Time videos.

Adding feeds to InnerTube
Figure 4-3. Adding feeds to InnerTube

How InnerTube Works

Now that we’ve seen what InnerTube does, we’ll go through the key parts of how InnerTube is built, including the following:

  • How YouTube’s API works

  • InnerTube’s classes for videos and feeds

  • Converting XML from YouTube’s API into InnerTube objects

  • How to download videos from YouTube

  • How to convert the media format for YouTube videos

  • A quick overview of how InnerTube’s background processing works

  • How to sync to iTunes and Zune

  • Building the InnerTube application

  • Building the InnerTube WPF user interface

How YouTube’s API Works

YouTube’s API is a REST service that uses Google’s Gdata format to enable you to perform operations such as searching for and retrieving information about videos as well as uploading videos to YouTube.

Unlike other services, such as those from Live.com or Amazon.com, you do not need to sign up for a developer account or an API key to read data from YouTube’s API. Figure 4-4 shows the results from retrieving the top rated videos of all time on YouTube using Firefox.

Note

For full documentation on what services are available from YouTube, go to http://code.google.com/apis/youtube/developers_guide_protocol.html.

The raw XML from YouTube’s API
Figure 4-4. The raw XML from YouTube’s API

Standard YouTube Feeds

If you look closely at the URL in Figure 4-4’s image, you’ll notice that the Top Rated video feed is a “Standard Feed.” YouTube defines a number of standard feeds, including:

You can further restrict the results from standard feeds by appending a time parameter to the query string. For example, this parameter will only retrieve the most viewed videos this week:

http://gdata.youtube.com/feeds/api/standardfeeds/most_viewed?time=this_week

The time parameter can have the following values: today, this_week, this_month, and all_time. The default value is all_time if no parameter is provided.

User-Specific YouTube Feeds

By default, YouTube makes user data (such as your favorite videos or video subscriptions) public and available via its API. To get a user’s favorites or subscriptions, just replace the username in the following URLs with a valid YouTube username. Unlike the generic YouTube feeds, you cannot add a time parameter to filter user-specific feeds by a time interval.

YouTube also provides a search API that can be called by appending vq=searchterm. For example, the search URL here will search YouTube for all Microsoft videos:

http://gdata.youtube.com/feeds/api/videos?vq=Microsoft

Optional Query String Parameters

InnerTube’s YouTube wrapper provides a simple implementation for its search, but the YouTube API adds additional search features, such as paging or ordering by appending query string values. The following is a sampling of query string parameters:

orderby

Specify how you want videos sorted—say, by relevance, rating, published date, or number of views. By default, this is set to relevance.

max-results

Specify the maximum number of results. By default, this is set to 25.

start-index

Enable paging through data by setting the start-index to the point from which you want the result set to start. If you set the start-index to 100, the API would retrieve the 100–125th search results. By default, this is set to 0.

Note

For a full list of parameters and options, see the YouTube Query Parameter Definitions at http://code.google.com/apis/youtube/reference.html#Query_parameter_definitions.

Other Important YouTube URLs

YouTube uses a number of other, well-defined URLs that we’ll need to know for the purposes of building our application. Here are quick explanations for each:

Watch

This is the link you commonly see when linking to a video: http://www.youtube.com/watch?v=VideoID

Embed

This is the link used for embedding a video on another site, such as MySpace: http://www.youtube.com/v/VideoID

Thumbnail

This is the link to the largest image thumbnail available for a video (425×344): http://img.youtube.com/vi/VideoID/0.jpg

Download

This is the (hidden) link that we’ll use to download a video: http://www.youtube.com/get_video?video_id=VideoID&t=sessiontoken

Note

The Download link to download a video will work only if you append a valid, time-based session token. This is done to try to prevent end users from downloading YouTube videos directly. We’ll show how you can work around this restriction in the Downloading Videos from YouTube section, later in this chapter.

Building Classes for YouTube Feeds and Videos

Now that you’ve seen how the YouTube API works, we will show how you can represent the data types from the YouTube API as .NET classes (see Figure 4-5).

InnerTubeTime

This is an enumeration that represents the possible time parameter values we can pass in to YouTube. We’ll use this in the InnerTubeService class as an optional parameter to pass into YouTube’s API.

InnerTubeFeed

This class represents a specific video feed, such as the Top Rated videos. The Feed Name is a user-friendly name describing the feed, and the URL points to a YouTube API like the ones listed earlier.

InnerTubeVideo

This class represents a single YouTube video and includes most of the fields we get from YouTube (author, comments, categories), as well as a set of download properties that point to the location indicating where that video is stored on disk (once we download it).

YouTube’s API as .NET classes
Figure 4-5. YouTube’s API as .NET classes

Calling the InnerTubeService Class

Now that we have our classes designed, let’s show how we can convert the XML from Figure 4-4 into the objects represented in Figure 4-5. To do this, we’ll use another class, our InnerTubeService class. As you can see in Figure 4-6, the common feeds—Top Rated, Most Viewed, etc.—from YouTube’s API are all available as methods. The return type for all of the methods in InnerTubeService is an ObservableCollection<Inner TubeVideo>.

IntelliSense showing the available methods for retrieving YouTube data
Figure 4-6. IntelliSense showing the available methods for retrieving YouTube data

Converting YouTube XML into .NET Objects

The InnerTubeService class actually has only one real method that does the work of converting XML into objects, and that’s the ConvertYouTubeXmlIntoObjects method. The reason for this is that each YouTube XML API call has an identical structure, whether it is for search results or top-rated videos, so we can use the same code to parse the XML but still maintain some “convenience” methods, such as Search(string query), to make it easier to reuse the library.

As you can see in Example 4-1 and Example 4-2, the ConvertYouTubeXmlToObjects method takes two parameters, the Uri for a valid YouTube API, and a Setting object, which we must pass in for configuring the directories on your PC that we will save video files (thumbnail, video, etc.). We will cover the Setting class in depth later, in the Application Settings section.

Example 4-1.  C# code for ConvertYouTubeXmlToObjects method signature
public static ObservableCollection<InnerTubeVideo> ConvertYouTubeXmlToObjects(
  Uri youTubeUrl, Setting setting)
Example 4-2.  Visual Basic code for ConvertYouTubeXmlToObjects method signature
Public Shared Function ConvertYouTubeXmlToObjects(ByVal youTubeUrl As Uri, _
  ByVal setting As Setting) As ObservableCollection(Of InnerTubeVideo)

Inside the ConvertYouTubeXmlToObjects method, we declare a number of XNamespace variables, which are required because the XML we receive back from YouTube makes heavy use of XML namespaces (see Example 4-3 and Example 4-4).

Example 4-3.  C# code to declare XML namespaces
XNamespace nsBase = @"http://www.w3.org/2005/Atom";
XNamespace nsGData = @"http://schemas.google.com/g/2005";
XNamespace nsYouTube = @"http://gdata.youtube.com/schemas/2007";
Example 4-4.  Visual Basic code to declare XML namespaces
Dim nsBase As XNamespace = "http://www.w3.org/2005/Atom"
Dim nsGData As XNamespace = "http://schemas.google.com/g/2005"
Dim nsYouTube As XNamespace = "http://gdata.youtube.com/schemas/2007"

To get the actual data “over-the-wire,” we’ll call the YouTube service using the WebClient class in the System.Net namespace by passing in the URL for the service and calling the OpenRead() method with YouTube’s schema URL. This is equivalent to how we previously opened our browser to peruse the XML from Figure 4-5. We’ll then use the XDocument class to load the XML with the XmlTextReader as shown in Example 4-5 and Example 4-6.

Example 4-5.  C# code to make a web request to YouTube’s API
//Use to call service
WebClient wc = new WebClient();

//Get Data
XmlTextReader xr = new XmlTextReader(wc.OpenRead(youTubeUrl));
XDocument rawData = XDocument.Load(xr);
Example 4-6.  Visual Basic code to make a web request to YouTube API
'Use to call service
Dim wc As New WebClient()

'Get Data
Dim xr As New XmlTextReader(wc.OpenRead(youTubeUrl))
Dim rawData As XDocument = XDocument.Load(xr)

At this point, we have the raw XML results with a listing of videos in memory, with each <entry> element (a YouTube video) being an XElement object. Next, we want to use LINQ for XML to loop through every entry element and create a new InnerTube Video object, setting properties such as the video author, categories, title, and more, as shown in Example 4-7 and Example 4-8.

In the first line of Example 4-7 and Example 4-8, you’ll notice that we pass in the nsBase + "entry" as a parameter to the Descendents method. This is required because the entry element is in the nsBase namespace, and without this namespace, your LINQ query would throw an exception saying the element attribute does not exist. Also note that we are not concatenating strings, but instead using the overloaded + operator for the XNamespace class. This is why the Visual Basic code uses the + operator instead of the concatenation operator (&).

Example 4-7.  C# code to loop through entry elements and assign properties
var query = from entry in rawData.Descendants(nsBase + "entry")
  select new InnerTubeVideo
  {
    Author = entry.Element(nsBase + "author").Element(nsBase + "name").Value,
    Categories = ParseCategories(entry.Elements(nsBase + "category")),
    Id = ParseID(entry.Element(nsBase + "id").Value),
    Published = DateTime.Parse(entry.Element(nsBase + "published").Value),
    Updated = DateTime.Parse(entry.Element(nsBase + "updated").Value),
    Title = entry.Element(nsBase + "title").Value,
    Description = entry.Element(nsBase + "content").Value,
...
Example 4-8.  Visual Basic code to loop through entry elements and assign properties
Dim query = From entry In rawData.Descendants(nsBase + "entry") _
  Select New InnerTubeVideo With _
  { _
    .Author = entry.Element(nsBase + "author").Element(nsBase + "name").Value, _
    .Categories = ParseCategories(entry.Elements(nsBase + "category")), _
    .Id = ParseID(entry.Element(nsBase + "id").Value), _
    .Published = DateTime.Parse(entry.Element(nsBase + "published").Value), _
    .Updated = DateTime.Parse(entry.Element(nsBase + "updated").Value), _
    .Title = entry.Element(nsBase + "title").Value, _
    .Description = entry.Element(nsBase + "content").Value, _
...

As mentioned previously, some fields that we want to retrieve are in different namespaces, so we have to, in code, swap out the namespace to read that field. To show exactly what this means, Example 4-9 contains the raw XML received from YouTube’s API for the total number of views for a video.

Example 4-9. Snippet of XML for the total number of views for a YouTube video
<yt:statistics viewCount="130534" favoriteCount="1210" />

Since the viewCount attribute lives in the <yt> (YouTube) XML namespace, we must explicitly add the nsYouTube namespace variable to get the value. In Example 4-10 and Example 4-11, we start at the root entry element and get the statistics element, and then pull the viewCount attribute.

Example 4-10.  C# code to parse the viewCount attribute from XML
Views = int.Parse(entry.Element(nsYouTube +
        "statistics").Attribute("viewCount").Value),
Example 4-11.  Visual Basic code to parse the viewCount attribute from XML
.Views = Integer.Parse(entry.Element(nsYouTube + _
         "statistics").Attribute("viewCount").Value), _

To keep the parsing relatively compact, one nice thing you can do with LINQ for XML is pass in a chunk of XML to a function. For example, Johnny Lee’s head-tracking video (http://www.youtube.com/watch?v=cI1AwZN4ZYg) includes 15 different YouTube video categories, like the listing in Example 4-12.

Example 4-12. Snippet of XML for a YouTube video’s categories
<category scheme="..." term="wiimote" />
  <category scheme="..." term="display" />
  <category scheme="..." term="3D" />
  <category scheme="..." term="reality" />
  <category scheme="..." term="nintendo"/>
...

What we want to do is convert the term attributes in the <category> elements from Example 4-12 into a nice Collection<string> variable, with each term stored as a string. To do this, we’ll call ParseCategories, as shown in Example 4-13 and Example 4-14.

Example 4-13.  C# code to send XML to the ParseCategories method
Categories = ParseCategories(entry.Elements(nsBase + "category")),
Example 4-14.  Visual Basic code to send XML to the ParseCategories method
.Categories = ParseCategories(entry.Elements(nsBase + "category")), _

ParseCategories receives an IEnumerable<Xelement>, which will be formatted exactly like the <category> elements shown in Example 4-12. We can then use LINQ to pull the value of the <term> attribute from each category and return it all as a Collection<string> class, as shown in Example 4-15 and Example 4-16.

Example 4-15.  C# code to parse the value of the term attributes
private static Collection<string> ParseCategories(IEnumerable<XElement> Categories)
{
  var vals = from c in Categories.Attributes("term")
    select c.Value;
  return (Collection<string>)vals;
}
Example 4-16.  Visual Basic code to parse the value of the term attributes
Private Shared Function ParseCategories(ByVal Categories As IEnumerable(Of XElement)) As Collection(Of String)
  Dim vals = From c In Categories.Attributes("term") _
  Select c.Value
  Return vals.ToCollection()
End Function

Example 4-17 and Example 4-18 show the ConvertYouTubeXmlToObjects method in its entirety.

Example 4-17.  C# code for the ConvertYouTubeXmlToObjects method
public static ObservableCollection<InnerTubeVideo> ConvertYouTubeXmlToObjects(
  Uri youTubeUrl, Setting setting)
{
  XNamespace nsBase = @"http://www.w3.org/2005/Atom";
  XNamespace nsGData = @"http://schemas.google.com/g/2005";
  XNamespace nsYouTube = @"http://gdata.youtube.com/schemas/2007";

  //Use to call service
  WebClient wc = new WebClient();

  //Get Data
  XmlTextReader xr = new XmlTextReader(wc.OpenRead(youTubeUrl));
  XDocument rawData = XDocument.Load(xr);
  var query = from entry in rawData.Descendants(nsBase + "entry")
  select new InnerTubeVideo
  {
    Author = entry.Element(nsBase + "author").Element(nsBase + "name").Value,
    Categories = ParseCategories(entry.Elements(nsBase + "category")),
    Id = ParseID(entry.Element(nsBase + "id").Value),
    Published = DateTime.Parse(entry.Element(nsBase + "published").Value),
    Updated = DateTime.Parse(entry.Element(nsBase + "updated").Value),
    Title = entry.Element(nsBase + "title").Value,
    Description = entry.Element(nsBase + "content").Value,
    ThumbnailLink = _BaseThumbnailUrl + ParseID(entry.Element(nsBase + "id").Value) +
      @"/0.jpg",
    Link = _BasewatchUrl + ParseID(entry.Element(nsBase + "id").Value),
    EmbedLink = _baseEmbedUrl + ParseID(entry.Element(nsBase + "id").Value),
    DownloadLink = _BaseDownloadUrl + ParseID(entry.Element(nsBase + "id").Value),
    Views = int.Parse(entry.Element(nsYouTube +
            "statistics").Attribute("viewCount").Value),
    AvgRating = float.Parse(entry.Element(nsGData +
                "rating").Attribute("average").Value),
    NumRaters = int.Parse(entry.Element(nsGData +
                "rating").Attribute("numRaters").Value),

    //set download locations
    DownloadedImage = FileHelper.BuildFileName(setting.SubPath,
      ParseID(entry.Element(nsBase + "id").Value), FileType.Image),
    DownloadedFlv = FileHelper.BuildFileName(setting.SubPath,
      entry.Element(nsBase + "title").Value, FileType.Flv),
    DownloadedMp4 = FileHelper.BuildFileName(setting.VideoPath,
      entry.Element(nsBase + "title").Value, FileType.Mp4),
    DownloadedWmv = FileHelper.BuildFileName(setting.VideoPath,
     entry.Element(nsBase + "title").Value, FileType.Wmv)
  };

  return query.ToObservableCollection<InnerTubeVideo>();
}
Example 4-18.  Visual Basic code for the ConvertYouTubeXmlToObjects method
Public Shared Function ConvertYouTubeXmlToObjects(ByVal youTubeUrl As Uri, _
  ByVal setting  As Setting) As ObservableCollection(Of InnerTubeVideo)

  Dim nsBase As XNamespace = "http://www.w3.org/2005/Atom"
  Dim nsGData As XNamespace = "http://schemas.google.com/g/2005"
  Dim nsYouTube As XNamespace = "http://gdata.youtube.com/schemas/2007"

  'Use to call service
  Dim wc As New WebClient()

  'Get Data
  Dim xr As New XmlTextReader(wc.OpenRead(youTubeUrl))
  Dim rawData As XDocument = XDocument.Load(xr)
  Dim query = From entry In rawData.Descendants(nsBase + "entry") _

  Select New InnerTubeVideo With _
  { _
    .Author = entry.Element(nsBase + "author").Element(nsBase + "name").Value, _
    .Categories = ParseCategories(entry.Elements(nsBase + "category")), _
    .Id = ParseID(entry.Element(nsBase + "id").Value), _
    .Published = DateTime.Parse(entry.Element(nsBase + "published").Value), _
    .Updated = DateTime.Parse(entry.Element(nsBase + "updated").Value), _
    .Title = entry.Element(nsBase + "title").Value, _
    .Description = entry.Element(nsBase + "content").Value, _
    .ThumbnailLink = _BaseThumbnailUrl & ParseID(entry.Element(nsBase + _
      "id").Value)& "/0.jpg", _
    .Link = _BasewatchUrl & ParseID(entry.Element(nsBase + "id").Value), _
    .EmbedLink = _baseEmbedUrl & ParseID(entry.Element(nsBase + "id").Value), _
    .DownloadLink = _BaseDownloadUrl & ParseID(entry.Element(nsBase + "id").Value), _
    .Views = Integer.Parse(entry.Element(nsYouTube + _
      "statistics").Attribute("viewCount").Value), _
    .AvgRating = Single.Parse(entry.Element(nsGData + _
      "rating").Attribute("average").Value), _
    .NumRaters = Integer.Parse(entry.Element(nsGData + _
      "rating").Attribute("numRaters").Value), _
    .DownloadedImage = FileHelper.BuildFileName(setting.SubPath, _
      ParseID(entry.Element(nsBase + "id").Value), FileType.Image), _
    .DownloadedFlv = FileHelper.BuildFileName(setting.SubPath,
      entry.Element(nsBase + "title").Value, FileType.Flv), _
    .DownloadedMp4 = FileHelper.BuildFileName(setting.VideoPath, _
      entry.Element(nsBase + "title").Value, FileType.Mp4), _
    .DownloadedWmv = FileHelper.BuildFileName(setting.VideoPath, _
      entry.Element(nsBase + "title").Value, FileType.Wmv) _
  }

  Return query.ToObservableCollection()

End Function

Now that we know how to retrieve information about YouTube videos, let’s see how we can use this data to programmatically download a YouTube video.

Downloading Videos from YouTube

Now that we’ve seen how YouTube’s API works, we’ll now show how to download YouTube videos, first manually with your browser and then programmatically with code.

Downloading a YouTube Video with Your Browser

YouTube videos are stored in the Flash video (FLV) format, which is what we’ll want to download.

Before we show how you can programmatically download a YouTube video, let’s look at how you can download a video just by using your browser. To begin with, open any YouTube video page—say, the infamous Tay Zonday “Chocolate Rain” video, which is at http://www.youtube.com/watch?v=EwTZ2xpQwpA. From this URL, we can determine that the video ID (v=) is “EwTZ2xpQwpA”.

YouTube Blocks You (by Design)

YouTube has a (hidden) URL that you can use to directly download a video, but you need to pass in two things, a video_id and a session token. A session token is an identifier that YouTube assigns to a browser that lasts for approximately 15 minutes. If you don’t have a valid session token, YouTube will block your request to download a Flash video.

To see this in action, open your browser, go to http://www.youtube.com/get_video?video_id=EwTZ2xpQwpA, and notice how YouTube pretends the URL is a bad URL by sending back an HTTP 404 Not Found error to Internet Explorer browsers (Firefox 3 users will see a blank page), as shown in Figure 4-7. You get a 404 error because you must append a valid session token to the URL for the request to work.

YouTube will return a 404 error in Internet Explorer unless you provide a session token
Figure 4-7. YouTube will return a 404 error in Internet Explorer unless you provide a session token

Getting a Session Token from JavaScript

To get a valid session token from YouTube, we will have to open a browser to the YouTube page for “Chocolate Rain” at: http://www.youtube.com/watch?v=EwTZ2xpQwpA and click View Source to view the page’s HTML contents. In the HTML, you’ll find a JavaScript variable named swfArgs that YouTube uses to pass in a number of parameters to the Flash player as shown in Example 4-19.

Example 4-19. The swfArgs Javascript variable needed to play a YouTube video
var swfArgs = {"usef": 0, "cust_p": "jMWn75PwKgutJQ0J3mrLbA", "iv_storage_
server": "http://www.google.com/reviews/y/", "ad_module": "http://s.ytimg.com/yt/
swf/ad-vfl59966.swf", "ad_channel_code": "invideo_overlay_480x70_cat10,afv_overlay",
"video_id": "EwTZ2xpQwpA", "l": 292, "fmt_map": "34/0/9/0/115", "ad_host":
"ca-host-pub-5311789755034317", "sk": "_N53QD2G0B79IwT2MIi7nNSvpkgWSWWwC",
"invideo": true, "t": "OEgsToPDskJUt8xv3hrKiGOAYlLYcl1L", "hl": "en", "plid":
"AARaIAIC2yUqgjl1AAAA-YT8YQA", "vq": null, "iv_module": "http://s.ytimg.com/yt/swf/
iv_module-vfl57703.swf", "afv": true, "ad_host_tier": "12789", "ad_video_pub_id":
"ca-pub-6219811747049371", "tk": "5Eu9v6C5n2lxivxOt0UqRqT0yNnUmUCs3oR7ZzuDg8_
JZHd6DyO2jw=="};

Houston, We Have a Token!

Buried deep inside the swfArgs variable, is the name/value pair "t": "OEgsTo PDskJUt8xv3hrKiGOAYlLYcl1L", which represents a valid session token that can be used to download the video. Now try the same download URL for which YouTube previously returned a 404 error, except this time we’ll append the "t" session token value as shown below:

http://www.youtube.com/get_video?video_id=EwTZ2xpQwpA&t=OEgsToPDskJUt8xv3hrKiGOAYlLYcl1L

Note

The exact download URL will not work because, by the time you read this, the session token (t= part) would have expired. If you want to try this in your browser, you will first have to navigate to a YouTube video page, click view source in your browser and manually copy/paste the session token into the download URL. If you try to use a session token after the session has expired, you will receive a HTTP 410 Gone Error saying the page does not exist. Session tokens are also unique to a video such that you cannot use the same session token to download a different video.

You should now be prompted to save the 11.3 MB FLV file to your hard drive as shown in Figure 4-8. Let’s now see how we can do the same thing using code.

Prompt to save a YouTube video to your PC
Figure 4-8. Prompt to save a YouTube video to your PC

Getting a Token Programmatically

Now that we’ve shown how you can do it in a browser, we want to replicate the process of getting a token in code by reading the HTML from the video page, pulling the swfArgs Javascript parameter, and then calling the download link with a valid session token.

Note

HTML parsing like this is brittle and subject to breaking when YouTube changes the way they do session tokens or if they rename their Javascript variables. Given that there is no API to download videos, developers building applications that depend on HTML internals of a website need to continually test and make sure a change to the site doesn’t cause an application to break.

To do this in code, we’ve created a method in the Download class called CreateTokenRequest. When given an InnerTubeVideo, it makes a WebClient request to download the video page HTML as a string. It then parses out the swfArgs Javascript variable and retrieves the "t:" argument, just like we did manually with the browser.

The code in Example 4-20 and Example 4-21 retrieves the HTML from the web page using the WebClient DownloadString method passing in the Link (the URL for the YouTube video). Next, it gets the index of the swfArgs variable (see Example 4-19) in the HTML string. Once we find the location of swfArgs, we can retrieve the variable’s value by finding the index of the bracket characters “{” and “}” which contain the value. Once we get the index of both of those characters, we then use the SubString method to retrieve the value in between the brackets and load that into the variable fullString.

Example 4-20.  C# code to programmatically get a session token
private static string CreateTokenRequest(InnerTubeVideo video)
{
  //YouTube variables
  const string jsVariable = "swfArgs";
  const string argName = "t";

  //get raw html from YouTube video page
  string rawHtml;
  using (WebClient wc = new WebClient())
  {
    rawHtml = wc.DownloadString(video.Link);
  }

  //extract the Javascript name/value pairs
  int jsIndex = rawHtml.IndexOf(jsVariable);
  int startIndex = rawHtml.IndexOf("{", jsIndex);
  int endIndex = rawHtml.IndexOf("}", startIndex);
  string fullString = rawHtml.Substring(startIndex + 1, endIndex - startIndex - 1);

  //remove all quotes (")
  fullString = fullString.Replace("\"","");

  //split all values
  string[] allArgs = fullString.Split(',');

  //loop through javascript parameters
  foreach (string swfArg in allArgs)
  {
    if (swfArg.Trim().StartsWith(argName))
    {
      var nameValuePair = swfArg.Split(':');
      return string.Format("{0}={1}", argName, nameValuePair[1].Trim());
    }
  }
  throw new Exception(string.Format("token not found in swfArgs," +
    " make sure {0} is accessible", video.Link));
}
Example 4-21.  Visual Basic code to programmatically get a session token
Private Shared Function CreateTokenRequest(ByVal video As InnerTubeVideo) As String
  'YouTube variables
  Const jsVariable As String = "swfArgs"
  Const argName As String = "t"

  'get raw html from YouTube video page
  Dim rawHtml As String
  Using wc As New WebClient()
    rawHtml = wc.DownloadString(video.Link)
  End Using

  'extract the JavaScript name/value pairs
  Dim jsIndex As Integer = rawHtml.IndexOf(jsVariable)
  Dim startIndex As Integer = rawHtml.IndexOf("{", jsIndex)
  Dim endIndex As Integer = rawHtml.IndexOf("}", startIndex)
  Dim fullString As String = rawHtml.Substring(startIndex + 1, _
    endIndex - startIndex - 1)

  'remove all quotes (")
  fullString = fullString.Replace("""", "")

  'split all values
  Dim allArgs() As String = fullString.Split(","c)

  'loop through javascript parameters
  For Each swfArg As String In allArgs
    If swfArg.Trim().StartsWith(argName) Then
      Dim nameValuePair = swfArg.Split(":"c)
      Return String.Format("{0}={1}", argName, nameValuePair(1).Trim())
    End If
  Next swfArg
  Throw New Exception(String.Format("token not found in swfArgs, " & _
    "make sure {0} is accessible", video.Link))
End Function

Once the fullString variable holds the value of swfArgs, we’ll need to do a little bit more work to parse the session token. To do that, we’ll remove quotes from the string and split the contents of the variable (each argument is comma delimited). Finally, we’ll loop through each argument looking for the "t" argument (the session token). Once found, we return a string in the format "Name=Value", which, for this video, would return the following:

t=OEgsToPDskJUt8xv3hrKiGOAYlLYcl1L

Downloading the Video

Now that we’ve solved how to get the session token programmatically, we now need to download the file to our hard drive. To do this, we will use the DownloadVideo method of the Download class which is included in its entirety below in Example 4-22 and Example 4-23.

The first thing our code does is make sure we haven’t downloaded the Flash video (FLV) before by checking if the file already exists.

Next, we build the download URL which, like in our browser example, is a combination of the download URL, the video ID, and the session token that we get from calling the CreateTokenRequest from above.

We open a request to the file stream of the YouTube Flash video file on YouTube’s server using the download URL we previously built. At the same time, we also create a FileStream on our hard drive that we’ll use to save the video file. The code will read 65K of data from the web stream of the video and write those bytes directly to the hard drive file stream. The code continues looping, reading 65K from the Web and writing it to the hard drive until we reach the end of the file, at which point calling ReadBytes from the web stream will return 0 and we exit the loop.

Note

Both the WebStream and the FileStream are wrapped in using statements that automatically handle the proper disposal of these resources in an automatically generated try/finally block. We do this to ensure that even if an exception is thrown, we will still clean up system resources properly.

Example 4-22.  C# code for the DownloadVideo method
public static void DownloadVideo(InnerTubeVideo source, string destination)
{
  if (!File.Exists(destination))
  {
    UriBuilder final = new UriBuilder(source.DownloadLink);
    final.Query = "video_id=" + source.Id + "&" + CreateTokenRequest(source);

    WebRequest request = WebRequest.Create(final.ToString());
    request.Timeout = 500000;

    try
    {
      WebResponse response = request.GetResponse();

      using (Stream webStream = response.GetResponseStream())
      {
        try
        {
          int _bufferSize = 65536;

          using (FileStream fs = File.Create(destination, _bufferSize))
          {
            int readBytes = −1;
            byte[] inBuffer = new byte[_bufferSize];

            //Loop until we hit the end
            while (readBytes != 0)
            {
              //read data from web into filebuffer, then write to file
              readBytes = webStream.Read(inBuffer, 0, _bufferSize);
              fs.Write(inBuffer, 0, readBytes);
            }
          }
        }
        catch (Exception ex)
        {
          Debug.WriteLine("Error in Buffer Download");
          Debug.Indent();
          Debug.WriteLine(ex.Message);
        }
      }
    }
    catch (Exception ex)
    {
      Debug.WriteLine("Error in request.GetResponse()");
      Debug.Indent();
      Debug.WriteLine(ex.Message);
    }
  }
}
Example 4-23.  Visual Basic code for the DownloadVideo method
Public Shared Sub DownloadVideo(ByVal source As InnerTubeVideo, ByVal destination As String)
  If (Not File.Exists(destination)) Then
    Dim final As New UriBuilder(source.DownloadLink)
    final.Query = "video_id=" & source.Id & "&" & CreateTokenRequest(source)

    Dim request As WebRequest = WebRequest.Create(final.ToString())
    request.Timeout = 500000

    Try
      Dim response As WebResponse = request.GetResponse()

      Using webStream As Stream = response.GetResponseStream()
        Try
          Dim _bufferSize As Integer = 65536

          Using fs As FileStream = File.Create(destination, _bufferSize)
            Dim readBytes As Integer = −1
            Dim inBuffer(_bufferSize - 1) As Byte

            'Loop until we hit the end
            Do While readBytes <> 0
              'read data from web into filebuffer, then write to file
              readBytes = webStream.Read(inBuffer, 0, _bufferSize)
              fs.Write(inBuffer, 0, readBytes)
            Loop
          End Using
          Catch ex As Exception
            Debug.WriteLine("Error in Buffer Download")
            Debug.Indent()
            Debug.WriteLine(ex.Message)
        End Try
      End Using
    Catch ex As Exception
      Debug.WriteLine("Error in request.GetResponse()")
      Debug.Indent()
      Debug.WriteLine(ex.Message)
    End Try
  End If
 End Sub

Downloading a Video’s Thumbnail Image

In addition to downloading a YouTube video, InnerTube also downloads the large thumbnail image, sized 425 pixels × 344 pixels, to use as the preview image before a video starts playing. The format of the large image thumbnail URL is http://img.you tube.com/vi/VideoID/0.jpg.

The code to download the image (Example 4-24 and Example 4-25) simply checks whether the file has already been downloaded, and if it hasn’t, calls the DownloadFile method, passing in the location for the source file to download and the destination \filename.

Example 4-24.  C# code to download a video’s thumbnail image
public static void DownloadImage(InnerTubeVideo source, string destination)
{
    //if we haven't downloaded the image yet, download it
    if (!File.Exists(destination))
    {
        using (WebClient wc = new WebClient())
        {
            wc.DownloadFile(new Uri(source.ThumbnailLink), destination);
        }
    }
}
Example 4-25.  Visual Basic code to download a video’s thumbnail image
Public Shared Sub DownloadImage(ByVal source As InnerTubeVideo, ByVal destination As String)
  'if we haven't downloaded the image yet, download it
  If (Not File.Exists(destination)) Then
    Using wc As New WebClient()
      wc.DownloadFile(New Uri(source.ThumbnailLink), destination)
    End Using
  End If
 End Sub

Converting YouTube Videos Using ffmpeg

We now have the video on our hard drive in the Flash (FLV) format, and although we could build a simple HTML page to embed the Flash videos to watch them locally, what we really want to do is convert the video from FLV format to WMV (Zune) or MP4 (iTunes) format. To do that, we’ll use an open source tool called ffmpeg, which is a C++ library and command-line tool for converting, resizing, or resampling media from one format to another, with more than 50 formats supported.

In its simplest form, you can call ffmpeg.exe by passing in two command-line arguments, the source file that you want to convert and the name of the new file you want it converted to. For example, we can convert a Flash video file named source.flv into an MP4 (MPEG-4) format file named destination.mp4 using the command shown in Example 4-26.

Example 4-26. Converting a flash video to the MP4 video format
ffmpeg.exe -i "source.flv" "destination.mp4"

To convert the source.flv file into a .wmv (Windows Media Video) file, add the -vcodec wmv2 command-line arguments, as shown in Example 4-27.

Example 4-27. Converting a flash video to the WMV video format
ffmpeg.exe -i "source.flv" -vcodec wmv2 "destination.wmv"

You can even add metadata—such as the author, title, and comment—to the newly converted video by setting additional ffmpeg.exe command-line arguments, as shown in Example 4-28.

Example 4-28. Setting metadata when converting a video
ffmpeg.exe -title "My Video Title" -author "Dan" -comment "best video ever" -i
"source.flv" -vcodec wmv2 "destination.wmv"

To call ffmpeg.exe programmatically, first we’ll build a ConversionType enum to state the possible types of conversion our application allows (see Example 4-29 and Example 4-30).

Example 4-29.  C# code for the ConversionType enum
public enum ConversionType
{
  Mp4,
  Wmv
}
Example 4-30.  Visual Basic code for the ConversionType enum
Public Enum ConversionType
  Mp4
  Wmv
End Enum

We want to programmatically call the ffmpeg.exe console application from code using the System.Diagnostics.Process class. The call to ffmpeg is wrapped in the VideoConverter class, which has one method, ConvertFlv, with multiple overloads. The ConvertFlv method overload we’ll want to use accepts an InnerTubeVideo and a ConversionType enum (MP4 or WMV). We use this overload because we’ll use the video’s title, author, and description to set the video’s metadata, as described in Example 4-28. Since these fields might contain illegal characters, in which case ffmpeg.exe would not work, we will use the ReplaceIllegalCharacters method to clean them up.

As you can see in Example 4-31 and Example 4-32, we are building the command-line arguments to pass into ffmpeg.exe based on the type of conversion we are doing (MP4 or WMV).

Example 4-31.  C# code for setting ffmpeg parameters
public static void ConvertFlv(InnerTubeVideo source, ConversionType conversion)
{
  string title = FileHelper.ReplaceIllegalCharacters(source.Title);
  string author = FileHelper.ReplaceIllegalCharacters(source.Author);
  string description = FileHelper.ReplaceIllegalCharacters(source.Description);

  //set values based on switch
  string cmdLineArgs = String.Empty;
  string destination = String.Empty;

  switch (conversion)
  {
    case ConversionType.Mp4:
    //ffmpeg.exe -title "Chocolate Rain" -author "TayZonday" -comment "Original
    //Song by Tay Zonday" -i "Chocolate Rain.flv" "Chocolate Rain.mp4"
      destination = source.DownloadedMp4;
      cmdLineArgs = String.Format(" -title \"{0}\" -author \"{1}\" -comment \"{2}\"
        -i \"{3}\" \"{4}\"",title, author, description, source.DownloadedFlv,
        destination);
    break;
    case ConversionType.Wmv:
    //ffmpeg.exe -title "Chocolate Rain" -author "TayZonday" -comment "Original
    //Song by Tay Zonday" -i "Chocolate Rain.flv" -vcodec wmv2 "Chocolate Rain.wmv"
      destination = source.DownloadedWmv;
      cmdLineArgs = String.Format(" -title \"{0}\" -author \"{1}\" -comment \"{2}\"
      -i \"{3}\" -vcodec wmv2 \"{4}\"", title, author, description,
      source.DownloadedFlv, destination);
    break;
  }
  ConvertFlv(source.DownloadedFlv, destination, cmdLineArgs);
}
Example 4-32.  Visual Basic code for setting ffmpeg parameters
Public Shared Sub ConvertFlv(ByVal source As InnerTubeVideo, ByVal conversion _
  As ConversionType)
  Dim title As String = FileHelper.ReplaceIllegalCharacters(source.Title)
  Dim author As String = FileHelper.ReplaceIllegalCharacters(source.Author)
  Dim description As String = FileHelper.ReplaceIllegalCharacters(source.Description)

  'set values based on switch
  Dim cmdLineArgs As String = String.Empty
  Dim destination As String = String.Empty
  Select Case conversion
    Case ConversionType.Mp4
      'ffmpeg.exe -title "Chocolate Rain" -author "TayZonday" -comment "Original Song
      'by Tay Zonday" -i "Chocolate Rain.flv" "Chocolate Rain.mp4"
      destination = source.DownloadedMp4
      cmdLineArgs = String.Format(" -title ""{0}"" -author ""{1}"" " & _
        "-comment ""{2}"" -i ""{3}"" ""{4}""", title, author, _
        description, source.DownloadedFlv, destination)
   Case ConversionType.Wmv
    'ffmpeg.exe -title "Chocolate Rain" -author "TayZonday" -comment "Original Song
    'by Tay Zonday" -i "Chocolate Rain.flv" -vcodec wmv2 "Chocolate Rain.wmv"
    destination = source.DownloadedWmv
    cmdLineArgs = String.Format(" -title ""{0}"" -author ""{1}"" -comment " & _
      " ""{2}"" -i ""{3}"" -vcodec wmv2 ""{4}""", title, author, description, _
      source.DownloadedFlv, destination)
  End Select
  ConvertFlv(source.DownloadedFlv, destination, cmdLineArgs)
 End Sub

As you may have noticed in Example 4-31 and Example 4-32, the last line of code calls another overload for the ConvertFlv method, which actually calls ffmpeg from the command line, which we’ll discuss next.

In Example 4-33 and Example 4-34, the ConvertFlv method code includes the location of the ffmpeg.exe executable. The file itself is distributed in the SharedUtilities directory in an ffmpeg folder, so we can call Environment.CurrentDirectory to point to the executable and store the path into the exePath variable.

We’ll also make sure that we have the source file and don’t have the destination file. Assuming we have everything to run, we create a process and set some properties, such as the command-line arguments, the location of the .exe file, and other properties that will keep the console window from appearing while we convert a file. To get everything running, you call the Start method of the convert class. Since ffmpeg.exe will automatically close when it is finished converting a file, we’ll use the Wait ForExit() method, which means that the ConvertFlv method will not return until the conversion is complete.

Example 4-33.  C# code to create a process and call ffmpeg.exe
private static void ConvertFlv(string sourceFile, string destination,
  string cmdLineArgs)
{
  //point to ffmpeg conversion tool
  string exePath = Path.Combine(Environment.CurrentDirectory, @"ffmpeg\ffmpeg.exe");

  //ensure sourceFile files exist and the destination doesn't
  if (File.Exists(sourceFile) && File.Exists(exePath) && !File.Exists(destination))
  {
    //Start a Process externally as we're converting from the command line
    using (Process convert = new Process())
    {
      //Set properties
      convert.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
      convert.StartInfo.CreateNoWindow = true;
      convert.StartInfo.RedirectStandardOutput = true;
      convert.StartInfo.UseShellExecute = false;
      convert.StartInfo.Arguments = cmdLineArgs;
      convert.StartInfo.FileName = exePath;
      convert.Start();
      convert.WaitForExit();
    }
  }
}
Example 4-34.  Visual Basic code to create a process and call ffmpeg.exe
Private Shared Sub ConvertFlv(ByVal sourceFile As String, ByVal destination As _
  String, ByVal cmdLineArgs As String)
  'point to ffmpeg conversion tool
  Dim exePath As String = Path.Combine(Environment.CurrentDirectory, _
    "ffmpeg\ffmpeg.exe")

  'ensure sourceFile files exist and the destination doesn't
  If File.Exists(sourceFile) AndAlso File.Exists(exePath) AndAlso _
    (Not File.Exists(destination)) Then

    'Start a Process externally as we're converting from the command line
    Using convert As New Process()
      'Set properties
      convert.StartInfo.WindowStyle = ProcessWindowStyle.Hidden
      convert.StartInfo.CreateNoWindow = True
      convert.StartInfo.RedirectStandardOutput = True
      convert.StartInfo.UseShellExecute = False
      convert.StartInfo.Arguments = cmdLineArgs
      convert.StartInfo.FileName = exePath
      convert.Start()
      convert.WaitForExit()
    End Using
  End If
 End Sub

Now that we have converted the Flash videos into MP4 and/or WMV files, let’s show how you can sync them to iTunes or the Zune client software.

Syncing YouTube Videos to iTunes and Zune

iTunes and Zune provide different mechanisms for syncing videos, but we will build one interface that we can use for both videos, to keep a standard mechanism for syncing files (see Example 4-35 and Example 4-36). This interface will sync files for a given device based on the file path.

Example 4-35.  C# interface for syncing to iTunes and Zune
interface IVideoService
{
  void Sync(string filePath);
}
Example 4-36.  Visual Basic interface for syncing with iTunes and Zune
Friend Interface IVideoService
  Sub Sync(ByVal filePath As String)
End Interface

Syncing Videos to iTunes

To sync YouTube videos to iTunes, we’ll use the iTunes COM interop library. This library is automatically included when you install iTunes, and you can add it to your Visual Studio project by clicking in the Solution Explorer and selecting Add Reference.... Then, click the COM tab and select the iTunes 1.11 Type Library, which is a COM wrapper class that simplifies automating iTunes with .NET code.

As you can see in Example 4-37 and Example 4-38, we import the iTunesLib namespace and create a private member named iTunes.

Example 4-37.  C# class declaration for iTunes class
using iTunesLib;
public class iTunesSync : IVideoService
{
  iTunesApp iTunes = new iTunesApp();
...
Example 4-38.  Visual Basic class declaration for iTunes class
Imports iTunesLib
Public Class iTunesSync
Implements IVideoService
  Private iTunes As New iTunesApp()
...

The next step is to implement the IVideoService Sync interface, as shown in Example 4-39 and Example 4-40. To do this, we filter the list of files in the filePath variable to only those files with an MP4 file extension. Once we’ve filtered the list of videos, we then loop through each video and add it to the iTunes Library. When the Sync() method is called, iTunes will open (if it’s not already open) and will begin adding videos to your library. As iTunes builds a thumbnail for each video, it may take 1–3 seconds for each video to be added to your library, depending on your machine’s performance.

Example 4-39.  C# code for implementing the Sync interface
#region IVideoService Members
public void Sync(string filePath)
{
  //only get MP4 files
  string[] fileList = Directory.GetFiles(filePath, "*.mp4",
    SearchOption.TopDirectoryOnly);
  try
  {
    foreach (var f in fileList)
    {
      //Add file
      iTunes.LibraryPlaylist.AddFile(f);
    }
  }
  catch (Exception ex)
  {
    System.Diagnostics.Debug.WriteLine("iTunes error: " + ex.Message);
  }
  finally
  {
    this.iTunes = null;
  }
}
#endregion
Example 4-40.  Visual Basic code for implementing the Sync interface
#Region "IVideoService Members"
Public Sub Sync(ByVal filePath As String) Implements IVideoService.Sync
  'only get MP4 files
  Dim fileList() As String = Directory.GetFiles(filePath, "*.mp4", _
    SearchOption.TopDirectoryOnly)
  Try
    For Each f In fileList
      'Add file
      iTunes.LibraryPlaylist.AddFile(f)
    Next f
  Catch ex As Exception
    System.Diagnostics.Debug.WriteLine("iTunes error: " & ex.Message)
  Finally
    Me.iTunes = Nothing
  End Try
 End Sub
#End Region

Note

The reason we filter the list of files instead of looping through all the files in the directory is that per the iTunes COM SDK, the AddFile method will fail if you add a file that is not supported by iTunes (for example, a Windows Media Video file). The SDK suggests using the iTunes ConvertFile method, which will convert the file to a format iTunes understands. Although ConvertFile is one option for developers, ffmpeg.exe supports more file formats and has easy programmatic access.

We can, with one line of code, tell iTunes to update an iPod (if it’s connected), as shown in Example 4-41 and Example 4-42. Finally, as this is COM and since the iTunesApp class doesn’t implement Dispose, make sure to clean up the memory by setting the variable to null.

Example 4-41.  C# code to update an iPod and cleanup memory
public void UpdateIPod()
{
  iTunes.UpdateIPod();
}

˜iTunesSync()
{
  if (this.iTunes != null)
  {
    //cleanup
    this.iTunes = null;
  }
}
Example 4-42.  Visual Basic to update an iPod and cleanup memory
Public Sub UpdateIPod()
  iTunes.UpdateIPod()
 End Sub

Protected Overrides Sub Finalize()
If Me.iTunes IsNot Nothing Then
  'cleanup
  Me.iTunes = Nothing
 End If

 End Sub

Syncing Videos to Zune

The process for syncing videos to Zune is quite different than syncing videos to iTunes. Although the Zune team doesn’t provide an API we can call programmatically, Zune does make it very easy to add files to its library by simply copying the files to directories that Zune monitors.

Note

Underneath the covers, Zune continuously runs a process called Zune-Launcher.exe. Conceptually, ZuneLauncher implements the same functionality that a .NET FileSystemWatcher class, as it monitors changes to a set of folders for audio and video files.

Zune monitored folders

To pull the list of folders that Zune is monitoring, we will need to go spelunking into the Windows registry. Specifically, we will need to open the HKEY_CURRENT_USER\Software\Microsoft\Zune\Groveler registry key, as shown in Figure 4-9.

Zune monitored folders are stored in the registry
Figure 4-9. Zune monitored folders are stored in the registry

To do the same thing programmatically, we’ll create an enum named ZuneMonitoredFolders that will represent each monitored folder registry key. We can then call GetZuneMonitoredFolders to get a string array of folders by calling the GetValue method for the Groveler subkey using the Registry class, as shown in Example 4-43 and Example 4-44.

Example 4-43.  C# code to programmatically extract Zune monitored folders from the registry
public enum ZuneMonitoredFolders
{
  MonitoredAudioFolders,
  MonitoredVideoFolders,
  MonitoredPhotoFolders,
  MonitoredPodcastFolders
}

public class ZuneSync : IVideoService
{
  public static string[] GetZuneMonitoredFolders(ZuneMonitoredFolders folder)
  {
    //Pull registry
    string hive = @"Software\Microsoft\Zune\Groveler\";
    string[] values =
       (string[])Registry.CurrentUser.OpenSubKey(hive).GetValue(folder.ToString());
    return values;
}
...
Example 4-44.  Visual Basic code to programmatically extract Zune monitored folders from the registry
Public Enum ZuneMonitoredFolders
  MonitoredAudioFolders
  MonitoredVideoFolders
  MonitoredPhotoFolders
  MonitoredPodcastFolders
End Enum

Public Class ZuneSync
  Implements IVideoService

  Public Shared Function GetZuneMonitoredFolders(ByVal folder As _
    ZuneMonitoredFolders) As String()
    'Pull registry
    Dim hive As String = "Software\Microsoft\Zune\Groveler\"
    Dim values() As String = _
    CType(My.Computer.Registry.CurrentUser.OpenSubKey(hive). _
      GetValue(folder.ToString()), String())

    Return values
  End Function
...

Like we did with the iTunes wrapper, we will implement the IVideoService.Sync interface for Zune, as shown in Example 4-45 and Example 4-46. In the implementation of the Sync method, we first get the list of video folders that are being monitored. If the passed-in folder is already being monitored, then we are done because new videos being added will automatically show up in Zune. If it’s not in the list of monitored folders, we’re going to pick the first monitored folder, currentFolders[0], and similar to the iTunes code, we filter just those files with the .wmv extension and copy them to the first monitored folder.

Example 4-45.  C# code for implementing the Sync interface
#region VideoService Members
public void Sync(string filePath)
{
  string[] currentFolders = ZuneSync.GetZuneMonitoredFolders(
    ZuneMonitoredFolders.MonitoredVideoFolders);
  bool found = currentFolders.Contains(filePath);
  //check if we are already added the files to the folder
  if (!found)
  {
    //copy the files to the first specified directory
    if (currentFolders.Length >0)
    {
      string destinationPath = currentFolders[0];
      string[] Files = Directory.GetFiles(filePath, "*.wmv",
        SearchOption.TopDirectoryOnly);
      foreach (var f in Files)
      {
        File.Copy(f, destinationPath, true);
      }
    }
    else
    {
      throw new ArgumentException("Zune is not configured to monitor *any* " +
        "folders, to fix this, open zune.exe, click settings, " +
        "and add a video folder");
    }
  }
}
#endregion
Example 4-46.  Visual Basic code for implementing the Sync interface
#Region "VideoService Members"
Public Sub Sync(ByVal filePath As String) Implements IVideoService.Sync
  Dim currentFolders() As String = ZuneSync.GetZuneMonitoredFolders( _
    ZuneMonitoredFolders.MonitoredVideoFolders)
  Dim found As Boolean = currentFolders.Contains(filePath)
  'check if we are already adding the files to the folder
  If (Not found) Then
  'copy the files to the first specified directory
    If currentFolders.Length >0 Then
      Dim destinationPath As String = currentFolders(0)
      Dim Files() As String = Directory.GetFiles(filePath, "*.wmv", _
        SearchOption.TopDirectoryOnly)
      For Each f In Files
        File.Copy(f, destinationPath, True)
      Next f
    Else
      Throw New ArgumentException("Zune is not configured to monitor *any* " & _
      "folders,to fix this, open zune.exe, click settings, and add a video folder")
    End If
  End If
 End Sub
#End Region

Putting It All Together

At this point, you’ve seen how we can:

  • Convert YouTube’s API XML into objects

  • Download a YouTube video and a YouTube image thumbnail

  • Convert a YouTube video into WMV and MP4 formats

  • Sync videos to iTunes and Zune

What we now want to do is combine those four tasks and make them run asynchronously, so that we can still use our application while we’re downloading videos. To do all of the tasks just listed asynchronously, we’ll use the InnerTubeFeedWorker class.

To give you an idea of the size of the asynchronous operations that InnerTube needs to do, let’s walk through an example set of feeds that an InnerTube user might have:

Top Rated Videos of All Time

This represents the top-rated videos that have ever been on YouTube.

Most Viewed Videos This Week

This represents the most viewed videos for the current week.

That means that, at a high level, InnerTubeFeedWorker needs to do the following:

  1. Make two HTTP requests to YouTube’s API for each feed (two feeds requested once each).

  2. Make 50 image download requests for the thumbnails (2 feeds × 25 videos each = 50 images).

  3. Make 50 session token requests to get a valid token to download a video (2 feeds × 25 videos = 50 requests), and then make another 50 download video requests for the FLV videos (2 feeds × 25 videos each = 50 video files).

  4. Convert all 50 videos from the Flash format to Windows Media Video (WMV) format. If the user has opted to sync with iTunes, this would mean 100 video conversions because each video would need to be converted to both WMV format (InnerTube requires this) and the MP4 format (iTunes requires this).

  5. Sync the newly converted videos to iTunes and Zune.

Because of the scale of these tasks, InnerTube is designed to run in the background and uses SmartThreadPool, a www.codeproject.com library built by Ami Bar that enables you to build and control multiple thread pools.

To put this all together, look at Figure 4-10, which shows the five main tasks involved.

  • The Update Feed Pool task updates each of the feeds. Because these requests are relatively small (3–10K), we leave three threads to work on these tasks, as each thread will complete relatively quickly.

  • The Download Video Pool task is where we download the thumbnail images and videos. As this task is a long-running task that could potentially download hundreds of megabytes, we will spin up five threads to handle the work. This task tends to be the most network- and disk-intensive, as it is pulling large files and saving them on disk.

  • The Convert Video Pool task runs once we’ve downloaded all the videos. This task is very CPU intensive, and you will see your machine crank up to 100% utilization with this task. Because of this, we use only two threads to convert video files.

  • The Update Master List task updates our in-memory collection of videos that we will use for our application UI.

  • The Sync to iTunes/Zune Task adds the newly converted videos to your iTunes and/or Zune music collection.

The tasks done by the InnerTubeFeedWorker class
Figure 4-10. The tasks done by the InnerTubeFeedWorker class

Note

Rather than duplicate the documentation in Ami Bar’s CodeProject.com article where he walks through how to use the library, here we show how to call the InnerTubeFeedWorker background worker. For detailed documentation on SmartThreadPool, visit http://www.codeproject.com/KB/threads/smartthreadpool.aspx.

Calling InnerTubeFeedWorker

As you can see in the upcoming code, instead of using the new keyword to create an instance of InnerTubeFeedWorker, instead we use the static GetInstance method, which will retrieve an instance of the InnerTubeFeedWorker class. This is done so that there is only one instance of InnerTubeFeedWorker at one time and to prevent bad things from happening, such as multiple attempts to download and convert videos on different threads, as each thread would attempt to write to the same file stream, resulting in exceptions or data corruption.

When we call GetInstance, we pass in the list of InnerTubeFeeds and our application settings. We will cover the App class and the Setting class later in the Global variables section.

We can also set up event handlers for InnerTubeFeedWorker when it reports progress and when it’s finished executing, as shown in Example 4-47 and Example 4-48.

Example 4-47.  C# code for InnerTubeFeedWorker event handlers
InnerTubeFeedWorker iWork = InnerTubeFeedWorker.GetInstance(App.InnerTubeFeeds,
  App.Settings);

iWork.ProgressChanged += new System.ComponentModel.ProgressChangedEventHandler(
  iWork_ProgressChanged);
iWork.RunWorkerCompleted += new System.ComponentModel.
 RunWorkerCompletedEventHandler(iWork_RunWorkerCompleted);
Example 4-48.  Visual Basic code for InnerTubeFeedWorker event handlers
Dim iWork As InnerTubeFeedWorker = InnerTubeFeedWorker.GetInstance( _
  App.InnerTubeFeeds, App.Settings)

AddHandler iWork.ProgressChanged, AddressOf iWork_ProgressChanged
AddHandler iWork.RunWorkerCompleted, AddressOf iWork_RunWorkerCompleted

InnerTubeFeedWorker has a WorkType enumeration that will tell what specific tasks InnerTubeFeedWorker should execute, as explained in the code comments in Example 4-49 and Example 4-50.

Example 4-49.  C# code for the types of work the InnerTubeFeedWorker class can do
public enum WorkType
{
  UpdateFeeds, //just Update YouTube API feeds
  Download, //just download FLV files
  DownloadAndConvert, //download and convert FLV files
  Convert, //just convert FLV files
  All //do all of the above
}
Example 4-50.  Visual Basic code for the types of work the InnerTubeFeedWorker class can do
Public Enum WorkType
  UpdateFeeds 'just Update YouTube API feeds
  Download 'just download FLV files
  DownloadAndConvert 'download and convert FLV files
  Convert 'just convert FLV files
  All 'do all of the above
End Enum

To actually start the background worker, we need to call the RunWorkerAsync method and pass in the WorkType enum we want it to complete. This method will execute immediately, with all of the processing for InnerTubeFeedWorker taking place on separate, background threads.

To receive progress updates from InnerTubeFeedWorker, you can read e.UserState, which will contain a string with progress updates, such as when a video download is starting, when it’s complete, and so forth, as shown in Example 4-51 and Example 4-52. Similarly, when InnerTubeFeedWorker finishes executing, it will return an updated list of InnerTubeFeeds with which we can cast and update our InnerTubeFeeds variable.

Example 4-51.  C# code to read progress changed events
void iWork_ProgressChanged(object sender,
  System.ComponentModel.ProgressChangedEventArgs e)
{
  string s = (string)(e.UserState);
...

void iWork_RunWorkerCompleted(object sender,
  System.ComponentModel.RunWorkerCompletedEventArgs e)
{
  App.InnerTubeFeeds = (ObservableCollection<InnerTubeFeed>)e.Result;
...
Example 4-52.  Visual Basic code to read progress changed events
Private Sub iWork_ProgressChanged(ByVal sender As Object, _
  ByVal e As System.ComponentModel.ProgressChangedEventArgs)
  Dim s As String = CStr(e.UserState)
...

Private Sub iWork_RunWorkerCompleted(ByVal sender As Object, _
  ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs)
  App.InnerTubeFeeds = CType(e.Result, ObservableCollection(Of InnerTubeFeed))

Setting Up the InnerTube WPF Application

Now that you’ve seen the core libraries, let’s show how you can build a WPF application that can display our list of videos. The first thing we need to do is set up a couple of conventions for our application, namely where we will store global variables, such as the list of feeds and our application setting variables, whether iTunes or Zune is installed, and what location we will save downloaded videos to.

Global Variables

When you create a WPF project, it includes an Application class named App.xaml, which includes a code-behind file that, since it’s an application-level scope class, would be a perfect place for us to store our global variables. To do that, we declare our list of feeds and our application settings as static properties, as shown in Example 4-53 and Example 4-54.

Example 4-53.  C# code for InnerTube’s global variables
public partial class App : Application
{
  public static ObservableCollection<InnerTubeFeed> InnerTubeFeeds { get; set; }
  public static Setting Settings { get; set; }
...
Example 4-54.  Visual Basic code for InnerTube’s global variables
Partial Public Class App
    Inherits Application
  Private Shared privateInnerTubeFeeds As ObservableCollection(Of InnerTubeFeed)
  Public Shared Property InnerTubeFeeds() As ObservableCollection(Of InnerTubeFeed)
    Get
      Return privateInnerTubeFeeds
    End Get
    Set(ByVal value As ObservableCollection(Of InnerTubeFeed))
      privateInnerTubeFeeds = value
    End Set
  End Property

  Private Shared privateSettings As Setting
  Public Shared Property Settings() As Setting
    Get
          Return privateSettings
        End Get
        Set(ByVal value As Setting)
          privateSettings = value
        End Set
  End Property
...

As InnerTubeFeeds is a global variable storing all of the feeds and feed videos, we will need to update it during the following conditions:

  • When a user adds a feed in the AddNewFeed.xaml window

  • When a user right-clicks and deletes a feed or feed video from the MainWindow.xaml window

  • When a user kicks off the InnerTubeFeedWorker process, which will add/update the list videos in a feed (say, by downloading a new favorite or top-rated video)

Application Settings

Our application has a number of different settings that we will need to use and store. The following are the fields for the Setting class, along with a description of what each property does.

AppName

The name of the application and, more importantly, the name of the directory we’ll use to store our application’s files. Its default value is InnerTube.

FirstRun

A Boolean field used to determine whether it’s the first time the application has been run. It determines whether we should show the FirstRunWindow.xaml file and build an initial set of feeds for the user. Its default value is true.

InnerTubeFeedFile

This is the exact location of the InnerTubeFeeds.xml file when it’s saved to disk.

SubPath

This is the directory on your hard drive that we’ll use to store files such as the thumbnail images, unconverted FLV files, or serialized classes, such as the InnerTubeFeed class and the Setting class. It is named using the AppName property, and its default value is C:\Users\Username\Videos\InnerTube.

VideoPath

This is the directory on your hard drive that we’ll use to save videos once we convert them. Its default value is C:\Users\Username\Videos\.

iTunesInstalled

This is a Boolean field that will tell us whether iTunes is installed. Its default value is false.

ZuneInstalled

This is a Boolean field that will tell us whether the Zune software is installed. Its default value is false.

UpdateFeedPoolThreads

This is an integer field that sets the number of threads to use for the UpdateFeed Pool process. Its default value is 3 threads.

DownloadPoolThreads

This is an integer field that sets the number of threads to use for the Download Pool process. Its default value is 5 threads.

ConversionPoolThreads

This is an integer field that sets the number of threads to use for the Conversion Pool process. Its default value is 2 threads.

When our application is first run, the FirstRunWindow.xaml file will check whether a Settings file has already been created. If it hasn’t, it will call the SettingService.BuildDefaultSettings method, which will initialize the fields just listed to the machine-specific default values.

Building the InnerTube UI

The InnerTube UI uses a three-pane Microsoft Outlook-like UI, with the left pane representing a list of InnerTubeFeed objects, the middle pane representing a list of InnerTubeVideos for the selected feed, and the rightmost pane showing the detail for the selected video. The next section walks through the skeleton of the UI, and then we’ll show a screenshot of the actual UI and describe the required controls.

InnerTube Skeleton

This is the skeleton of the WPF controls contained in the application (see Figure 4-11):

  • At the highest level inside MainWindow, we have a DockPanel control that we use to set the Menu and ToolBar controls so they dock to the top of our window.

  • For the three-column layout, we use a Grid control with three column definitions and specify the column width with a wildcard (*), which, as with web applications, means the column size can grow when resized.

  • The first column (remember columns are zero-based) is a ListBox control that will hold the list of InnerTubeFeed objects for our application.

  • The second column is also a ListBox control, and will list the videos for a particular feed.

  • The third column contains a canvas control with multiple controls: a user control for the media player, TextBlock controls for fields such as the video title, and so on.

The primary controls in MainWindow.xaml
Figure 4-11. The primary controls in MainWindow.xaml

Note

InnerTube uses the C4fStyle.xaml file to define the appearance of buttons, ListBox controls, and more. To see what’s included in the C4fStyle, see the Appendix.

Dissecting MainWindow.xaml

Since it can be difficult to explain exactly how you take a set of skeleton controls and turn them into a fully functioning UI, we’ll dissect MainWindow.xaml into the three pieces, labeled 1, 2, and 3 in Figure 4-12. You can refer back to this screenshot to follow along with the expected UI.

The three Grid columns in MainWindow.xaml
Figure 4-12. The three Grid columns in MainWindow.xaml

Setting the MainWindow.xaml data context

Previously we defined a global variable named InnerTubeFeeds that holds the list of feeds and the corresponding list of videos. To data bind all of the controls in Main Window to InnerTubeFeeds, we will set the DataContext for the window using the Window_Loaded event, as shown in Example 4-55 and Example 4-56.

Example 4-55.  C# code to set the MainWindow.xaml data context
private void Window_Loaded(object sender, RoutedEventArgs e)
{
...
    this.DataContext = App.InnerTubeFeeds;
}
Example 4-56.  Visual Basic code to set the MainWindow.xaml data context
Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
...
  Me.DataContext = App.InnerTubeFeeds
 End Sub

Part 1: The Feed List ListBox

For the Feed List UI, we will use a templated ListBox control, as shown in Example 4-57. As the main UI is comprised of a Grid control, we need to add the control to the first column of the Grid control by setting the Grid.Column attribute for ListBox to zero (the first column). We won’t discuss this in detail here, but you’ll notice that we have a ContextMenu defined to delete a feed (when right-clicked). Next, we’ll delve into how we data bind using the ListBox ItemTemplate.

Example 4-57. XAML code for the first column in MainWindow.xaml
<!--First Column-->
<ListBox Grid.Column="0" x:Name="feedList" IsSynchronizedWithCurrentItem="True"
  ItemsSource="{Binding}"  Background="{x:Null}">
  <ListBox.ContextMenu>
    <ContextMenu>
      <MenuItem Click="DeleteFeed" CommandParameter="{Binding Path=/}"
        Header="Delete Feed" >
        <MenuItem.Icon>
          <Image Source="images/cross.png"></Image>
        </MenuItem.Icon>
      </MenuItem>
    </ContextMenu>
  </ListBox.ContextMenu>
  <ListBox.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <TextBlock FontWeight="Bold" Text="{Binding Path=FeedName}"/>
      </StackPanel>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

Data binding the feedList ListBox

To data bind the feedList ListBox control, we will set a couple of properties:

ItemSource

This is the data source you are binding to. Since we already set up a DataContext for all of MainWindow.xaml, this is just "{Binding}".

IsSynchronizedWithCurrentItem

This property must be set to true so that all of the controls on the window stay in sync, meaning that if a user clicks on another feed, the ListBox control that contains the list of videos and the details pane controls should update to show the videos in that feed.

DataTemplate

This template controls how each data-bound item will be displayed. In this example, we can data bind the Text property to the FeedName property from App.Inner TubeFeeds using Text="{Binding Path=FeedName}" as the binding expression.

Part 2: The VideoList ListBox

In the middle column, we will display a list of videos for the selected feed, including a thumbnail image, the video title, and the author of each video that belongs to the currently selected feed. Just like we did before, we declare the ListBox control and define the Grid.Column property, but this time we set it to 1, which is the second column. We also have a ContextMenu control that is bound to the Delete key by setting the InputGestureText, and we have a ListBox ItemTemplate that defines the UI for the ListBox, as shown in Example 4-58.

Example 4-58. XAML code for the second column in MainWindow.xaml
<!--Second Column-->
<ListBox Grid.Column="1"  IsSynchronizedWithCurrentItem="True"
  ItemsSource="{Binding Path=FeedVideos}" Name="VideoList"
  Background="{x:Null}" >
  <ListBox.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <StackPanel.ContextMenu>
          <ContextMenu Name="mnuDeleteVideo">
            <MenuItem Click="DeleteVideo" CommandParameter="{Binding}"
              Header="Foo" InputGestureText="Del" >
              <MenuItem.Icon>
                <Image Source="images/cross.png"></Image>
              </MenuItem.Icon>
            </MenuItem>
          </ContextMenu>
        </StackPanel.ContextMenu>
        <Image Margin="2,2,2,2" Source="{Binding Path=DownloadedImage,
          Converter={StaticResource img} }"
          Width="48" Height="48" VerticalAlignment="Center"></Image>
        <StackPanel VerticalAlignment="Center">
          <TextBlock  FontWeight="Bold" Text="{Binding Path=Title}"
            TextTrimming="WordEllipsis"  TextWrapping="Wrap" />
          <TextBlock Foreground="White"  Text="{Binding Path=Author}" />
        </StackPanel>
      </StackPanel>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

The Data Template UI

Let’s take a second to understand how we get XAML to produce a template that looks like Figure 4-13. All of the controls in the data template are defined in a StackPanel with the orientation set to Horizontal, meaning they will stack left to right.

Starting left to right in Example 4-59, we have an image, which itself has a margin of 2 (device-independent) pixels on each side, and a fixed width and height. The next control is another StackPanel control, and because it doesn’t explicitly have an Orientation, this means the Orientation is vertical (top to bottom). Inside the StackPanel we have two TextBlock controls, representing the title and author of the video.

The ListBox data template
Figure 4-13. The ListBox data template
Example 4-59. XAML data template for video list ListBox
<DataTemplate>
...
  <StackPanel Orientation="Horizontal">
    <Image Margin="2,2,2,2" Source="{Binding Path=DownloadedImage,
      Converter={StaticResource img} }"
      Width="48" Height="48" VerticalAlignment="Center">
    </Image>
    <StackPanel VerticalAlignment="Center">
      <TextBlock  FontWeight="Bold" Text="{Binding Path=Title}"
        TextTrimming="WordEllipsis"  TextWrapping="Wrap" />
      <TextBlock Foreground="White"  Text="{Binding Path=Author}" />
    </StackPanel>
  </StackPanel>
</DataTemplate>

Data Binding the Video List ListBox

Just like we did with the Feed List ListBox, we’ll set a couple of properties to enable data binding for the Video List:

ItemSource

As the top-level data binding is set to InnerTubeFeeds, we want to bind to just FeedVideos, so we’ll set ItemSource to "{Binding Path=FeedVideos}". The FeedVideos property is simply a collection of InnerTubeVideo classes, which means that we can set the Binding Path to any property in the InnerTubeVideo class.

IsSynchronizedWithCurrentItem

Just like before, we must be set this to true so that all of the controls on the window stay in sync.

Binding to an Image

Take a second to examine the binding syntax for the Image and the two TextBlock controls in Example 4-59. While the TextBlock controls simply bind to the Title and Author properties, the Image binding adds something new, a Converter and a Static Resource. We’ll quickly explain what’s going on here and how to use converters and static resources.

Building the Image Value Converter

We’re using a converter for the image because we want to have some custom code run when we data bind, to make sure that the DownloadedImage value actually exists; if it doesn’t, we can swap in a default image. A Converter is a .NET class that implements an interface named IValueConverter. IValueConverter has two methods, Convert and ConvertBack, which both receive a value of type object and have a return type of object, as shown in Example 4-60 and Example 4-61.

Example 4-60.  C# code for the IValueConverter interface
public interface IValueConverter
{
  object Convert(object value, Type targetType,
    object parameter, CultureInfo culture);

  object ConvertBack(object value, Type targetType,
    object parameter, CultureInfo culture);
}
Example 4-61.  Visual Basic code for the IValueConverter interface
Public Interface IValueConverter
  Function Convert(ByVal value As Object, ByVal targetType As Type, _
    ByVal parameter As Object, ByVal culture As CultureInfo) As Object
  Function ConvertBack(ByVal value As Object, ByVal targetType As Type, _
    ByValparameter As Object, ByVal culture As CultureInfo) As Object
End Interface

The next piece of code defines our implementation for IValueConverter. Let’s explain exactly what will happen when we data bind. Since we declared the Converter for the Image source, WPF will first pass the value of DownloadedImage property to Converter Image.Convert. The Convert method will check whether the DownloadedImage value is null, empty, or doesn’t exist. If it does exist, it’ll just return the image path. If the image doesn’t exist, then we’ll instead return a default “placeholder” image. The full code for the ConverterImage class is shown in Example 4-62 and Example 4-63.

Example 4-62.  C# code for the Image Converter
[ValueConversion(typeof(object), typeof(string))]
public class ConverterImage : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
  System.Globalization.CultureInfo culture)
{
  string path = (string)value;
  return SetImage(path);
}

public static object SetImage(string imagePath)
{
  //if the path doesn't exist
  if (String.IsNullOrEmpty(imagePath) || (!File.Exists(imagePath)))
  {
    //use default image
    return FileHelper.DefaultImage;
  }
  else
  {
    return imagePath;
  }
}

public object ConvertBack(object value, Type targetType,
  object parameter, System.Globalization.CultureInfo culture)
  {
    // we don't intend this to ever be called
    return null;
  }
}
Example 4-63.  Visual Basic code for the Image Converter
<ValueConversion(GetType(Object), GetType(String))> _
Public Class ConverterImage
  Implements IValueConverter

Public Function Convert(ByVal value As Object, ByVal targetType As Type, _
  ByVal parameter As Object, _
  ByVal culture As System.Globalization.CultureInfo) As Object
  Implements IValueConverter.Convert

  Dim path As String = CStr(value)
    Return SetImage(path)
End Function

Public Shared Function SetImage(ByVal imagePath As String) As Object
  'if the path doesn't exist
  If String.IsNullOrEmpty(imagePath) OrElse ((Not File.Exists(imagePath))) Then
     'use default image
     Return Path.Combine(App.Settings.SubPath, FileHelper.DefaultImage)
  Else
    Return imagePath
  End If
End Function

Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, _
  ByVal parameter As Object, _
  ByVal culture As System.Globalization.CultureInfo) As Object
  Implements IValueConverter.ConvertBack
  ' we don't intend this to ever be called
  Return Nothing
 End Function
End Class

Setting Up a Class As a Static Resource

Now that we’ve seen how to define a class that implements IValueConverter, let’s show how you can call that class declaratively using XAML by setting it as a Static Resource in MainWindow.xaml. A static resource is a way to define a class or other resource file in XAML. We can set the ConverterImage class shown earlier to be a Static Resource by doing a couple of things. First, we need to explicitly define the namespace that the ConverterImage class lives in and set up an alias for the namespace, which I chose to be Util, as shown in Example 4-64.

Example 4-64. Code to declare a .NET Namespace in XAML
<Window
        xmlns:Util="clr-namespace:InnerTube"

Second, we define the ConverterImage class as a StaticResource by declaring it in a ResourceDictionary in the format shown in the next code example. Notice that we reference the InnerTube namespace (aka Util), followed by the class name, and then define the Key that we can use to reference the StaticResource in XAML, as shown in Example 4-65.

Example 4-65. Code to declare a class and assign a key in XAML
<Window.Resources>
<ResourceDictionary>
     <!-- Value Converters for databinding-->
        <Util:ConverterImage x:Key="img" />
    </ResourceDictionary>
</Window.Resources>

Part 3: The Details Pane

As is evident by the name, the details pane displays the details of the currently selected InnerTubeVideo. The pane itself is made up of a Canvas element with a Grid.Column property of 2, which indicates the third column since the Grid Index starts at zero. The Canvas is made up of a set of controls that define their Canvas.Top and Canvas.Left properties, as shown in Example 4-66.

Note

Experienced WPF developers normally don’t like using the Canvas control, as it has fixed pixel values and doesn’t resize as gracefully as other controls. But it is fine for some scenarios, and there are benefits of using Visual Studio’s snap lines to align labels and their corresponding values without having to manually muck with XAML.

Example 4-66. XAML code for the details pane
<!-- 3rd Column -->
<Canvas Grid.Column="2" Name="canvas1">
  <!-- Title -->
<TextBlock Text="{Binding Path=FeedVideos/Title}" Name="VideoTitle"
  TextAlignment="Center" Height="28.453" Canvas.Left="18.859" Canvas.Top="9.228"
  Width="438.133" FontSize="16" FontWeight="Bold"></TextBlock>

  <!-- Video -->
<Util:MediaPlayer InnerTubeVideoFile="{Binding Path=FeedVideos/}"
  x:Name="VideoPlayer" Canvas.Left="18.571" Canvas.Top="47.678" Height="398.342"
  Width="438.421" />

  <!-- Description -->
  <ScrollViewer Canvas.Left="32.867" Canvas.Top="458.425" Height="86.684"
  Name="scrollViewer1" Width="424.413">
  <TextBlock Height="180" Text="{Binding Path=FeedVideos/Description}"
    Name="txtDescription" TextDecorations="None" TextWrapping="Wrap" />
  </ScrollViewer>

  <!-- Author -->
  <Label Canvas.Left="46" Canvas.Top="551.777" Height="28.339" Name="lblAuthor"
    Width="120" HorizontalContentAlignment="Right">Author:</Label>
  <TextBlock Text="{Binding Path=FeedVideos/Author}" Canvas.Left="173"
    Canvas.Top="559.209" Height="20.907" Name="txtAuthor" Width="120"/>

  <!-- Views -->
  <Label Canvas.Left="46" Canvas.Top="580.116" Height="28.339" Name="lblViews"
    Width="120" HorizontalContentAlignment="Right">Views:</Label>
  <TextBlock Text="{Binding Path=FeedVideos/Views,StringFormat=N0 }"
    Name="txtViews" Canvas.Left="173" Canvas.Top="587.548" Height="20.907"
    Width="120"/>

  <!-- Average Rating -->
  <Label Canvas.Left="46" Canvas.Top="610.122" Height="28.339" Name="lblRaters"
    Width="120" HorizontalContentAlignment="Right">Average Rating: </Label>
  <Util:RatingUserControl StarRating="{Binding Path=FeedVideos/AvgRating}"
    x:Name="Rating" Canvas.Left="173" Canvas.Top="615.123" Height="16.67" />

  <!-- Number of Ratings -->
  <Label Name="lblNumRating" Canvas.Left="46" Canvas.Top="636.794" Height="28.339"
    Width="120" HorizontalContentAlignment="Right"># of Ratings:</Label>
  <TextBlock Text="{Binding Path=FeedVideos/NumRaters, StringFormat=N0}"
    Name="txtNumRating" Canvas.Left="173" Canvas.Top="641.795" Height="21.671"
    Width="120" />

  <!-- Publish Date -->
  <Label Canvas.Left="46" Canvas.Top="660.132" Height="28.339" Name="lblPublished"
    Width="120" HorizontalContentAlignment="Right">Published:</Label>
  <TextBlock Text="{Binding Path=FeedVideos/Published}" Name="txtPublished"
    Canvas.Left="173" Canvas.Top="665.133" Height="21.671" Width="216" />
</Canvas>

Formatting Numbers in WPF

For fields such as txtViews and txtNumRating, we can format the values using the StringFormat property and set its value to a valid .NET Framework formatting string such as N0 (numbers with commas), as shown in Example 4-67.

Example 4-67. Data binding the total views for a video using a value converter
<TextBlock Text="{Binding Path=FeedVideos/Views,StringFormat=N0 }" ...

As the txtViews property is an int data type, if we were to data bind without the StringFormat property, we would get the following example value: 8675309.

After setting the StringFormat property, the txtViews property will appear as 8,675,309.

Data Binding in the Details Pane

Unlike the previous two columns, the details pane is made up of separate, unlinked controls, so we do not define an ItemSource or set the IsSynchronizedProperty.

As a result, the binding syntax for each of the controls is slightly different. Specifically, each Binding Path will start at the root of the InnerTubeFeed class, so to get to the title of a particular video, we need to set the path starting at the FeedVideos property, as in Example 4-68.

Example 4-68. Data binding the video title
<TextBlock Text="{Binding Path=FeedVideos/Title}" ...

Data Binding to User Controls

Back in the details pane XAML listing, you’ll notice that for actually playing the video, we are data binding to a custom user control named MediaPlayer, which has a custom property named InnerTubeVideoFile. As you can see, the Path value is set to FeedVideos/, which means that we will pass in the entire currently selected Inner TubeVideo class to the InnerTubeVideoFile property when we data bind, as shown in Example 4-69.

Example 4-69. XAML code to pass an InnerTubeVideo to the MediaPlayer user control
<Util:MediaPlayer InnerTubeVideoFile="{Binding Path=FeedVideos/}"

MediaPlayer User Control UI

The UI for the MediaPlayer user control is a Grid that contains four elements. The first two are the Image and MediaElement controls, which are placed directly on top of each other (notice the same Margin values for both), as shown in Example 4-70. We will use the Image control to display a video’s preview image when the control first loads, but once a video starts playing, we will hide the image. As both of the elements are stacked right on top of each other, we can use the Grid.ZIndex attribute to state that the Image, which has a higher ZIndex, should appear on top of the MediaElement.

Next is a StackPanel control, which contains one element, a ToggleButton to toggle between playing and pausing a video. Inside of it is a Path, which is an XAML shape for the Play button.

Example 4-70. XAML for the MediaPlayer user control
<Grid>
  <Image Grid.ZIndex="1" Margin="5,0,5,73" Name="PreviewImage" ></Image>
  <MediaElement Grid.ZIndex="0" Margin="5,0,5,73" Name="VideoPlayer"
    LoadedBehavior="Manual"></MediaElement>
  <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
    VerticalAlignment="Bottom">
    <ToggleButton Margin="0,0,0,10" Height="50" Width="50"
      Name="PlayButton" Style="{StaticResource PlayButton}"
      Click="Play">
      <Path Data="F1M149.333,406L149.333,598 317.333,502 149.333,406z"
        Fill="#80000000" Height="14" Margin="4,0,0,0" Name="Path" Stretch="Fill"
        Width="14" />
    </ToggleButton>
  </StackPanel>
</Grid>

MediaPlayer Data Binding

To enable data binding with the MediaPlayer control, we need to create a special type of WPF property known as a dependency property, which enables you to make your controls data-bindable in WPF (among other benefits).

You can see the definition for the InnerTubeVideoFile dependency property in the MediaPlayer code-behind file shown next. The first step is to build a property like you normally would, with the getter returning the DependencyProperty value (C# developers should not use C# 3.0’s automatic properties for this), as shown in Example 4-71 and Example 4-72.

Next, you declare and register the dependency property, passing in the name, the type you’ll receive, the class you are registering for, and finally an event handler that will fire when the value is changed. This event handler replaces the set code in the property because that code will never execute, and therefore change values should be handled via the event handler.

Example 4-71.  C# code for the InnerTubeVideoFile dependency property
public InnerTubeVideo InnerTubeVideoFile
{
  get { return (InnerTubeVideo)GetValue(InnerTubeVideoProperty); }
  set { /* don't write code here it won't execute */ }
}

public static readonly DependencyProperty InnerTubeVideoProperty =
  DependencyProperty.Register("InnerTubeVideoFile",
  typeof(InnerTubeVideo),
  typeof(MediaPlayer),
  new UIPropertyMetadata(MediaPlayer.InnerTubeVideoFileChanged));
Example 4-72.  Visual Basic code for the InnerTubeVideoFile dependency property
Public Property InnerTubeVideoFile() As InnerTubeVideo
  Get
    Return CType(GetValue(InnerTubeVideoProperty), InnerTubeVideo)
  End Get
  Set(ByVal value As InnerTubeVideo)
    'Don't write code here it won't execute
  End Set
End Property

Public Shared ReadOnly InnerTubeVideoProperty As DependencyProperty = _
  DependencyProperty.Register("InnerTubeVideoFile", _
  GetType(InnerTubeVideo), _
  GetType(MediaPlayer), _
  New UIPropertyMetadata(AddressOf MediaPlayer.InnerTubeVideoFileChanged))

Let’s now take a look at a snippet of the InnerTubeVideoFile event handler, which will fire any time the property changes (Example 4-73 and Example 4-74). There are a couple of things to notice here. First, the event is static and therefore doesn’t have access to instance members of the current MediaPlayer control (using this.foo will not work in the event handler). The nice thing is that the current instance class is passed into you as the DependencyObject d, which you can then cast to a MediaPlayer and set instance properties.

The second thing to note is that we can retrieve the passed-in class and cast it to an InnerTubeVideo through the DependencyPropertyChangedEventArgs e variable.

Now that we have the current instance and the new value, we can set the properties for the Image and the MediaElement controls, as shown in the code snippets.

Note

The MediaElement control does not support MP4 video playback, so we must data bind to a Windows Media (WMV) file.

Example 4-73.  Snippets from the C# event that fires when the InnerTubeVideFile property changes
private static void InnerTubeVideoFileChanged(DependencyObject d,
  DependencyPropertyChangedEventArgs e)
{
  MediaPlayer player = (MediaPlayer)d;
  InnerTubeVideo newVideo = (InnerTubeVideo)e.NewValue;
...
  ImageSourceConverter imageConvert = new ImageSourceConverter();
...
  player.PreviewImage.Source =
 (ImageSource)imageConvert.ConvertFromString(newVideo.DownloadedImage);
...
  //Set Video File
  player.VideoPlayer.Source = new Uri(newVideo.DownloadedWmv);
...
Example 4-74.  Snippets from the Visual Basic event that fires when the InnerTubeVideoFile property changes
Private Shared Sub InnerTubeVideoFileChanged(ByVal d As DependencyObject, _
  ByVal e As DependencyPropertyChangedEventArgs)
  Dim player As MediaPlayer = CType(d, MediaPlayer)
  Dim newVideo As InnerTubeVideo = CType(e.NewValue, InnerTubeVideo)
...
  Dim imageConvert As New ImageSourceConverter()
...
  player.PreviewImage.Source = CType(imageConvert.ConvertFromString( _
    newVideo.DownloadedImage), ImageSource)
...
  'Set Video File
  player.VideoPlayer.Source = New Uri(newVideo.DownloadedWmv)
...

Playing and Pausing Video

The final thing we’ll look at is the ToggleButton Click event, which is declared as shown in Example 4-75.

Example 4-75. Declaring the ToggleButton in XAML
< ToggleButton ... Name="PlayButton" Click="Play">

In the Play event, we can cast the ToggleButton to see whether it’s currently checked, and do things such as hide the preview image, as shown in Example 4-76 and Example 4-77. The MediaElement control has static methods to Play() and Pause() audio and video files, assuming a source has been set.

Example 4-76.  C# code for the ToggleButton’s Click method
void Play(object sender, RoutedEventArgs args)
{
  ToggleButton tb = (ToggleButton)sender;
  if (isVideo)
  {
    //Hide image if it's a video
    this.PreviewImage.Visibility = Visibility.Hidden;
    if ((bool)tb.IsChecked)
    {
      VideoPlayer.Play();
    }
    else
    {
      VideoPlayer.Pause();
    }
  }
}
Example 4-77.  Visual Basic code for the ToggleButton’s Click method
Private Sub Play(ByVal sender As Object, ByVal args As RoutedEventArgs)
  Dim tb As ToggleButton = CType(sender, ToggleButton)
  If isVideo Then
    'Hide image if it's a video
    Me.PreviewImage.Visibility = Visibility.Hidden
    If CBool(tb.IsChecked) Then
      VideoPlayer.Play()
    Else
      VideoPlayer.Pause()
    End If
  End If
 End Sub

Final Thoughts

In this chapter, you’ve seen how YouTube’s API works, how to circumvent YouTube’s restrictions for downloading videos, how to convert video files easily using ffmpeg, how to programmatically sync videos to iTunes and Zune, and how to put it all together in one application.

Although InnerTube will never replace YouTube (nor was it designed to), I can’t tell you how many times I have ended up stuck somewhere with either my Zune or my laptop with time to kill but no Internet connection. Then I excitedly remember that I have a fresh new set of popular YouTube videos downloaded to my hard drive to watch and entertain myself.

You can of course adapt this code to work with other Flash video websites, such as Break.com or CollegeHumor.com, and build a way to automatically download and convert any Flash video on the Web!

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required