PageRenderTime 275ms CodeModel.GetById 150ms app.highlight 118ms RepoModel.GetById 1ms app.codeStats 0ms

/core/externals/update-engine/externals/gdata-objectivec-client/Examples/YouTubeSample/YouTubeSampleWindowController.m

http://macfuse.googlecode.com/
Objective C | 907 lines | 565 code | 187 blank | 155 comment | 75 complexity | f36959529646a25eaeea2290cec6ba93 MD5 | raw file
  1/* Copyright (c) 2008 Google Inc.
  2 *
  3 * Licensed under the Apache License, Version 2.0 (the "License");
  4 * you may not use this file except in compliance with the License.
  5 * You may obtain a copy of the License at
  6 *
  7 *     http://www.apache.org/licenses/LICENSE-2.0
  8 *
  9 * Unless required by applicable law or agreed to in writing, software
 10 * distributed under the License is distributed on an "AS IS" BASIS,
 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 * See the License for the specific language governing permissions and
 13 * limitations under the License.
 14 */
 15
 16//
 17//  YouTubeSampleWindowController.m
 18//
 19
 20//
 21// IMPORTANT:
 22//
 23// The XML-based API for YouTube has been replaced with a more efficient
 24// and easier-to-use JSON API.  The JSON API is documented at
 25//
 26//   https://developers.google.com/youtube/v3/
 27//
 28// See the new Objective-C client library and sample code at
 29//   http://code.google.com/p/google-api-objectivec-client/
 30//
 31// This sample application and library support for the XML-based YouTube
 32// API will eventually be removed.
 33//
 34
 35
 36#import "YouTubeSampleWindowController.h"
 37#import "GData/GDataServiceGoogleYouTube.h"
 38#import "GData/GDataEntryPhotoAlbum.h"
 39#import "GData/GDataEntryPhoto.h"
 40#import "GData/GDataFeedPhoto.h"
 41#import "GData/GDataEntryYouTubeUpload.h"
 42
 43#import "GData/GTMOAuth2WindowController.h"
 44
 45static NSString* const kActivityFeed = @"activity";
 46static NSString* const kChannelsFeed = @"channels";
 47static NSString* const kMostPopularFeed = @"most popular";
 48
 49@interface YouTubeSampleWindowController (PrivateMethods)
 50- (void)updateUI;
 51
 52- (void)fetchEntryImageURLString:(NSString *)urlString;
 53
 54- (GDataEntryBase *)selectedEntry;
 55- (void)fetchAllEntries;
 56- (void)uploadVideoFile;
 57- (void)restartUpload;
 58
 59- (GDataFeedYouTubeVideo *)entriesFeed;
 60- (void)setEntriesFeed:(GDataFeedYouTubeVideo *)feed;
 61
 62- (NSError *)entriesFetchError;
 63- (void)setEntriesFetchError:(NSError *)error;
 64
 65- (GDataServiceTicket *)entriesFetchTicket;
 66- (void)setEntriesFetchTicket:(GDataServiceTicket *)ticket;
 67
 68- (NSString *)entryImageURLString;
 69- (void)setEntryImageURLString:(NSString *)str;
 70
 71- (GDataServiceTicket *)uploadTicket;
 72- (void)setUploadTicket:(GDataServiceTicket *)ticket;
 73
 74- (NSURL *)uploadLocationURL;
 75- (void)setUploadLocationURL:(NSURL *)url;
 76
 77- (GDataServiceGoogleYouTube *)youTubeService;
 78
 79- (void)ticket:(GDataServiceTicket *)ticket
 80  hasDeliveredByteCount:(unsigned long long)numberOfBytesRead
 81  ofTotalByteCount:(unsigned long long)dataLength;
 82
 83- (void)fetchStandardCategories;
 84@end
 85
 86@implementation YouTubeSampleWindowController
 87
 88static YouTubeSampleWindowController* gYouTubeSampleWindowController = nil;
 89
 90static NSString *const kKeychainItemName = @"YouTubeSample: YouTube";
 91
 92+ (YouTubeSampleWindowController *)sharedYouTubeSampleWindowController {
 93
 94  if (!gYouTubeSampleWindowController) {
 95    gYouTubeSampleWindowController = [[YouTubeSampleWindowController alloc] init];
 96  }
 97  return gYouTubeSampleWindowController;
 98}
 99
100- (id)init {
101  return [self initWithWindowNibName:@"YouTubeSampleWindow"];
102}
103
104- (void)awakeFromNib {
105  // Load the OAuth token from the keychain, if it was previously saved
106  NSString *clientID = [mClientIDField stringValue];
107  NSString *clientSecret = [mClientSecretField stringValue];
108
109  GTMOAuth2Authentication *auth;
110  auth = [GTMOAuth2WindowController authForGoogleFromKeychainForName:kKeychainItemName
111                                                            clientID:clientID
112                                                        clientSecret:clientSecret];
113  [[self youTubeService] setAuthorizer:auth];
114
115  // Set the result text fields to have a distinctive color and mono-spaced font
116  // to aid in understanding of each album and photo query operation.
117  [mEntriesResultTextField setTextColor:[NSColor darkGrayColor]];
118
119  NSFont *resultTextFont = [NSFont fontWithName:@"Monaco" size:9];
120  [mEntriesResultTextField setFont:resultTextFont];
121
122  // load the user feed types into the pop-up menu, and default to showing
123  // the feed of the user's uploads, as it's generally most interesting
124  NSArray *userFeedTypes = [NSArray arrayWithObjects:
125    kChannelsFeed,
126    kMostPopularFeed,
127    kGDataYouTubeUserFeedIDContacts,
128    kGDataYouTubeUserFeedIDFavorites,
129    kGDataYouTubeUserFeedIDInbox,
130    kGDataYouTubeUserFeedIDPlaylists,
131    kGDataYouTubeUserFeedIDSubscriptions,
132    kActivityFeed,
133    kGDataYouTubeUserFeedIDFriendsActivity,
134    kGDataYouTubeUserFeedIDUploads,
135    nil];
136
137  [mUserFeedPopup removeAllItems];
138  [mUserFeedPopup addItemsWithTitles:userFeedTypes];
139  [mUserFeedPopup selectItemWithTitle:kGDataYouTubeUserFeedIDUploads];
140
141  // reset the upload file path
142  [mFilePathField setStringValue:@""];
143
144  [self updateUI];
145
146  // start retrieving the list of assignable upload categories
147  [self fetchStandardCategories];
148}
149
150- (void)dealloc {
151  [mEntriesFeed release];
152  [mEntriesFetchError release];
153  [mEntriesFetchTicket release];
154  [mEntryImageURLString release];
155
156  [mUploadTicket release];
157  [mUploadLocationURL release];
158
159  [super dealloc];
160}
161
162#pragma mark -
163
164- (NSString *)signedInUsername {
165  // Get the email address of the signed-in user
166  GTMOAuth2Authentication *auth = [[self youTubeService] authorizer];
167  BOOL isSignedIn = auth.canAuthorize;
168  if (isSignedIn) {
169    return auth.userEmail;
170  } else {
171    return nil;
172  }
173}
174
175- (BOOL)isSignedIn {
176  NSString *name = [self signedInUsername];
177  return (name != nil);
178}
179
180- (void)runSigninThenInvokeSelector:(SEL)signInDoneSel {
181  // Applications should have client ID and client secret strings
182  // hardcoded into the source, but the sample application asks the
183  // developer for the strings
184  NSString *clientID = [mClientIDField stringValue];
185  NSString *clientSecret = [mClientSecretField stringValue];
186
187  if ([clientID length] == 0 || [clientSecret length] == 0) {
188    // Remind the developer that client ID and client secret are needed
189    [mClientIDButton performSelector:@selector(performClick:)
190                          withObject:self
191                          afterDelay:0.5];
192    return;
193  }
194
195  // Show the OAuth 2 sign-in controller
196  NSString *scope = [GDataServiceGoogleYouTube authorizationScope];
197
198  NSBundle *frameworkBundle = [NSBundle bundleForClass:[GTMOAuth2WindowController class]];
199  GTMOAuth2WindowController *windowController;
200  windowController = [GTMOAuth2WindowController controllerWithScope:scope
201                                                           clientID:clientID
202                                                       clientSecret:clientSecret
203                                                   keychainItemName:kKeychainItemName
204                                                     resourceBundle:frameworkBundle];
205  
206  [windowController setUserData:NSStringFromSelector(signInDoneSel)];
207  [windowController signInSheetModalForWindow:[self window]
208                                     delegate:self
209                             finishedSelector:@selector(windowController:finishedWithAuth:error:)];
210}
211
212- (void)windowController:(GTMOAuth2WindowController *)windowController
213        finishedWithAuth:(GTMOAuth2Authentication *)auth
214                   error:(NSError *)error {
215  // Callback from OAuth 2 sign-in
216  if (error == nil) {
217    [[self youTubeService] setAuthorizer:auth];
218
219    NSString *selStr = [windowController userData];
220    if (selStr) {
221      [self performSelector:NSSelectorFromString(selStr)];
222    }
223  } else {
224    [self setEntriesFetchError:error];
225    [self updateUI];
226  }
227}
228
229#pragma mark -
230
231// album and photo thumbnail display
232
233// fetch or clear the thumbnail for this specified entry
234- (void)updateImageForEntry:(GDataEntryBase *)entry {
235
236  if (!entry || ![entry respondsToSelector:@selector(mediaGroup)]) {
237
238    // clear the image; no entry is selected, or it's not an entry type with a
239    // thumbnail
240    [mEntryImageView setImage:nil];
241    [self setEntryImageURLString:nil];
242
243  } else {
244    // if the new thumbnail URL string is different from the previous one,
245    // save the new URL, clear the existing image and fetch the new image
246    GDataEntryYouTubeVideo *video = (GDataEntryYouTubeVideo *)entry;
247
248    GDataMediaThumbnail *thumbnail = [[video mediaGroup] highQualityThumbnail];
249    if (thumbnail != nil) {
250      NSString *imageURLString = [thumbnail URLString];
251      if (!imageURLString || ![mEntryImageURLString isEqual:imageURLString]) {
252
253        [self setEntryImageURLString:imageURLString];
254        [mEntryImageView setImage:nil];
255
256        if (imageURLString) {
257          [self fetchEntryImageURLString:imageURLString];
258        }
259      }
260    }
261  }
262}
263
264- (void)fetchEntryImageURLString:(NSString *)urlString {
265  GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithURLString:urlString];
266  [fetcher setComment:@"thumbnail"];
267  [fetcher beginFetchWithDelegate:self
268                didFinishSelector:@selector(imageFetcher:finishedWithData:error:)];
269}
270
271- (void)imageFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error {
272  if (error == nil) {
273    // got the data; display it in the image view
274    NSImage *image = [[[NSImage alloc] initWithData:data] autorelease];
275
276    [mEntryImageView setImage:image];
277  } else {
278    NSLog(@"imageFetcher:%@ failedWithError:%@", fetcher,  error);
279  }
280}
281
282#pragma mark -
283
284- (void)updateUI {
285  BOOL isSignedIn = [self isSignedIn];
286  NSString *username = [self signedInUsername];
287  [mSignedInButton setTitle:(isSignedIn ? @"Sign Out" : @"Sign In")];
288  [mSignedInField setStringValue:(isSignedIn ? username : @"No")];
289
290  // entry list display
291  [mEntriesTable reloadData];
292
293  if (mEntriesFetchTicket != nil) {
294    [mEntriesProgressIndicator startAnimation:self];
295  } else {
296    [mEntriesProgressIndicator stopAnimation:self];
297  }
298
299  // entry fetch result or selected item
300  NSString *entriesResultStr = @"";
301  if (mEntriesFetchError) {
302    entriesResultStr = [mEntriesFetchError description];
303    [self updateImageForEntry:nil];
304  } else {
305    GDataEntryBase *entry = [self selectedEntry];
306    if (entry) {
307      entriesResultStr = [entry description];
308    }
309    // fetch or clear the entry thumbnail
310    [self updateImageForEntry:entry];
311  }
312  [mEntriesResultTextField setString:entriesResultStr];
313
314  // show how many entries are in the table
315  NSString *countStr = @"Entries: -";
316  if (mEntriesFetchTicket == nil) {
317    // not currently fetching; show the count
318    unsigned int numEntries = [[mEntriesFeed entries] count];
319    countStr = [NSString stringWithFormat:@"Entries: %u", numEntries];
320  }
321  [mEntryCountField setStringValue:countStr];
322
323  // enable the upload button only if all preconditions are met
324  BOOL hasDevKey = [[mDeveloperKeyField stringValue] length] > 0;
325  BOOL hasTitle = [[mTitleField stringValue] length] > 0;
326  BOOL hasPath = [[mFilePathField stringValue] length] > 0;
327
328  BOOL canUpload = isSignedIn && hasDevKey
329    && hasTitle && hasPath;
330
331  BOOL isUploading = (mUploadTicket != nil);
332  BOOL canRestartUpload = !isUploading && (mUploadLocationURL != nil);
333  BOOL isUploadPaused = [mUploadTicket isUploadPaused];
334
335  [mUploadButton setEnabled:(canUpload && !isUploading)];
336  [mPauseUploadButton setEnabled:isUploading];
337  [mStopUploadButton setEnabled:isUploading];
338  [mRestartUploadButton setEnabled:canRestartUpload];
339
340  NSString *pauseTitle = (isUploadPaused ? @"Resume" : @"Pause");
341  [mPauseUploadButton setTitle:pauseTitle];
342
343  // Show or hide the text indicating that the client ID or client secret are
344  // needed
345  BOOL hasClientIDStrings = [[mClientIDField stringValue] length] > 0
346    && [[mClientSecretField stringValue] length] > 0;
347  [mClientIDRequiredTextField setHidden:hasClientIDStrings];
348}
349
350- (void)displayAlert:(NSString *)title format:(NSString *)format, ... {
351  NSString *result = format;
352  if (format) {
353    va_list argList;
354    va_start(argList, format);
355    result = [[[NSString alloc] initWithFormat:format
356                                     arguments:argList] autorelease];
357    va_end(argList);
358  }
359  NSBeginAlertSheet(title, nil, nil, nil, [self window], nil, nil,
360                    nil, nil, @"%@", result);
361}
362
363#pragma mark IBActions
364
365- (IBAction)signInClicked:(id)sender {
366  if (![self isSignedIn]) {
367    // Sign in
368    [self runSigninThenInvokeSelector:@selector(updateUI)];
369  } else {
370    // Sign out
371    GDataServiceGoogleYouTube *service = [self youTubeService];
372
373    [GTMOAuth2WindowController removeAuthFromKeychainForName:kKeychainItemName];
374    [service setAuthorizer:nil];
375    [self updateUI];
376  }
377}
378
379- (IBAction)getEntriesClicked:(id)sender {
380  if (![self isSignedIn]) {
381    // Sign in
382    [self runSigninThenInvokeSelector:@selector(fetchAllEntries)];
383  } else {
384    [self fetchAllEntries];
385  }
386}
387
388- (IBAction)cancelEntriesFetchClicked:(id)sender {
389  [mEntriesFetchTicket cancelTicket];
390  [self setEntriesFetchTicket:nil];
391  [self updateUI];
392}
393
394- (IBAction)APIConsoleClicked:(id)sender {
395  NSURL *url = [NSURL URLWithString:@"https://code.google.com/apis/console"];
396  [[NSWorkspace sharedWorkspace] openURL:url];
397}
398
399- (IBAction)loggingCheckboxClicked:(id)sender {
400  [GTMHTTPFetcher setLoggingEnabled:[sender state]];
401}
402
403- (IBAction)chooseFileClicked:(id)sender {
404  // ask the user to choose a video file
405  NSOpenPanel *openPanel = [NSOpenPanel openPanel];
406  [openPanel setPrompt:@"Choose"];
407
408  NSArray *movieTypes = [NSArray arrayWithObjects:@"mov", @"mp4", nil];
409
410  [openPanel setAllowedFileTypes:movieTypes];
411  [openPanel beginSheetModalForWindow:[self window]
412                    completionHandler:^(NSInteger result) {
413                      // callback
414                      if (result == NSOKButton) {
415                        // the user chose a file
416                        NSString *path = [[openPanel URL] path];
417
418                        [mFilePathField setStringValue:path];
419
420                        [self updateUI]; // update UI in case we need to enable the upload button
421                      }
422                    }];
423}
424
425- (IBAction)uploadClicked:(id)sender {
426
427  [self uploadVideoFile];
428}
429
430- (IBAction)pauseUploadClicked:(id)sender {
431  if ([mUploadTicket isUploadPaused]) {
432    // Resume from pause
433    [mUploadTicket resumeUpload];
434  } else {
435    // Pause
436    [mUploadTicket pauseUpload];
437  }
438
439  [self updateUI];
440}
441
442- (IBAction)stopUploadClicked:(id)sender {
443  [mUploadTicket cancelTicket];
444  [self setUploadTicket:nil];
445
446  [mUploadProgressIndicator setDoubleValue:0.0];
447  [self updateUI];
448}
449
450- (IBAction)restartUploadClicked:(id)sender {
451  [self restartUpload];
452}
453
454#pragma mark -
455
456// get a YouTube service object
457//
458// A "service" object handles networking tasks.  Service objects
459// contain user authentication information as well as networking
460// state information (such as cookies and the "last modified" date for
461// fetched data.)
462
463- (GDataServiceGoogleYouTube *)youTubeService {
464
465  static GDataServiceGoogleYouTube* service = nil;
466
467  if (!service) {
468    service = [[GDataServiceGoogleYouTube alloc] init];
469
470    [service setShouldCacheResponseData:YES];
471    [service setServiceShouldFollowNextLinks:YES];
472    [service setIsServiceRetryEnabled:YES];
473  }
474
475  NSString *devKey = [mDeveloperKeyField stringValue];
476  [service setYouTubeDeveloperKey:devKey];
477
478  return service;
479}
480
481// get the entry selected in the list, or nil if none
482- (GDataEntryBase *)selectedEntry {
483
484  NSArray *entries = [mEntriesFeed entries];
485  int rowIndex = [mEntriesTable selectedRow];
486  if ([entries count] > 0 && rowIndex > -1) {
487
488    GDataEntryBase *entry = [entries objectAtIndex:rowIndex];
489    return entry;
490  }
491  return nil;
492}
493
494#pragma mark Fetch all entries
495
496// begin retrieving the list of the user's entries
497- (void)fetchAllEntries {
498
499  [self setEntriesFeed:nil];
500  [self setEntriesFetchError:nil];
501  [self setEntriesFetchTicket:nil];
502
503  GDataServiceGoogleYouTube *service = [self youTubeService];
504  GDataServiceTicket *ticket;
505
506  // feedID is uploads, favorites, etc
507  //
508  // note that activity feeds require a developer key
509  NSString *feedID = [[mUserFeedPopup selectedItem] title];
510
511  NSURL *feedURL;
512  if ([feedID isEqual:kActivityFeed]) {
513    // the activity feed uses a unique URL
514    feedURL = [GDataServiceGoogleYouTube youTubeActivityFeedURLForUserID:kGDataServiceDefaultUser];
515  } else if ([feedID isEqual:kChannelsFeed]) {
516    feedURL = [GDataServiceGoogleYouTube youTubeURLForChannelsFeeds];
517  } else if ([feedID isEqual:kMostPopularFeed]) {
518    feedURL = [GDataServiceGoogleYouTube youTubeURLForFeedID:kGDataYouTubeFeedIDMostPopular];
519  } else {
520    feedURL = [GDataServiceGoogleYouTube youTubeURLForUserID:kGDataServiceDefaultUser
521                                                  userFeedID:feedID];
522  }
523
524  ticket = [service fetchFeedWithURL:feedURL
525                            delegate:self
526                   didFinishSelector:@selector(entryListFetchTicket:finishedWithFeed:error:)];
527
528  if ([feedID isEqual:kChannelsFeed] || [feedID isEqual:kMostPopularFeed]) {
529    // when using feeds which search all public videos, we don't want
530    // to follow the feed's next links, since there could be a huge
531    // number of pages of results
532    [ticket setShouldFollowNextLinks:NO];
533  }
534
535  [self setEntriesFetchTicket:ticket];
536
537  [self updateUI];
538}
539
540// feed fetch callback
541- (void)entryListFetchTicket:(GDataServiceTicket *)ticket
542            finishedWithFeed:(GDataFeedYouTubeVideo *)feed
543                       error:(NSError *)error {
544
545  [self setEntriesFeed:feed];
546  [self setEntriesFetchError:error];
547  [self setEntriesFetchTicket:nil];
548
549  [self updateUI];
550}
551
552#pragma mark -
553
554- (void)uploadVideoFile {
555
556  NSString *devKey = [mDeveloperKeyField stringValue];
557
558  GDataServiceGoogleYouTube *service = [self youTubeService];
559  [service setYouTubeDeveloperKey:devKey];
560
561  NSURL *url = [GDataServiceGoogleYouTube youTubeUploadURLForUserID:kGDataServiceDefaultUser];
562
563  // load the file data
564  NSString *path = [mFilePathField stringValue];
565  NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:path];
566  NSString *filename = [path lastPathComponent];
567
568  // gather all the metadata needed for the mediaGroup
569  NSString *titleStr = [mTitleField stringValue];
570  GDataMediaTitle *title = [GDataMediaTitle textConstructWithString:titleStr];
571
572  NSString *categoryStr = [[mCategoryPopup selectedItem] representedObject];
573  GDataMediaCategory *category = [GDataMediaCategory mediaCategoryWithString:categoryStr];
574  [category setScheme:kGDataSchemeYouTubeCategory];
575
576  NSString *descStr = [mDescriptionField stringValue];
577  GDataMediaDescription *desc = [GDataMediaDescription textConstructWithString:descStr];
578
579  NSString *keywordsStr = [mKeywordsField stringValue];
580  GDataMediaKeywords *keywords = [GDataMediaKeywords keywordsWithString:keywordsStr];
581
582  BOOL isPrivate = ([mPrivateCheckbox state] == NSOnState);
583
584  GDataYouTubeMediaGroup *mediaGroup = [GDataYouTubeMediaGroup mediaGroup];
585  [mediaGroup setMediaTitle:title];
586  [mediaGroup setMediaDescription:desc];
587  [mediaGroup addMediaCategory:category];
588  [mediaGroup setMediaKeywords:keywords];
589  [mediaGroup setIsPrivate:isPrivate];
590
591  NSString *mimeType = [GDataUtilities MIMETypeForFileAtPath:path
592                                             defaultMIMEType:@"video/mp4"];
593
594  // create the upload entry with the mediaGroup and the file
595  GDataEntryYouTubeUpload *entry;
596  entry = [GDataEntryYouTubeUpload uploadEntryWithMediaGroup:mediaGroup
597                                                  fileHandle:fileHandle
598                                                    MIMEType:mimeType
599                                                        slug:filename];
600
601  SEL progressSel = @selector(ticket:hasDeliveredByteCount:ofTotalByteCount:);
602  [service setServiceUploadProgressSelector:progressSel];
603
604  GDataServiceTicket *ticket;
605  ticket = [service fetchEntryByInsertingEntry:entry
606                                    forFeedURL:url
607                                      delegate:self
608                             didFinishSelector:@selector(uploadTicket:finishedWithEntry:error:)];
609  [self setUploadTicket:ticket];
610
611  // To allow restarting after stopping, we need to track the upload location
612  // URL. The location URL will be a different address than the upload URL that
613  // is used to start a new upload.
614  //
615  // For compatibility with systems that do not support Objective-C blocks
616  // (iOS 3 and Mac OS X 10.5), the location URL may also be obtained in the
617  // progress callback as ((GTMHTTPUploadFetcher *)[ticket objectFetcher]).locationURL
618  // 
619  GTMHTTPUploadFetcher *uploadFetcher = (GTMHTTPUploadFetcher *)[ticket objectFetcher];
620  [uploadFetcher setLocationChangeBlock:^(NSURL *url) {
621    [self setUploadLocationURL:url];
622    [self updateUI];
623  }];
624
625  [self updateUI];
626}
627
628- (void)restartUpload {
629  // Restart a stopped upload, using the location URL from the previous
630  // upload attempt
631  if (mUploadLocationURL == nil) return;
632
633  GDataServiceGoogleYouTube *service = [self youTubeService];
634
635  NSString *path = [mFilePathField stringValue];
636  NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:path];
637  NSString *mimeType = [GDataUtilities MIMETypeForFileAtPath:path
638                                             defaultMIMEType:@"video/mp4"];
639
640  GDataEntryYouTubeUpload *entry;
641  entry = [GDataEntryYouTubeUpload uploadEntryWithMediaGroup:nil
642                                                  fileHandle:fileHandle
643                                                    MIMEType:mimeType
644                                                        slug:nil];
645  [entry setUploadLocationURL:mUploadLocationURL];
646
647  SEL progressSel = @selector(ticket:hasDeliveredByteCount:ofTotalByteCount:);
648  [service setServiceUploadProgressSelector:progressSel];
649
650  GDataServiceTicket *ticket;
651  ticket = [service fetchEntryByInsertingEntry:entry
652                                    forFeedURL:nil
653                                      delegate:self
654                             didFinishSelector:@selector(uploadTicket:finishedWithEntry:error:)];
655  [self setUploadTicket:ticket];
656
657  // To allow restarting after stopping, we need to track the upload location
658  // URL.
659  GTMHTTPUploadFetcher *uploadFetcher = (GTMHTTPUploadFetcher *)[ticket objectFetcher];
660  [uploadFetcher setLocationChangeBlock:^(NSURL *url) {
661    [self setUploadLocationURL:url];
662    [self updateUI];
663  }];
664
665  [self updateUI];
666}
667
668// progress callback
669- (void)ticket:(GDataServiceTicket *)ticket
670   hasDeliveredByteCount:(unsigned long long)numberOfBytesRead
671   ofTotalByteCount:(unsigned long long)dataLength {
672  [mUploadProgressIndicator setMinValue:0.0];
673  [mUploadProgressIndicator setMaxValue:(double)dataLength];
674  [mUploadProgressIndicator setDoubleValue:(double)numberOfBytesRead];
675}
676
677// upload callback
678- (void)uploadTicket:(GDataServiceTicket *)ticket
679   finishedWithEntry:(GDataEntryYouTubeVideo *)videoEntry
680               error:(NSError *)error {
681  if (error == nil) {
682    // tell the user that the add worked
683    [self displayAlert:@"Uploaded"
684                format:@"Uploaded video: %@",
685     [[videoEntry title] stringValue]];
686
687    // refetch the current entries, in case the list of uploads
688    // has changed
689    [self fetchAllEntries];
690  } else {
691    [self displayAlert:@"Upload failed"
692                format:@"Upload failed: %@", error];
693  }
694  [mUploadProgressIndicator setDoubleValue:0.0];
695
696  [self setUploadTicket:nil];
697  [self updateUI];
698}
699
700// Setting likes/dislikes
701//
702// To set the authenticated user's rating for a video entry, insert an entry
703// into the ratings feed for the video. The value may be
704// kGDataYouTubeRatingValueLike or kGDataYouTubeRatingValueDislike
705//
706// Example:
707//
708//  - (void)setLikesValue:(NSString *)value
709//          forVideoEntry:(GDataEntryYouTubeVideo *)videoEntry {
710//
711//    GDataEntryYouTubeRating *ratingEntry = [GDataEntryYouTubeRating ratingEntryWithValue:value];
712//
713//    GDataServiceGoogleYouTube *service = [self youTubeService];
714//    [service fetchEntryByInsertingEntry:ratingEntry
715//                             forFeedURL:[[videoEntry ratingsLink] URL]
716//                               delegate:self
717//                      didFinishSelector:@selector(likesTicket:finishedWithEntry:error:)];
718//  }
719
720#pragma mark Client ID Sheet
721
722// Client ID and Client Secret Sheet
723//
724// Sample apps need this sheet to ask for the client ID and client secret
725// strings
726//
727// Your application will just hardcode the client ID and client secret strings
728// into the source rather than ask the user for them.
729//
730// The string values are obtained from the API Console,
731// https://code.google.com/apis/console
732
733- (IBAction)clientIDClicked:(id)sender {
734  // Show the sheet for developers to enter their client ID and client secret
735  [NSApp beginSheet:mClientIDSheet
736     modalForWindow:[self window]
737      modalDelegate:self
738     didEndSelector:@selector(clientIDSheetDidEnd:returnCode:contextInfo:)
739        contextInfo:NULL];
740}
741
742- (IBAction)clientIDDoneClicked:(id)sender {
743  [NSApp endSheet:mClientIDSheet returnCode:NSOKButton];
744}
745
746- (void)clientIDSheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
747  [sheet orderOut:self];
748  [self updateUI];
749}
750
751#pragma mark Text field delegate methods
752
753- (void)controlTextDidChange:(NSNotification *)note {
754
755  [self updateUI]; // enable/disable the upload button
756}
757
758
759#pragma mark TableView delegate methods
760//
761// table view delegate methods
762//
763
764- (void)tableViewSelectionDidChange:(NSNotification *)notification {
765
766  [self updateUI];
767}
768
769// table view data source methods
770- (int)numberOfRowsInTableView:(NSTableView *)tableView {
771  return [[mEntriesFeed entries] count];
772}
773
774- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row {
775  // get the entry entry's title
776  GDataEntryBase *entry = [[mEntriesFeed entries] objectAtIndex:row];
777  return [[entry title] stringValue];
778}
779
780#pragma mark Fetch the Categories
781
782- (void)fetchStandardCategories {
783
784  // This method initiates a fetch and parse of the assignable categories.
785  // If successful, the callback loads the category pop-up with the
786  // categories.
787
788  NSURL *categoriesURL = [NSURL URLWithString:kGDataSchemeYouTubeCategory];
789  GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithURL:categoriesURL];
790  [fetcher setComment:@"YouTube categories"];
791  [fetcher beginFetchWithDelegate:self
792                didFinishSelector:@selector(categoryFetcher:finishedWithData:error:)];
793}
794
795
796- (void)categoryFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error {
797  if (error) {
798    NSLog(@"categoryFetcher:%@ failedWithError:%@", fetcher, error);
799    return;
800  }
801
802  // The categories document looks like
803  //  <app:categories>
804  //    <atom:category term='Film' label='Film &amp; Animation'>
805  //      <yt:browsable />
806  //      <yt:assignable />
807  //    </atom:category>
808  //  </app:categories>
809  //
810  // We only want the categories which are assignable. We'll use XPath to
811  // select those, then get the string value of the resulting term attribute
812  // nodes.
813
814  NSString *const path = @"app:categories/atom:category[yt:assignable]";
815
816  NSXMLDocument *xmlDoc = [[[NSXMLDocument alloc] initWithData:data
817                                                       options:0
818                                                         error:&error] autorelease];
819  if (xmlDoc == nil) {
820    NSLog(@"category fetch could not parse XML: %@", error);
821  } else {
822    NSArray *nodes = [xmlDoc nodesForXPath:path
823                                     error:&error];
824    unsigned int numberOfNodes = [nodes count];
825    if (numberOfNodes == 0) {
826      NSLog(@"category fetch could not find nodes: %@", error);
827    } else {
828
829      // add the category labels as menu items, and the category terms as
830      // the menu item representedObjects.
831      [mCategoryPopup removeAllItems];
832      NSMenu *menu = [mCategoryPopup menu];
833
834      for (int idx = 0; idx < numberOfNodes; idx++) {
835        NSXMLElement *category = [nodes objectAtIndex:idx];
836
837        NSString *term = [[category attributeForName:@"term"] stringValue];
838        NSString *label = [[category attributeForName:@"label"] stringValue];
839
840        if (label == nil) label = term;
841
842        NSMenuItem *item = [menu addItemWithTitle:label
843                                           action:nil
844                                    keyEquivalent:@""];
845        [item setRepresentedObject:term];
846      }
847    }
848  }
849}
850
851#pragma mark Setters and Getters
852
853- (GDataFeedYouTubeVideo *)entriesFeed {
854  return mEntriesFeed;
855}
856
857- (void)setEntriesFeed:(GDataFeedYouTubeVideo *)feed {
858  [mEntriesFeed autorelease];
859  mEntriesFeed = [feed retain];
860}
861
862- (NSError *)entryFetchError {
863  return mEntriesFetchError;
864}
865
866- (void)setEntriesFetchError:(NSError *)error {
867  [mEntriesFetchError release];
868  mEntriesFetchError = [error retain];
869}
870
871- (GDataServiceTicket *)entriesFetchTicket {
872  return mEntriesFetchTicket;
873}
874
875- (void)setEntriesFetchTicket:(GDataServiceTicket *)ticket {
876  [mEntriesFetchTicket release];
877  mEntriesFetchTicket = [ticket retain];
878}
879
880- (NSString *)entryImageURLString {
881  return mEntryImageURLString;
882}
883
884- (void)setEntryImageURLString:(NSString *)str {
885  [mEntryImageURLString autorelease];
886  mEntryImageURLString = [str copy];
887}
888
889- (GDataServiceTicket *)uploadTicket {
890  return mUploadTicket;
891}
892
893- (void)setUploadTicket:(GDataServiceTicket *)ticket {
894  [mUploadTicket release];
895  mUploadTicket = [ticket retain];
896}
897
898- (NSURL *)uploadLocationURL {
899  return mUploadLocationURL;
900}
901
902- (void)setUploadLocationURL:(NSURL *)url {
903  [mUploadLocationURL release];
904  mUploadLocationURL = [url retain];
905}
906
907@end