Ok, last time we got the groundwork laid for MyTube. It doesn’t do anything extremely exciting but we can connect to YouTube and grab some information. In this installment we’re going to take the information returned from YouTube and for each video draw the thumbnail into our window.

We’re going to start off by adding another class in order to hold the image browser items. To do this ctrl-click on the Classes folder and select Add -> New File. Select the Objective-C class entry and hit next. Name the class MyTubeIKBrowserItem.

Edit the MyTubeIKBrowserItem.h file and make it look similar to the following:

#import <Cocoa/Cocoa.h>

@interface MyTubeIKBrowserItem : NSObject {
    NSString *imageID;
    NSImage *image;
}
- (id)init:theImageID image:(NSImage *)theImage;
- (NSString *)imageUID;
- (NSString *)imageRepresentationType;
- (id)imageRepresentation;
- (NSString *)imageTitle;
@end

These methods allow us to implement the protocol for an IKImageBrowserItem. The imageTitle function is optional but I like seeing the titles in my image view.

With that done, open the MyTubeIKBrowserImage.m file and paste the following:

#import "MyTubeIKBrowserItem.h"
#import <Quartz/Quartz.h>

@implementation MyTubeIKBrowserItem
- (id)init:theImageID image:(NSImage *)theImage {
    if (self = [super init]) {
        image = [theImage copy];
        imageID = [[theImageID lastPathComponent] copy];
    }
    return self;
}

- (NSString *)imageUID {
    return imageID;
}

- (NSString *)imageRepresentationType {
    return IKImageBrowserNSImageRepresentationType;
}

- (id)imageRepresentation {
    return image;
}

- (NSString*)imageTitle {
    return imageID;
}
@end

You’ll notice we’re including the Quartz headers. This is where the definitions for the IKImageBrowserView reside. We’ll need to add the Quartz framework to the project in the same way we added the GData framework in the last tutorial. The implementation of MyTubeIKBrowserItem is pretty simplistic. About the only confusing bit is that the image ID is the image title. I just did that as I figured the title would be unique so there was no reason to use a secondary ID. We’re going to use NSImage objects for our images and that’s what we’re saying with the IKImageBrowserNSImageRepresentationType.

You should probably do a test build at this point to make sure everything is in there correctly. Don’t worry, I’ll wait.

Great, we’ve got our foundation in place so let’s get this party started. Open the MyTubeAppController.h file and add the following below the feed_type entry:

IBOutlet IKImageBrowserView *browser;
NSMutableArray *imageList;

Don’t forget to import <Quartz/Quartz.h> at the top of the file.

That’s all we need to do to the .h file. Let’s setup the GUI for our image browser. Open up the MainMenu.nib file in Interface Builder. From the Library select Image Kit and drag the Image Browser View to your window. In the control window ctrl-click on the My Tube object we created last time and drag the circle beside browser to the newly placed Image Browser View.

This will add the component and hook it up to our controller. Before we close Interface Builder lets set a couple of properties up to make things nicer. Select the Image Browser View we just placed in the window. We’re going to embed it into a scroll view as there are usually more thumbnails then we can see on a screen. Select Layout -> Embed Objects In -> Scroll View. Verify in the apple-3 pane of the inspector that everything is going to resize correctly.

Now, with the scroll view selected press apple-1 to go back to the attributes pane of the inspector. Make sure horizontal scrolling is disabled and turn on the auto hiding of the scroll bars. Select the Image Browser View again and in the attributes pane make sure that Titled is selected, Multiple Selection is disabled and I like to enable the Animates option.

That’s it, you can now save and close interface builder.

Open the MyTubeAppController.m file in interface builder. We need to add a bit of initialization for our new array and browser view so go to the awakeFromNib function and add the following at the end:

imageList = [[NSMutableArray alloc] initWithCapacity:20];
[browser setDelegate:self];
[browser setDataSource:self];

We’re creating our initial image list with a capacity of 20 items. Don’t worry, this will grow if needed. For our image browser we set ourselves as the delegate and the data source. We’ll need to implement a couple of methods to handle being the data source for the image browser.

Add the following two methods to the @implementation section of MyTubeAppController.

/* these two functions are for the image browser delegate */
- (NSUInteger)numberOfItemsInImageBrowser:(IKImageBrowserView *)imageBrowser {
    return [imageList count];
}

- (id)imageBrowser:(IKImageBrowserView *)imageBrowser itemAtIndex:(NSUInteger)index {
    return [imageList objectAtIndex:index];
}

All we’re doing is telling the image browser the number of items we want to display and returning each item when queried. That’s it. Other then actually having data items to display our image browser is ready to rock.

Ok, so the next step is to actually retrieve some images from YouTube to show in our browser. To do that we’re going to edit the entryListFetchTicket:finishedWithFeed: method. The final method should look similar to:

- (void)entryListFetchTicket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedBase *)feed {
    GDataFeedYouTubeVideo *vfeed = (GDataFeedYouTubeVideo *)feed;
    int i;

    for (i = 0; i < [[vfeed entries] count]; i++) {
        GDataEntryBase *entry = [[vfeed entries] objectAtIndex:i];
        if (![entry respondsToSelector:@selector(mediaGroup)]) continue;

        GDataEntryYouTubeVideo *video = (GDataEntryYouTubeVideo *)entry;
        NSArray *thumbnails = [[video mediaGroup] mediaThumbnails];
        if ([thumbnails count] == 0) continue;

        NSString *imageURLString = [[thumbnails objectAtIndex:0] URLString];
        [self fetchEntryImageURLString:imageURLString withTitle:[[entry title] stringValue]];
    }
}

You can see that in place of just printing the feed title we’re actually doing something useful with the returned data. In this case we first verify that the entry responds to the mediaGroup selector. If this is true then we have a video item. If the entry doesn’t respond to mediaGroup then this isn’t a video object and we just skip it as there won’t be any thumbnails.

Since we know this is a video object we cast it to a GDataEntryYouTubeVideo object. With the video object in hand we call [[video mediaGroup] mediaThumbnails] to first retrieve the media group for the video and then grab any thumbnails for that media group.

If there are no thumbnails present in the group we can skip this video object as there is nothing to display. (Yes, this is probably silly if you want to see everything, I’m only interested in pretty pictures here.)

Once we know we’ve got some thumbnails we generate the URL for the first thumbnail. This is done by using the URLString method of the first thumbnail item. Using the URL string we call fetchEntryImageURLString:withTitle: which we’ll define next.

fetchEntryImageURLString:withTitle: is a private method of the controller class. As such we need to add - (void)fetchEntryImageURLString:(NSString *)urlString withTitle:(NSString *)title; into the @interface MyTubeAppController (Private) section we’ve previously defined at the top of the file.

Then, we’ll put the following implementation into the private @implementation section with the youTubeService method.

/* fetch a specific image URL from YouTube */
- (void)fetchEntryImageURLString:(NSString *)urlString withTitle:(NSString *)title {
    if (!urlString) return;

    NSURL *imageURL = [NSURL URLWithString:urlString];
    NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
    GDataHTTPFetcher *fetcher = [GDataHTTPFetcher httpFetcherWithRequest:request];
    [fetcher setUserData:title];

    [fetcher beginFetchWithDelegate:self
                  didFinishSelector:@selector(imageFetcher:finishedWithData:)
          didFailWithStatusSelector:@selector(imageFetcher:failedWithStatus:data:)
           didFailWithErrorSelector:@selector(imageFetcher:failedWithError:)];
}

Similar to when we were fetching the original feed we generate a NSURL object. We then use this object to create a NSURLRequest. Our request gets fed into the GDataHTTPFetcher object which will do the actual work. As I want to get access to it later I set the title onto the fetcher as a bit of data. We then use the beingFetchWithDelegate:didFinishSelector:didFailWithStatusSelector:didFailWithErrorSelector: method to initiate the fetch.

With that done all we have to do is implement the final three selectors and we’re finished.

Back in the main @implementation section for MyTubeAppController we add the following:

/* These three functions handle the responses for fetching an image */
- (void)imageFetcher:(GDataHTTPFetcher *)fetcher finishedWithData:(NSData *)data {
    NSImage *image = [[[NSImage alloc] initWithData:data] autorelease];

    [imageList addObject:[[MyTubeIKBrowserItem alloc] init:[fetcher userData] image:image]];
    [browser reloadData];
}

- (void)imageFetcher:(GDataHTTPFetcher *)fetcher failedWithStatus:(int)status data:(NSData *)data {
    NSString *dataStr = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
    NSLog(@"image fetch error %d with data %@", status, dataStr);
}

- (void)imageFetcher:(GDataHTTPFetcher *)fetcher failedWithError:(NSError *)error {
    NSLog(@"Image fetch error %@", error);
}

The last too functions we’re going to skip over. They both deal with error cases and in both cases we just spit the error to the console.

The first function is the one we’re interested in. This will be called when the HTTP fetcher successfully retrieves the data from YouTube. We know the data we’re fetching is an image so we initialize an NSImage object with the provided data. We then take the given object and the image title (pulled out of the data attached to our HTTP fetcher) and create a new MyTubeIKBrowserItem which is added to our image list. Finally, we reload the browser view and we’re done.

If you run the app you should be able to grab the feeds and see the thumbnails appear in the image browser. Pretty simple, eh? Next time we’ll see if we can play a video that we select.

A complete listing of MyTubeAppController.m follows:

#import "MyTubeAppController.h"
#import "MyTubeIKBrowserItem.h"
#import "GData/GData.h"

@interface MyTubeAppController (Private)
- (GDataServiceGoogleYouTube *)youTubeService;
- (void)fetchEntryImageURLString:(NSString *)urlString withTitle:(NSString *)title;
@end

@implementation MyTubeAppController
- (void)awakeFromNib
{
    NSArray *feedTypes = [NSArray arrayWithObjects:
            @"Most Discussed",
            @"Most Linked",
            @"Most Responded",
            @"Most Viewed",
            @"Top Favorites",
            @"Recently Featured",
            nil];

    /* set the items into the field types combo box */
    [feed_type removeAllItems];
    [feed_type addItemsWithTitles:feedTypes];
    [feed_type selectItemWithTitle:@"Most Viewed"];

    imageList = [[NSMutableArray alloc] initWithCapacity:20];
    [browser setDelegate:self];
    [browser setDataSource:self];
}

- (IBAction)grab:(id)sender
{
    GDataServiceGoogleYouTube *service = [self youTubeService];
    GDataServiceTicket *ticket;

    NSString *feedName = [[feed_type selectedItem] title];
    feedName = [feedName lowercaseString];
    feedName = [feedName stringByReplacingOccurrencesOfString:@" " withString:@"_"];

    NSURL *feedURL = [GDataServiceGoogleYouTube youTubeURLForFeedID:feedName];

    ticket = [service fetchYouTubeFeedWithURL:feedURL
                                     delegate:self
                            didFinishSelector:@selector(entryListFetchTicket:finishedWithFeed:)
                              didFailSelector:@selector(entryListFetchTicket:failedWithError:)];

    [imageList removeAllObjects];
}

/* these two functions are for handling the response from fetching a feed */
- (void)entryListFetchTicket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedBase *)feed {
    GDataFeedYouTubeVideo *vfeed = (GDataFeedYouTubeVideo *)feed;
    int i;

    for (i = 0; i < [[vfeed entries] count]; i++) {
        GDataEntryBase *entry = [[vfeed entries] objectAtIndex:i];
        if (![entry respondsToSelector:@selector(mediaGroup)]) continue;

        GDataEntryYouTubeVideo *video = (GDataEntryYouTubeVideo *)entry;
        NSArray *thumbnails = [[video mediaGroup] mediaThumbnails];
        if ([thumbnails count] == 0) continue;

        NSString *imageURLString = [[thumbnails objectAtIndex:0] URLString];
        [self fetchEntryImageURLString:imageURLString withTitle:[[entry title] stringValue]];
    }
}

- (void)entryListFetchTicket:(GDataServiceTicket *)ticket failedWithError:(NSError *)error {
    NSLog(@"Error %@", error);
}

/* These three functions handle the responses for fetching an image */
- (void)imageFetcher:(GDataHTTPFetcher *)fetcher finishedWithData:(NSData *)data {
    NSImage *image = [[[NSImage alloc] initWithData:data] autorelease];

    [imageList addObject:[[MyTubeIKBrowserItem alloc] init:[fetcher userData] image:image]];
    [browser reloadData];
}

- (void)imageFetcher:(GDataHTTPFetcher *)fetcher failedWithStatus:(int)status data:(NSData *)data {
    NSString *dataStr = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
    NSLog(@"image fetch error %d with data %@", status, dataStr);
}

- (void)imageFetcher:(GDataHTTPFetcher *)fetcher failedWithError:(NSError *)error {
    NSLog(@"Image fetch error %@", error);
}

/* these two functions are for the image browser delegate */
- (NSUInteger)numberOfItemsInImageBrowser:(IKImageBrowserView *)imageBrowser {
    return [imageList count];
}

- (id)imageBrowser:(IKImageBrowserView *)imageBrowser itemAtIndex:(NSUInteger)index {
    return [imageList objectAtIndex:index];
}
@end

@implementation MyTubeAppController (Private)
- (GDataServiceGoogleYouTube *)youTubeService {
    static GDataServiceGoogleYouTube *service = nil;

    if (!service) {
        service = [[GDataServiceGoogleYouTube alloc] init];
        [service setUserAgent:@"MyTube-1.0"];
        [service setShouldCacheDatedData:YES];

        /* this is where we'd set the username/password if we accepted one */
        [service setUserCredentialsWithUsername:nil password:nil];
    }

    return service;
}

/* fetch a specific image URL from YouTube */
- (void)fetchEntryImageURLString:(NSString *)urlString withTitle:(NSString *)title {
    if (!urlString) return;

    NSURL *imageURL = [NSURL URLWithString:urlString];
    NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
    GDataHTTPFetcher *fetcher = [GDataHTTPFetcher httpFetcherWithRequest:request];
    [fetcher setUserData:title];

    [fetcher beginFetchWithDelegate:self
                  didFinishSelector:@selector(imageFetcher:finishedWithData:)
          didFailWithStatusSelector:@selector(imageFetcher:failedWithStatus:data:)
           didFailWithErrorSelector:@selector(imageFetcher:failedWithError:)];
}
@end