1 /*
2  * Copyright (C) 2010, 2011 Igalia S.L.
3  * Copyright (C) 2011 Intel Corporation.
4  *
5  * Contact: Iago Toral Quiroga <itoral@igalia.com>
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public License
9  * as published by the Free Software Foundation; version 2.1 of
10  * the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
20  * 02110-1301 USA
21  *
22  */
23 
24 #ifdef HAVE_CONFIG_H
25 #include "config.h"
26 #endif
27 
28 #include <grilo.h>
29 #include <glib/gi18n-lib.h>
30 #include <net/grl-net.h>
31 #include <gdata/gdata.h>
32 #include <totem-pl-parser.h>
33 #include <string.h>
34 
35 #include "grl-youtube.h"
36 
37 enum {
38   PROP_0,
39   PROP_SERVICE,
40 };
41 
42 /* --------- Logging  -------- */
43 
44 #define GRL_LOG_DOMAIN_DEFAULT youtube_log_domain
45 GRL_LOG_DOMAIN_STATIC(youtube_log_domain);
46 
47 /* ----- Root categories ---- */
48 
49 #define YOUTUBE_ROOT_NAME       "YouTube"
50 
51 #define ROOT_DIR_FEEDS_INDEX      0
52 #define ROOT_DIR_CATEGORIES_INDEX 1
53 
54 #define YOUTUBE_FEEDS_ID        "standard-feeds"
55 #define YOUTUBE_FEEDS_NAME      N_("Standard feeds")
56 
57 #define YOUTUBE_CATEGORIES_ID   "categories"
58 #define YOUTUBE_CATEGORIES_NAME N_("Categories")
59 #define YOUTUBE_CATEGORIES_URL  "https://gdata.youtube.com/schemas/2007/categories.cat"
60 
61 /* ----- Feeds categories ---- */
62 
63 #define YOUTUBE_TOP_RATED_ID         (YOUTUBE_FEEDS_ID "/0")
64 #define YOUTUBE_TOP_RATED_NAME       N_("Top Rated")
65 
66 #define YOUTUBE_TOP_FAVS_ID          (YOUTUBE_FEEDS_ID "/1")
67 #define YOUTUBE_TOP_FAVS_NAME        N_("Top Favorites")
68 
69 #define YOUTUBE_MOST_VIEWED_ID       (YOUTUBE_FEEDS_ID "/2")
70 #define YOUTUBE_MOST_VIEWED_NAME     N_("Most Viewed")
71 
72 #define YOUTUBE_MOST_POPULAR_ID      (YOUTUBE_FEEDS_ID "/3")
73 #define YOUTUBE_MOST_POPULAR_NAME    N_("Most Popular")
74 
75 #define YOUTUBE_MOST_RECENT_ID       (YOUTUBE_FEEDS_ID "/4")
76 #define YOUTUBE_MOST_RECENT_NAME     N_("Most Recent")
77 
78 #define YOUTUBE_MOST_DISCUSSED_ID    (YOUTUBE_FEEDS_ID "/5")
79 #define YOUTUBE_MOST_DISCUSSED_NAME  N_("Most Discussed")
80 
81 #define YOUTUBE_MOST_LINKED_ID       (YOUTUBE_FEEDS_ID "/6")
82 #define YOUTUBE_MOST_LINKED_NAME     N_("Most Linked")
83 
84 #define YOUTUBE_MOST_RESPONDED_ID    (YOUTUBE_FEEDS_ID "/7")
85 #define YOUTUBE_MOST_RESPONDED_NAME  N_("Most Responded")
86 
87 #define YOUTUBE_FEATURED_ID          (YOUTUBE_FEEDS_ID "/8")
88 #define YOUTUBE_FEATURED_NAME        N_("Recently Featured")
89 
90 #define YOUTUBE_MOBILE_ID            (YOUTUBE_FEEDS_ID "/9")
91 #define YOUTUBE_MOBILE_NAME          N_("Watch On Mobile")
92 
93 /* --- Other --- */
94 
95 #define YOUTUBE_MAX_CHUNK       50
96 
97 #define YOUTUBE_VIDEO_INFO_URL  "https://www.youtube.com/get_video_info?video_id=%s"
98 #define YOUTUBE_VIDEO_URL       "https://www.youtube.com/get_video?video_id=%s&t=%s&asv="
99 #define YOUTUBE_CATEGORY_URL    "https://gdata.youtube.com/feeds/api/videos/-/%s?&start-index=%s&max-results=%s"
100 #define YOUTUBE_WATCH_URL       "https://www.youtube.com/watch?v="
101 
102 #define YOUTUBE_VIDEO_MIME      "application/x-shockwave-flash"
103 #define YOUTUBE_SITE_URL        "www.youtube.com"
104 
105 
106 /* --- Plugin information --- */
107 
108 #define SOURCE_ID   "grl-youtube"
109 #define SOURCE_NAME "YouTube"
110 #define SOURCE_DESC _("A source for browsing and searching YouTube videos")
111 
112 /* --- Data types --- */
113 
114 typedef void (*AsyncReadCbFunc) (gchar *data, gpointer user_data);
115 
116 typedef void (*BuildMediaFromEntryCbFunc) (GrlMedia *media, gpointer user_data);
117 
118 typedef struct {
119   const gchar *id;
120   const gchar *name;
121   guint count;
122 } CategoryInfo;
123 
124 typedef struct {
125   GrlSource *source;
126   GCancellable *cancellable;
127   guint operation_id;
128   const gchar *container_id;
129   GList *keys;
130   GrlResolutionFlags flags;
131   guint skip;
132   guint count;
133   GrlSourceResultCb callback;
134   gpointer user_data;
135   guint error_code;
136   CategoryInfo *category_info;
137   guint emitted;
138   guint matches;
139   gint ref_count;
140 } OperationSpec;
141 
142 typedef struct {
143   GrlSource *source;
144   GSourceFunc callback;
145   gpointer user_data;
146 } BuildCategorySpec;
147 
148 typedef struct {
149   AsyncReadCbFunc callback;
150   gchar *url;
151   gpointer user_data;
152 } AsyncReadCb;
153 
154 typedef struct {
155   GrlMedia *media;
156   GCancellable *cancellable;
157   BuildMediaFromEntryCbFunc callback;
158   gpointer user_data;
159 } SetMediaUrlAsyncReadCb;
160 
161 typedef enum {
162   YOUTUBE_MEDIA_TYPE_ROOT,
163   YOUTUBE_MEDIA_TYPE_FEEDS,
164   YOUTUBE_MEDIA_TYPE_CATEGORIES,
165   YOUTUBE_MEDIA_TYPE_FEED,
166   YOUTUBE_MEDIA_TYPE_CATEGORY,
167   YOUTUBE_MEDIA_TYPE_VIDEO,
168 } YoutubeMediaType;
169 
170 struct _GrlYoutubeSourcePriv {
171   GDataService *service;
172 
173   GrlNetWc *wc;
174 };
175 
176 #define YOUTUBE_CLIENT_ID "grilo"
177 
178 static GrlYoutubeSource *grl_youtube_source_new (const gchar *api_key,
179 						 const gchar *client_id,
180 						 const gchar *format);
181 
182 static void grl_youtube_source_set_property (GObject *object,
183                                              guint propid,
184                                              const GValue *value,
185                                              GParamSpec *pspec);
186 static void grl_youtube_source_finalize (GObject *object);
187 
188 gboolean grl_youtube_plugin_init (GrlRegistry *registry,
189                                   GrlPlugin *plugin,
190                                   GList *configs);
191 
192 static const GList *grl_youtube_source_supported_keys (GrlSource *source);
193 
194 static const GList *grl_youtube_source_slow_keys (GrlSource *source);
195 
196 static void grl_youtube_source_search (GrlSource *source,
197                                        GrlSourceSearchSpec *ss);
198 
199 static void grl_youtube_source_browse (GrlSource *source,
200                                        GrlSourceBrowseSpec *bs);
201 
202 static void grl_youtube_source_resolve (GrlSource *source,
203                                         GrlSourceResolveSpec *rs);
204 
205 static gboolean grl_youtube_test_media_from_uri (GrlSource *source,
206 						 const gchar *uri);
207 
208 static void grl_youtube_get_media_from_uri (GrlSource *source,
209 					    GrlSourceMediaFromUriSpec *mfus);
210 
211 static void grl_youtube_source_cancel (GrlSource *source,
212                                        guint operation_id);
213 
214 static void produce_from_directory (CategoryInfo *dir, guint dir_size, OperationSpec *os);
215 
216 /* ==================== Global Data  ================= */
217 
218 guint root_dir_size = 2;
219 CategoryInfo root_dir[] = {
220   {YOUTUBE_FEEDS_ID,      YOUTUBE_FEEDS_NAME,      10},
221   {YOUTUBE_CATEGORIES_ID, YOUTUBE_CATEGORIES_NAME, -1},
222   {NULL, NULL, 0}
223 };
224 
225 CategoryInfo feeds_dir[] = {
226   {YOUTUBE_TOP_RATED_ID,      YOUTUBE_TOP_RATED_NAME,       -1},
227   {YOUTUBE_TOP_FAVS_ID,       YOUTUBE_TOP_FAVS_NAME,        -1},
228   {YOUTUBE_MOST_VIEWED_ID,    YOUTUBE_MOST_VIEWED_NAME,     -1},
229   {YOUTUBE_MOST_POPULAR_ID,   YOUTUBE_MOST_POPULAR_NAME,    -1},
230   {YOUTUBE_MOST_RECENT_ID,    YOUTUBE_MOST_RECENT_NAME,     -1},
231   {YOUTUBE_MOST_DISCUSSED_ID, YOUTUBE_MOST_DISCUSSED_NAME,  -1},
232   {YOUTUBE_MOST_LINKED_ID,    YOUTUBE_MOST_LINKED_NAME,     -1},
233   {YOUTUBE_MOST_RESPONDED_ID, YOUTUBE_MOST_RESPONDED_NAME,  -1},
234   {YOUTUBE_FEATURED_ID,       YOUTUBE_FEATURED_NAME,        -1},
235   {YOUTUBE_MOBILE_ID,         YOUTUBE_MOBILE_NAME,          -1},
236   {NULL, NULL, 0}
237 };
238 
239 CategoryInfo *categories_dir = NULL;
240 
241 static GrlYoutubeSource *ytsrc = NULL;
242 
243 /* =================== YouTube Plugin  =============== */
244 
245 gboolean
grl_youtube_plugin_init(GrlRegistry * registry,GrlPlugin * plugin,GList * configs)246 grl_youtube_plugin_init (GrlRegistry *registry,
247                          GrlPlugin *plugin,
248                          GList *configs)
249 {
250   gchar *api_key;
251   gchar *format;
252   GrlConfig *config;
253   gint config_count;
254   GrlYoutubeSource *source;
255 
256   GRL_LOG_DOMAIN_INIT (youtube_log_domain, "youtube");
257 
258   GRL_DEBUG ("youtube_plugin_init");
259 
260   /* Initialize i18n */
261   bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
262   bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
263 
264   if (!configs) {
265     GRL_INFO ("Configuration not provided! Plugin not loaded");
266     return FALSE;
267   }
268 
269   config_count = g_list_length (configs);
270   if (config_count > 1) {
271     GRL_INFO ("Provided %d configs, but will only use one", config_count);
272   }
273 
274   config = GRL_CONFIG (configs->data);
275   api_key = grl_config_get_api_key (config);
276   if (!api_key) {
277     GRL_INFO ("Missing API Key, cannot load plugin");
278     return FALSE;
279   }
280   format = grl_config_get_string (config, "format");
281 
282   source = grl_youtube_source_new (api_key, YOUTUBE_CLIENT_ID, format);
283 
284   grl_registry_register_source (registry,
285                                 plugin,
286                                 GRL_SOURCE (source),
287                                 NULL);
288 
289   g_free (api_key);
290   g_free (format);
291 
292   return TRUE;
293 }
294 
295 GRL_PLUGIN_DEFINE (GRL_MAJOR,
296                    GRL_MINOR,
297                    YOUTUBE_PLUGIN_ID,
298                    "YouTube",
299                    "A plugin for browsing and searching YouTube videos",
300                    "Igalia",
301                    VERSION,
302                    "LGPL-2.1-or-later",
303                    "http://www.igalia.com",
304                    grl_youtube_plugin_init,
305                    NULL,
306                    NULL);
307 
308 /* ================== YouTube GObject ================ */
309 
G_DEFINE_TYPE_WITH_PRIVATE(GrlYoutubeSource,grl_youtube_source,GRL_TYPE_SOURCE)310 G_DEFINE_TYPE_WITH_PRIVATE (GrlYoutubeSource, grl_youtube_source, GRL_TYPE_SOURCE)
311 
312 static GrlYoutubeSource *
313 grl_youtube_source_new (const gchar *api_key, const gchar *client_id, const gchar *format)
314 {
315   GrlYoutubeSource *source;
316   GDataYouTubeService *service;
317   GIcon *icon;
318   GFile *file;
319   const char *tags[] = {
320     "net:internet",
321     NULL
322   };
323 
324   GRL_DEBUG ("grl_youtube_source_new");
325 
326   service = gdata_youtube_service_new (api_key, NULL);
327   if (!service) {
328     GRL_WARNING ("Failed to initialize gdata service");
329     return NULL;
330   }
331 
332   file = g_file_new_for_uri ("resource:///org/gnome/grilo/plugins/youtube/channel-youtube.svg");
333   icon = g_file_icon_new (file);
334   g_object_unref (file);
335 
336   /* Use auto-split mode because YouTube fails for queries
337      that request more than YOUTUBE_MAX_CHUNK results */
338   source = GRL_YOUTUBE_SOURCE (g_object_new (GRL_YOUTUBE_SOURCE_TYPE,
339 					     "source-id", SOURCE_ID,
340 					     "source-name", SOURCE_NAME,
341 					     "source-desc", SOURCE_DESC,
342 					     "auto-split-threshold",
343 					     YOUTUBE_MAX_CHUNK,
344                                              "yt-service", service,
345                                              "supported-media", GRL_SUPPORTED_MEDIA_VIDEO,
346                                              "source-icon", icon,
347                                              "source-tags", tags,
348 					     NULL));
349 
350   g_object_unref (icon);
351   ytsrc = source;
352   g_object_add_weak_pointer (G_OBJECT (source), (gpointer *) &ytsrc);
353 
354   return source;
355 }
356 
357 static void
grl_youtube_source_class_init(GrlYoutubeSourceClass * klass)358 grl_youtube_source_class_init (GrlYoutubeSourceClass * klass)
359 {
360   GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
361   GrlSourceClass *source_class = GRL_SOURCE_CLASS (klass);
362 
363   gobject_class->set_property = grl_youtube_source_set_property;
364   gobject_class->finalize = grl_youtube_source_finalize;
365 
366   source_class->supported_keys = grl_youtube_source_supported_keys;
367   source_class->slow_keys = grl_youtube_source_slow_keys;
368   source_class->cancel = grl_youtube_source_cancel;
369 
370   source_class->search = grl_youtube_source_search;
371   source_class->browse = grl_youtube_source_browse;
372   source_class->resolve = grl_youtube_source_resolve;
373   source_class->test_media_from_uri = grl_youtube_test_media_from_uri;
374   source_class->media_from_uri = grl_youtube_get_media_from_uri;
375 
376   g_object_class_install_property (gobject_class,
377                                    PROP_SERVICE,
378                                    g_param_spec_object ("yt-service",
379                                                         "youtube-service",
380                                                         "gdata youtube service object",
381                                                         GDATA_TYPE_YOUTUBE_SERVICE,
382                                                         G_PARAM_WRITABLE
383                                                         | G_PARAM_CONSTRUCT_ONLY
384                                                         | G_PARAM_STATIC_NAME));
385 }
386 
387 static void
grl_youtube_source_init(GrlYoutubeSource * source)388 grl_youtube_source_init (GrlYoutubeSource *source)
389 {
390   source->priv = grl_youtube_source_get_instance_private (source);
391 }
392 
393 static void
grl_youtube_source_set_property(GObject * object,guint propid,const GValue * value,GParamSpec * pspec)394 grl_youtube_source_set_property (GObject *object,
395                                  guint propid,
396                                  const GValue *value,
397                                  GParamSpec *pspec)
398 
399 {
400   switch (propid) {
401   case PROP_SERVICE: {
402     GrlYoutubeSource *self;
403     self = GRL_YOUTUBE_SOURCE (object);
404     self->priv->service = g_value_get_object (value);
405     break;
406   }
407   default:
408     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propid, pspec);
409   }
410 }
411 
412 static void
grl_youtube_source_finalize(GObject * object)413 grl_youtube_source_finalize (GObject *object)
414 {
415   GrlYoutubeSource *self;
416 
417   self = GRL_YOUTUBE_SOURCE (object);
418 
419   g_clear_object (&self->priv->wc);
420   g_clear_object (&self->priv->service);
421 
422   G_OBJECT_CLASS (grl_youtube_source_parent_class)->finalize (object);
423 }
424 
425 /* ======================= Utilities ==================== */
426 
427 static void
entry_parsed_cb(TotemPlParser * parser,const char * uri,GHashTable * metadata,GrlMedia * media)428 entry_parsed_cb (TotemPlParser *parser,
429                  const char    *uri,
430                  GHashTable    *metadata,
431                  GrlMedia      *media)
432 {
433   grl_media_set_url (media, uri);
434 }
435 
436 static void
release_operation_data(guint operation_id)437 release_operation_data (guint operation_id)
438 {
439   GCancellable *cancellable = grl_operation_get_data (operation_id);
440 
441   g_clear_object (&cancellable);
442 
443   grl_operation_set_data (operation_id, NULL);
444 }
445 
446 static OperationSpec *
operation_spec_new(void)447 operation_spec_new (void)
448 {
449   OperationSpec *os;
450 
451   GRL_DEBUG ("Allocating new spec");
452 
453   os =  g_slice_new0 (OperationSpec);
454   g_atomic_int_set (&os->ref_count, 1);
455 
456   return os;
457 }
458 
459 static void
operation_spec_unref(OperationSpec * os)460 operation_spec_unref (OperationSpec *os)
461 {
462   if (g_atomic_int_dec_and_test (&os->ref_count)) {
463     g_clear_object (&os->cancellable);
464     g_slice_free (OperationSpec, os);
465     GRL_DEBUG ("freeing spec");
466   }
467 }
468 
469 static void
operation_spec_ref(OperationSpec * os)470 operation_spec_ref (OperationSpec *os)
471 {
472   GRL_DEBUG ("Reffing spec");
473   g_atomic_int_inc (&os->ref_count);
474 }
475 
476 static void
build_media_from_entry(GrlYoutubeSource * source,GrlMedia * content,GDataEntry * entry,GCancellable * cancellable,const GList * keys,BuildMediaFromEntryCbFunc callback,gpointer user_data)477 build_media_from_entry (GrlYoutubeSource *source,
478                         GrlMedia *content,
479                         GDataEntry *entry,
480                         GCancellable *cancellable,
481                         const GList *keys,
482                         BuildMediaFromEntryCbFunc callback,
483                         gpointer user_data)
484 {
485   GDataYouTubeVideo *video;
486   GDataMediaThumbnail *thumbnail;
487   GrlMedia *media;
488   GList *iter;
489 
490   if (!content) {
491     media = grl_media_video_new ();
492   } else {
493     media = content;
494   }
495 
496   video = GDATA_YOUTUBE_VIDEO (entry);
497 
498   /* Make sure we set the media id in any case */
499   if (!grl_media_get_id (media)) {
500     grl_media_set_id (media, gdata_entry_get_id (entry));
501   }
502 
503   iter = (GList *) keys;
504   while (iter) {
505     GrlKeyID key = GRLPOINTER_TO_KEYID (iter->data);
506     if (key == GRL_METADATA_KEY_TITLE) {
507       grl_media_set_title (media, gdata_entry_get_title (entry));
508     } else if (key == GRL_METADATA_KEY_DESCRIPTION) {
509       grl_media_set_description (media,
510 				 gdata_youtube_video_get_description (video));
511     } else if (key == GRL_METADATA_KEY_THUMBNAIL) {
512       GList *thumb_list;
513       thumb_list = gdata_youtube_video_get_thumbnails (video);
514       while (thumb_list) {
515         thumbnail = GDATA_MEDIA_THUMBNAIL (thumb_list->data);
516         grl_media_add_thumbnail (media,
517                                  gdata_media_thumbnail_get_uri (thumbnail));
518         thumb_list = g_list_next (thumb_list);
519       }
520     } else if (key == GRL_METADATA_KEY_PUBLICATION_DATE) {
521       GTimeVal date;
522       gint64 published = gdata_entry_get_published (entry);
523       date.tv_sec = (glong) published;
524       date.tv_usec = 0;
525       if (date.tv_sec != 0 || date.tv_usec != 0) {
526         GDateTime *date_time;
527         date_time = g_date_time_new_from_timeval_utc (&date);
528         grl_media_set_publication_date (media, date_time);
529         g_date_time_unref (date_time);
530       }
531     } else if (key == GRL_METADATA_KEY_DURATION) {
532       grl_media_set_duration (media, gdata_youtube_video_get_duration (video));
533     } else if (key == GRL_METADATA_KEY_MIME) {
534       grl_media_set_mime (media, YOUTUBE_VIDEO_MIME);
535     } else if (key == GRL_METADATA_KEY_SITE) {
536       grl_media_set_site (media, gdata_youtube_video_get_player_uri (video));
537     } else if (key == GRL_METADATA_KEY_EXTERNAL_URL) {
538       grl_media_set_external_url (media,
539 				  gdata_youtube_video_get_player_uri (video));
540     } else if (key == GRL_METADATA_KEY_RATING) {
541       gdouble average;
542       gdata_youtube_video_get_rating (video, NULL, NULL, NULL, &average);
543       grl_media_set_rating (media, average, 5.00);
544     } else if (key == GRL_METADATA_KEY_URL) {
545       TotemPlParser *parser;
546       TotemPlParserResult res;
547 
548       parser = totem_pl_parser_new ();
549       g_signal_connect (parser, "entry-parsed",
550                         G_CALLBACK (entry_parsed_cb), media);
551       res = totem_pl_parser_parse (parser,
552                                    (char *) gdata_youtube_video_get_player_uri (video),
553                                    FALSE);
554       if (res != TOTEM_PL_PARSER_RESULT_SUCCESS)
555 	GRL_WARNING ("Failed to get video URL. totem-pl-parser error '%d'", res);
556       g_clear_object (&parser);
557     } else if (key == GRL_METADATA_KEY_EXTERNAL_PLAYER) {
558       const gchar *uri = gdata_youtube_video_get_player_uri (video);
559       grl_media_set_external_player (media, uri);
560     }
561     iter = g_list_next (iter);
562   }
563 
564   callback (media, user_data);
565 }
566 
567 static void
build_categories_directory_read_cb(GObject * source_object,GAsyncResult * result,gpointer user_data)568 build_categories_directory_read_cb (GObject *source_object,
569                                     GAsyncResult *result,
570                                     gpointer user_data)
571 {
572   GDataYouTubeService *service;
573   BuildCategorySpec *bcs;
574   GDataAPPCategories *app_categories = NULL;
575   GList *categories = NULL;  /*<unowned GDataCategory>*/
576   GError *error = NULL;
577   guint total = 0;
578   GList *all = NULL, *iter;
579   CategoryInfo *cat_info;
580   guint index = 0;
581 
582   GRL_DEBUG (G_STRFUNC);
583 
584   service = GDATA_YOUTUBE_SERVICE (source_object);
585   bcs = user_data;
586 
587   app_categories = gdata_youtube_service_get_categories_finish (service,
588                                                                 result,
589                                                                 &error);
590 
591   if (error != NULL) {
592     g_error_free (error);
593     goto done;
594   }
595 
596   categories = gdata_app_categories_get_categories (app_categories);
597 
598   for (; categories != NULL; categories = categories->next) {
599     GDataCategory *category = GDATA_CATEGORY (categories->data);
600 
601     cat_info = g_slice_new (CategoryInfo);
602     cat_info->id = g_strconcat (YOUTUBE_CATEGORIES_ID, "/",
603                                 gdata_category_get_term (category), NULL);
604     cat_info->name = g_strdup (gdata_category_get_label (category));
605     all = g_list_prepend (all, cat_info);
606     total++;
607     GRL_DEBUG ("  Found category: '%d - %s'", index++, cat_info->name);
608   }
609 
610   if (all) {
611     root_dir[ROOT_DIR_CATEGORIES_INDEX].count = total;
612     categories_dir = g_new0 (CategoryInfo, total + 1);
613     iter = all;
614     do {
615       cat_info = (CategoryInfo *) iter->data;
616       categories_dir[total - 1].id = cat_info->id ;
617       categories_dir[total - 1].name = (gchar *) g_dgettext (GETTEXT_PACKAGE,
618                                                              cat_info->name);
619       categories_dir[total - 1].count = -1;
620       total--;
621       g_slice_free (CategoryInfo, cat_info);
622       iter = g_list_next (iter);
623     } while (iter);
624     g_list_free (all);
625   }
626 
627 done:
628   bcs->callback (bcs);
629   g_slice_free (BuildCategorySpec, bcs);
630 }
631 
632 static gint
get_feed_type_from_id(const gchar * feed_id)633 get_feed_type_from_id (const gchar *feed_id)
634 {
635   gchar *tmp;
636   gchar *test;
637   gint feed_type;
638 
639   tmp = g_strrstr (feed_id, "/");
640   if (!tmp) {
641     return -1;
642   }
643   tmp++;
644 
645   feed_type = strtol (tmp, &test, 10);
646   if (*test != '\0') {
647     return -1;
648   }
649 
650   return feed_type;
651 }
652 
653 static const gchar *
get_category_term_from_id(const gchar * category_id)654 get_category_term_from_id (const gchar *category_id)
655 {
656   gchar *term;
657   term = g_strrstr (category_id, "/");
658   if (!term) {
659     return NULL;
660   }
661   return ++term;
662 }
663 
664 static gint
get_category_index_from_id(const gchar * category_id)665 get_category_index_from_id (const gchar *category_id)
666 {
667   guint i;
668 
669   for (i=0; i<root_dir[ROOT_DIR_CATEGORIES_INDEX].count; i++) {
670     if (!strcmp (categories_dir[i].id, category_id)) {
671       return i;
672     }
673   }
674   return -1;
675 }
676 
677 static void
build_media_from_entry_resolve_cb(GrlMedia * media,gpointer user_data)678 build_media_from_entry_resolve_cb (GrlMedia *media, gpointer user_data)
679 {
680   GrlSourceResolveSpec *rs = (GrlSourceResolveSpec *) user_data;
681   release_operation_data (rs->operation_id);
682   rs->callback (rs->source, rs->operation_id, media, rs->user_data, NULL);
683 }
684 
685 static void
build_media_from_entry_search_cb(GrlMedia * media,gpointer user_data)686 build_media_from_entry_search_cb (GrlMedia *media, gpointer user_data)
687 {
688   /*
689    * TODO: Async resolution of URL messes (or could mess) with the sorting,
690    * If we want to ensure a particular sorting or implement sorting
691    * mechanisms we should add code to handle that here so we emit items in
692    * the right order and not just when we got the URL resolved (would
693    * damage response time though).
694    */
695   OperationSpec *os = (OperationSpec *) user_data;
696   guint remaining;
697 
698   if (g_cancellable_is_cancelled (os->cancellable)) {
699     GRL_DEBUG ("%s: cancelled", __FUNCTION__);
700     return;
701   }
702 
703   if (os->emitted < os->count) {
704     remaining = os->count - os->emitted - 1;
705     if (remaining == 0) {
706       release_operation_data (os->operation_id);
707     }
708     os->callback (os->source,
709 		  os->operation_id,
710 		  media,
711 		  remaining,
712 		  os->user_data,
713 		  NULL);
714     if (remaining == 0) {
715       GRL_DEBUG ("Unreffing spec in build_media_from_entry_search_cb");
716       operation_spec_unref (os);
717     } else {
718       os->emitted++;
719     }
720   }
721 }
722 
723 static void
build_category_directory(BuildCategorySpec * bcs)724 build_category_directory (BuildCategorySpec *bcs)
725 {
726   GrlYoutubeSource *source;
727   GDataYouTubeService *service;
728 
729   GRL_DEBUG (__FUNCTION__);
730 
731   source = GRL_YOUTUBE_SOURCE (bcs->source);
732   service = GDATA_YOUTUBE_SERVICE (source->priv->service);
733   gdata_youtube_service_get_categories_async (service, NULL,
734                                               build_categories_directory_read_cb,
735                                               bcs);
736 }
737 
738 static void
resolve_cb(GObject * object,GAsyncResult * result,gpointer user_data)739 resolve_cb (GObject *object,
740             GAsyncResult *result,
741             gpointer user_data)
742 {
743   GRL_DEBUG (__FUNCTION__);
744 
745   GError *error = NULL;
746   GrlYoutubeSource *source;
747   GDataEntry *video;
748   GDataService *service;
749   GrlSourceResolveSpec *rs = (GrlSourceResolveSpec *) user_data;
750 
751   source = GRL_YOUTUBE_SOURCE (rs->source);
752   service = GDATA_SERVICE (source->priv->service);
753 
754   video = gdata_service_query_single_entry_finish (service, result, &error);
755 
756   if (error) {
757     release_operation_data (rs->operation_id);
758     error->code = GRL_CORE_ERROR_RESOLVE_FAILED;
759     rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, error);
760     g_error_free (error);
761   } else {
762     build_media_from_entry (GRL_YOUTUBE_SOURCE (rs->source),
763                             rs->media,
764                             video,
765                             grl_operation_get_data (rs->operation_id),
766                             rs->keys,
767 			    build_media_from_entry_resolve_cb,
768                             rs);
769   }
770 
771   g_clear_object (&video);
772 }
773 
774 static void
search_progress_cb(GDataEntry * entry,guint index,guint count,gpointer user_data)775 search_progress_cb (GDataEntry *entry,
776 		    guint index,
777 		    guint count,
778 		    gpointer user_data)
779 {
780   OperationSpec *os = (OperationSpec *) user_data;
781 
782   /* Check if operation has been cancelled */
783   if (g_cancellable_is_cancelled (os->cancellable)) {
784     GRL_DEBUG ("%s: cancelled (%u, %u)", __FUNCTION__, index, count);
785     build_media_from_entry_search_cb (NULL, os);
786     return;
787   }
788 
789   if (index < count) {
790     /* Keep track of the items we got here. Due to the asynchronous
791      * nature of build_media_from_entry(), when search_cb is invoked
792      * we have to check if we got as many results as we requested or
793      * not, and handle that situation properly */
794     os->matches++;
795     build_media_from_entry (GRL_YOUTUBE_SOURCE (os->source),
796                             NULL,
797                             entry,
798                             os->cancellable,
799                             os->keys,
800                             build_media_from_entry_search_cb,
801                             os);
802   } else {
803     GRL_WARNING ("Invalid index/count received grom libgdata, ignoring result");
804   }
805 
806   /* The entry will be freed when freeing the feed in search_cb */
807 }
808 
809 static void
search_cb(GObject * object,GAsyncResult * result,OperationSpec * os)810 search_cb (GObject *object, GAsyncResult *result, OperationSpec *os)
811 {
812   GDataFeed *feed;
813   GError *error = NULL;
814   gboolean need_extra_unref = FALSE;
815   GrlYoutubeSource *source = GRL_YOUTUBE_SOURCE (os->source);
816 
817   GRL_DEBUG ("search_cb");
818 
819   /* Check if operation was cancelled */
820   if (g_cancellable_is_cancelled (os->cancellable)) {
821     GRL_DEBUG ("Search operation has been cancelled");
822     os->callback (os->source, os->operation_id, NULL, 0, os->user_data, NULL);
823     operation_spec_unref (os);
824     /* Look for OPERATION_SPEC_REF_RATIONALE for details on the reason for this
825      * extra unref */
826     operation_spec_unref (os);
827     return;
828   }
829 
830   feed = gdata_service_query_finish (source->priv->service, result, &error);
831   if (!error && feed) {
832     /* If we are browsing a category, update the count for it */
833     if (os->category_info) {
834       os->category_info->count = gdata_feed_get_total_results (feed);
835     }
836 
837     /* Check if we got as many results as we requested */
838     if (os->matches < os->count) {
839       os->count = os->matches;
840       /* In case we are resolving URLs asynchronously, from now on
841        * results will be sent with appropriate remaining, but it can
842        * also be the case that we have sent all the results already
843        * and the last one was sent with remaining>0, in that case
844        * we should send a finishing message now. */
845       if (os->emitted == os->count) {
846 	GRL_DEBUG ("sending finishing message");
847 	os->callback (os->source, os->operation_id,
848 		      NULL, 0, os->user_data, NULL);
849 	need_extra_unref = TRUE;
850       }
851     }
852   } else {
853     if (!error) {
854       error = g_error_new_literal (GRL_CORE_ERROR,
855                                    os->error_code,
856                                    _("Failed to get feed"));
857     } else {
858       error->code = os->error_code;
859     }
860     os->callback (os->source, os->operation_id, NULL, 0, os->user_data, error);
861     g_error_free (error);
862     need_extra_unref = TRUE;
863   }
864 
865   g_clear_object (&feed);
866 
867   GRL_DEBUG ("Unreffing spec in search_cb");
868   operation_spec_unref (os);
869   if (need_extra_unref) {
870     /* We did not free the spec in the emission callback, do it here */
871     GRL_DEBUG ("need extra spec unref in search_cb");
872     operation_spec_unref (os);
873   }
874 }
875 
876 static gboolean
is_category_container(const gchar * container_id)877 is_category_container (const gchar *container_id)
878 {
879   return g_str_has_prefix (container_id, YOUTUBE_CATEGORIES_ID "/");
880 }
881 
882 static gboolean
is_feeds_container(const gchar * container_id)883 is_feeds_container (const gchar *container_id)
884 {
885   return g_str_has_prefix (container_id, YOUTUBE_FEEDS_ID "/");
886 }
887 
888 static YoutubeMediaType
classify_media_id(const gchar * media_id)889 classify_media_id (const gchar *media_id)
890 {
891   if (!media_id) {
892     return YOUTUBE_MEDIA_TYPE_ROOT;
893   } else if (!strcmp (media_id, YOUTUBE_FEEDS_ID)) {
894     return YOUTUBE_MEDIA_TYPE_FEEDS;
895   } else if (!strcmp (media_id, YOUTUBE_CATEGORIES_ID)) {
896     return YOUTUBE_MEDIA_TYPE_CATEGORIES;
897   } else if (is_category_container (media_id)) {
898     return YOUTUBE_MEDIA_TYPE_CATEGORY;
899   } else if (is_feeds_container (media_id)) {
900     return YOUTUBE_MEDIA_TYPE_FEED;
901   } else {
902     return YOUTUBE_MEDIA_TYPE_VIDEO;
903   }
904 }
905 
906 static void
set_category_childcount(GDataService * service,GrlMedia * content,CategoryInfo * dir,guint index)907 set_category_childcount (GDataService *service,
908 			 GrlMedia *content,
909                          CategoryInfo *dir,
910                          guint index)
911 {
912   gint childcount;
913   gboolean set_childcount = TRUE;
914   const gchar *container_id;
915 
916   container_id = grl_media_get_id (GRL_MEDIA (content));
917 
918   if (dir == NULL) {
919     /* Special case: we want childcount of root category */
920     childcount = root_dir_size;
921   } else if (!strcmp (dir[index].id, YOUTUBE_FEEDS_ID)) {
922     childcount = root_dir[ROOT_DIR_FEEDS_INDEX].count;
923   } else if (!strcmp (dir[index].id, YOUTUBE_CATEGORIES_ID)) {
924     childcount = root_dir[ROOT_DIR_CATEGORIES_INDEX].count;
925   } else if (is_feeds_container (container_id)) {
926     gint feed_index = get_feed_type_from_id (container_id);
927     if (feed_index >= 0) {
928       childcount = feeds_dir[feed_index].count;
929     } else {
930       set_childcount = FALSE;
931     }
932   } else if (is_category_container (container_id)) {
933     gint cat_index = get_category_index_from_id (container_id);
934     if (cat_index >= 0) {
935       childcount = categories_dir[cat_index].count;
936     } else {
937       set_childcount = FALSE;
938     }
939   } else {
940     set_childcount = FALSE;
941   }
942 
943   if (set_childcount) {
944     grl_media_set_childcount (content, childcount);
945   }
946 }
947 
948 static GrlMedia *
produce_container_from_directory(GDataService * service,GrlMedia * media,CategoryInfo * dir,guint index)949 produce_container_from_directory (GDataService *service,
950 				  GrlMedia *media,
951 				  CategoryInfo *dir,
952 				  guint index)
953 {
954   GrlMedia *content;
955 
956   if (!media) {
957     /* Create mode */
958     content = grl_media_container_new ();
959   } else {
960     /* Update mode */
961     content = media;
962   }
963 
964   if (!dir) {
965     grl_media_set_id (content, NULL);
966     grl_media_set_title (content, YOUTUBE_ROOT_NAME);
967   } else {
968     grl_media_set_id (content, dir[index].id);
969     grl_media_set_title (content, g_dgettext (GETTEXT_PACKAGE, dir[index].name));
970   }
971   grl_media_set_site (content, YOUTUBE_SITE_URL);
972   set_category_childcount (service, content, dir, index);
973 
974   return content;
975 }
976 
977 static void
produce_from_directory(CategoryInfo * dir,guint dir_size,OperationSpec * os)978 produce_from_directory (CategoryInfo *dir, guint dir_size, OperationSpec *os)
979 {
980   guint index, remaining;
981 
982   GRL_DEBUG ("produce_from_directory");
983 
984   if (os->skip >= dir_size) {
985     /* No results */
986     os->callback (os->source,
987 		  os->operation_id,
988 		  NULL,
989 		  0,
990 		  os->user_data,
991 		  NULL);
992     operation_spec_unref (os);
993   } else {
994     index = os->skip;
995     remaining = MIN (dir_size - os->skip, os->count);
996 
997     do {
998       GDataService *service = GRL_YOUTUBE_SOURCE (os->source)->priv->service;
999 
1000       GrlMedia *content =
1001 	produce_container_from_directory (service, NULL, dir, index);
1002 
1003       remaining--;
1004       index++;
1005 
1006       os->callback (os->source,
1007 		    os->operation_id,
1008 		    content,
1009 		    remaining,
1010 		    os->user_data,
1011 		    NULL);
1012 
1013       if (remaining == 0) {
1014 	operation_spec_unref (os);
1015       }
1016     } while (remaining > 0);
1017   }
1018 }
1019 
1020 static void
produce_from_feed(OperationSpec * os)1021 produce_from_feed (OperationSpec *os)
1022 {
1023   GError *error = NULL;
1024   gint feed_type;
1025   GDataQuery *query;
1026   GDataService *service;
1027 
1028   feed_type = get_feed_type_from_id (os->container_id);
1029 
1030   if (feed_type < 0) {
1031     error = g_error_new (GRL_CORE_ERROR,
1032                          GRL_CORE_ERROR_BROWSE_FAILED,
1033                          _("Invalid feed identifier %s"),
1034                          os->container_id);
1035     os->callback (os->source,
1036 		  os->operation_id,
1037 		  NULL,
1038 		  0,
1039 		  os->user_data,
1040 		  error);
1041     g_error_free (error);
1042     operation_spec_unref (os);
1043     return;
1044   }
1045   /* OPERATION_SPEC_REF_RATIONALE
1046    * Depending on wether the URL has been requested, metadata resolution
1047    * for each item in the result set may or may not be asynchronous.
1048    * We cannot free the spec in search_cb because that may be called
1049    * before the asynchronous URL resolution is finished, and we cannot
1050    * do it in build_media_from_entry_search_cb either, because in the
1051    * synchronous case (when we do not request URL) search_cb will
1052    * be invoked after it.
1053    * Thus, the solution is to increase the reference count here and
1054    * have both places unreffing the spec, that way, no matter which
1055    * is invoked last, the spec will be freed only once. */
1056   operation_spec_ref (os);
1057 
1058   os->cancellable = g_cancellable_new ();
1059   grl_operation_set_data (os->operation_id, g_object_ref (os->cancellable));
1060 
1061   service = GRL_YOUTUBE_SOURCE (os->source)->priv->service;
1062 
1063   /* Index in GData starts at 1 */
1064   query = GDATA_QUERY (gdata_youtube_query_new (NULL));
1065   gdata_query_set_start_index (query, os->skip + 1);
1066   gdata_query_set_max_results (query, os->count);
1067   os->category_info = &feeds_dir[feed_type];
1068 
1069   gdata_youtube_service_query_standard_feed_async (GDATA_YOUTUBE_SERVICE (service),
1070                                                    feed_type,
1071                                                    query,
1072                                                    os->cancellable,
1073                                                    search_progress_cb,
1074                                                    os,
1075                                                    NULL,
1076                                                    (GAsyncReadyCallback) search_cb,
1077                                                    os);
1078 
1079   g_object_unref (query);
1080 }
1081 
1082 static void
produce_from_category(OperationSpec * os)1083 produce_from_category (OperationSpec *os)
1084 {
1085   GError *error = NULL;
1086   GDataQuery *query;
1087   GDataService *service;
1088   const gchar *category_term;
1089   gint category_index;
1090 
1091   category_term = get_category_term_from_id (os->container_id);
1092   category_index = get_category_index_from_id (os->container_id);
1093 
1094   if (!category_term) {
1095     error = g_error_new (GRL_CORE_ERROR,
1096                          GRL_CORE_ERROR_BROWSE_FAILED,
1097                          _("Invalid category identifier %s"),
1098                          os->container_id);
1099     os->callback (os->source,
1100 		  os->operation_id,
1101 		  NULL,
1102 		  0,
1103 		  os->user_data,
1104 		  error);
1105     g_error_free (error);
1106     operation_spec_unref (os);
1107     return;
1108   }
1109 
1110   /* Look for OPERATION_SPEC_REF_RATIONALE for details */
1111   operation_spec_ref (os);
1112 
1113   service = GRL_YOUTUBE_SOURCE (os->source)->priv->service;
1114 
1115   /* Index in GData starts at 1 */
1116   query = GDATA_QUERY (gdata_youtube_query_new (NULL));
1117   gdata_query_set_start_index (query, os->skip + 1);
1118   gdata_query_set_max_results (query, os->count);
1119   os->category_info = &categories_dir[category_index];
1120   gdata_query_set_categories (query, category_term);
1121 
1122   gdata_youtube_service_query_videos_async (GDATA_YOUTUBE_SERVICE (service),
1123                                             query,
1124                                             NULL,
1125                                             search_progress_cb,
1126                                             os,
1127                                             NULL,
1128                                             (GAsyncReadyCallback) search_cb,
1129                                             os);
1130 
1131   g_object_unref (query);
1132 }
1133 
1134 static gchar *
get_video_id_from_url(const gchar * url)1135 get_video_id_from_url (const gchar *url)
1136 {
1137   gchar *marker, *end, *video_id;
1138 
1139   if (url == NULL) {
1140     return NULL;
1141   }
1142 
1143   marker = strstr (url, YOUTUBE_WATCH_URL);
1144   if (!marker) {
1145     return NULL;
1146   }
1147 
1148   marker += strlen (YOUTUBE_WATCH_URL);
1149 
1150   end = marker;
1151   while (*end != '\0' && *end != '&') {
1152     end++;
1153   }
1154 
1155   video_id = g_strndup (marker, end - marker);
1156 
1157   return video_id;
1158 }
1159 
1160 static void
build_media_from_entry_media_from_uri_cb(GrlMedia * media,gpointer user_data)1161 build_media_from_entry_media_from_uri_cb (GrlMedia *media, gpointer user_data)
1162 {
1163   GrlSourceMediaFromUriSpec *mfus = (GrlSourceMediaFromUriSpec *) user_data;
1164 
1165   release_operation_data (mfus->operation_id);
1166   mfus->callback (mfus->source, mfus->operation_id,
1167                   media, mfus->user_data, NULL);
1168 }
1169 
1170 static void
media_from_uri_cb(GObject * object,GAsyncResult * result,gpointer user_data)1171 media_from_uri_cb (GObject *object, GAsyncResult *result, gpointer user_data)
1172 {
1173   GError *error = NULL;
1174   GrlYoutubeSource *source;
1175   GDataEntry *video;
1176   GDataService *service;
1177   GrlSourceMediaFromUriSpec *mfus = (GrlSourceMediaFromUriSpec *) user_data;
1178 
1179   source = GRL_YOUTUBE_SOURCE (mfus->source);
1180   service = GDATA_SERVICE (source->priv->service);
1181 
1182   video = gdata_service_query_single_entry_finish (service, result, &error);
1183 
1184   if (error) {
1185     error->code = GRL_CORE_ERROR_MEDIA_FROM_URI_FAILED;
1186     release_operation_data (mfus->operation_id);
1187     mfus->callback (mfus->source, mfus->operation_id, NULL, mfus->user_data, error);
1188     g_error_free (error);
1189   } else {
1190     build_media_from_entry (GRL_YOUTUBE_SOURCE (mfus->source),
1191                             NULL,
1192                             video,
1193                             grl_operation_get_data (mfus->operation_id),
1194                             mfus->keys,
1195 			    build_media_from_entry_media_from_uri_cb,
1196 			    mfus);
1197   }
1198 
1199   g_clear_object (&video);
1200 }
1201 
1202 static gboolean
produce_from_category_cb(BuildCategorySpec * spec)1203 produce_from_category_cb (BuildCategorySpec *spec)
1204 {
1205   produce_from_directory (categories_dir,
1206                           root_dir[ROOT_DIR_CATEGORIES_INDEX].count,
1207                           spec->user_data);
1208   return FALSE;
1209 }
1210 
1211 static gboolean
produce_container_from_category_cb(BuildCategorySpec * spec)1212 produce_container_from_category_cb (BuildCategorySpec *spec)
1213 {
1214   GError *error = NULL;
1215   GrlMedia *media = NULL;
1216 
1217   GrlSourceResolveSpec *rs = (GrlSourceResolveSpec *) spec->user_data;
1218   GDataService *service = GRL_YOUTUBE_SOURCE (rs->source)->priv->service;
1219   const gchar *id = grl_media_get_id (rs->media);
1220   gint index = get_category_index_from_id (id);
1221   if (index >= 0) {
1222     media = produce_container_from_directory (service,
1223                                               rs->media,
1224                                               categories_dir,
1225                                               index);
1226   } else {
1227     media = rs->media;
1228     error = g_error_new (GRL_CORE_ERROR,
1229                          GRL_CORE_ERROR_RESOLVE_FAILED,
1230                          _("Invalid category identifier %s"),
1231                          id);
1232   }
1233 
1234   rs->callback (rs->source, rs->operation_id, media, rs->user_data, error);
1235   g_clear_error (&error);
1236 
1237   return FALSE;
1238 }
1239 
1240 /* ================== API Implementation ================ */
1241 
1242 static const GList *
grl_youtube_source_supported_keys(GrlSource * source)1243 grl_youtube_source_supported_keys (GrlSource *source)
1244 {
1245   static GList *keys = NULL;
1246   if (!keys) {
1247     keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
1248                                       GRL_METADATA_KEY_TITLE,
1249                                       GRL_METADATA_KEY_URL,
1250                                       GRL_METADATA_KEY_EXTERNAL_URL,
1251                                       GRL_METADATA_KEY_DESCRIPTION,
1252                                       GRL_METADATA_KEY_DURATION,
1253                                       GRL_METADATA_KEY_PUBLICATION_DATE,
1254                                       GRL_METADATA_KEY_THUMBNAIL,
1255                                       GRL_METADATA_KEY_MIME,
1256                                       GRL_METADATA_KEY_CHILDCOUNT,
1257                                       GRL_METADATA_KEY_SITE,
1258                                       GRL_METADATA_KEY_RATING,
1259                                       GRL_METADATA_KEY_EXTERNAL_PLAYER,
1260                                       NULL);
1261   }
1262   return keys;
1263 }
1264 
1265 static const GList *
grl_youtube_source_slow_keys(GrlSource * source)1266 grl_youtube_source_slow_keys (GrlSource *source)
1267 {
1268   static GList *keys = NULL;
1269   if (!keys) {
1270     keys = grl_metadata_key_list_new (GRL_METADATA_KEY_URL,
1271                                       NULL);
1272   }
1273   return keys;
1274 }
1275 
1276 static void
grl_youtube_source_search(GrlSource * source,GrlSourceSearchSpec * ss)1277 grl_youtube_source_search (GrlSource *source,
1278                            GrlSourceSearchSpec *ss)
1279 {
1280   OperationSpec *os;
1281   GDataQuery *query;
1282 
1283   GRL_DEBUG ("%s (%u, %d)",
1284              __FUNCTION__,
1285              grl_operation_options_get_skip (ss->options),
1286              grl_operation_options_get_count (ss->options));
1287 
1288   os = operation_spec_new ();
1289   os->source = source;
1290   os->cancellable = g_cancellable_new ();
1291   os->operation_id = ss->operation_id;
1292   os->keys = ss->keys;
1293   os->skip = grl_operation_options_get_skip (ss->options);
1294   os->count = grl_operation_options_get_count (ss->options);
1295   os->callback = ss->callback;
1296   os->user_data = ss->user_data;
1297   os->error_code = GRL_CORE_ERROR_SEARCH_FAILED;
1298 
1299   /* Look for OPERATION_SPEC_REF_RATIONALE for details */
1300   operation_spec_ref (os);
1301 
1302   grl_operation_set_data (ss->operation_id, g_object_ref (os->cancellable));
1303 
1304   /* Index in GData starts at 1 */
1305   query = GDATA_QUERY (gdata_youtube_query_new (ss->text));
1306   gdata_query_set_start_index (query, os->skip + 1);
1307   gdata_query_set_max_results (query, os->count);
1308 
1309   gdata_youtube_service_query_videos_async (GDATA_YOUTUBE_SERVICE (GRL_YOUTUBE_SOURCE (source)->priv->service),
1310                                             query,
1311                                             os->cancellable,
1312                                             search_progress_cb,
1313                                             os,
1314                                             NULL,
1315                                             (GAsyncReadyCallback) search_cb,
1316                                             os);
1317 
1318   g_object_unref (query);
1319 }
1320 
1321 static void
grl_youtube_source_browse(GrlSource * source,GrlSourceBrowseSpec * bs)1322 grl_youtube_source_browse (GrlSource *source,
1323                            GrlSourceBrowseSpec *bs)
1324 {
1325   BuildCategorySpec *bcs;
1326   OperationSpec *os;
1327   const gchar *container_id;
1328 
1329   GRL_DEBUG ("%s: %s (%u, %d)",
1330              __FUNCTION__,
1331              grl_media_get_id (bs->container),
1332              grl_operation_options_get_skip (bs->options),
1333              grl_operation_options_get_count (bs->options));
1334 
1335   container_id = grl_media_get_id (bs->container);
1336 
1337   os = operation_spec_new ();
1338   os->source = bs->source;
1339   os->operation_id = bs->operation_id;
1340   os->container_id = container_id;
1341   os->keys = bs->keys;
1342   os->flags = grl_operation_options_get_resolution_flags (bs->options);
1343   os->skip = grl_operation_options_get_skip (bs->options);
1344   os->count = grl_operation_options_get_count (bs->options);
1345   os->callback = bs->callback;
1346   os->user_data = bs->user_data;
1347   os->error_code = GRL_CORE_ERROR_BROWSE_FAILED;
1348 
1349   switch (classify_media_id (container_id))
1350     {
1351     case YOUTUBE_MEDIA_TYPE_ROOT:
1352       produce_from_directory (root_dir, root_dir_size, os);
1353       break;
1354     case YOUTUBE_MEDIA_TYPE_FEEDS:
1355       produce_from_directory (feeds_dir,
1356 			      root_dir[ROOT_DIR_FEEDS_INDEX].count, os);
1357       break;
1358     case YOUTUBE_MEDIA_TYPE_CATEGORIES:
1359       if (!categories_dir) {
1360         bcs = g_slice_new0 (BuildCategorySpec);
1361         bcs->source = bs->source;
1362         bcs->callback = (GSourceFunc) produce_from_category_cb;
1363         bcs->user_data = os;
1364         build_category_directory (bcs);
1365       } else {
1366         produce_from_directory (categories_dir,
1367                                 root_dir[ROOT_DIR_CATEGORIES_INDEX].count,
1368                                 os);
1369       }
1370       break;
1371     case YOUTUBE_MEDIA_TYPE_FEED:
1372       produce_from_feed (os);
1373       break;
1374     case YOUTUBE_MEDIA_TYPE_CATEGORY:
1375       produce_from_category (os);
1376       break;
1377     case YOUTUBE_MEDIA_TYPE_VIDEO:
1378     default:
1379       g_assert_not_reached ();
1380       break;
1381     }
1382 }
1383 
1384 static void
grl_youtube_source_resolve(GrlSource * source,GrlSourceResolveSpec * rs)1385 grl_youtube_source_resolve (GrlSource *source,
1386                             GrlSourceResolveSpec *rs)
1387 {
1388   BuildCategorySpec *bcs;
1389   YoutubeMediaType media_type;
1390   const gchar *id;
1391   GCancellable *cancellable;
1392   GDataService *service;
1393   GError *error = NULL;
1394   GrlMedia *media = NULL;
1395 
1396   GRL_DEBUG (__FUNCTION__);
1397 
1398   id = grl_media_get_id (rs->media);
1399   media_type = classify_media_id (id);
1400   service = GRL_YOUTUBE_SOURCE (source)->priv->service;
1401 
1402   switch (media_type) {
1403   case YOUTUBE_MEDIA_TYPE_ROOT:
1404     media = produce_container_from_directory (service, rs->media, NULL, 0);
1405     break;
1406   case YOUTUBE_MEDIA_TYPE_FEEDS:
1407     media = produce_container_from_directory (service, rs->media, root_dir, 0);
1408     break;
1409   case YOUTUBE_MEDIA_TYPE_CATEGORIES:
1410     media = produce_container_from_directory (service, rs->media, root_dir, 1);
1411     break;
1412   case YOUTUBE_MEDIA_TYPE_FEED:
1413     {
1414       gint index = get_feed_type_from_id (id);
1415       if (index >= 0) {
1416         media = produce_container_from_directory (service, rs->media, feeds_dir,
1417                                                   index);
1418       } else {
1419         error = g_error_new (GRL_CORE_ERROR,
1420                              GRL_CORE_ERROR_RESOLVE_FAILED,
1421                              _("Invalid feed identifier %s"),
1422                              id);
1423       }
1424     }
1425     break;
1426   case YOUTUBE_MEDIA_TYPE_CATEGORY:
1427     {
1428       if (!categories_dir) {
1429         bcs = g_slice_new0 (BuildCategorySpec);
1430         bcs->source = source;
1431         bcs->callback = (GSourceFunc) produce_container_from_category_cb;
1432         bcs->user_data = rs;
1433         build_category_directory (bcs);
1434       } else {
1435         gint index = get_category_index_from_id (id);
1436         if (index >= 0) {
1437           media = produce_container_from_directory (service, rs->media,
1438                                                     categories_dir, index);
1439         } else {
1440           error = g_error_new (GRL_CORE_ERROR,
1441                                GRL_CORE_ERROR_RESOLVE_FAILED,
1442                                _("Invalid category identifier %s"),
1443                                id);
1444         }
1445       }
1446     }
1447     break;
1448   case YOUTUBE_MEDIA_TYPE_VIDEO:
1449   default:
1450     cancellable = g_cancellable_new ();
1451     grl_operation_set_data (rs->operation_id, cancellable);
1452     gchar *entryid = g_strconcat ("tag:youtube.com,2008:video:", id, NULL);
1453 
1454       gdata_service_query_single_entry_async (service,
1455                                               NULL,
1456                                               entryid,
1457                                               NULL,
1458                                               GDATA_TYPE_YOUTUBE_VIDEO,
1459                                               cancellable,
1460                                               resolve_cb,
1461                                               rs);
1462 
1463       g_free (entryid);
1464       break;
1465   }
1466 
1467   if (error) {
1468     rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, error);
1469     g_error_free (error);
1470   } else if (media) {
1471     rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, NULL);
1472   }
1473 }
1474 
1475 static gboolean
grl_youtube_test_media_from_uri(GrlSource * source,const gchar * uri)1476 grl_youtube_test_media_from_uri (GrlSource *source, const gchar *uri)
1477 {
1478   gchar *video_id;
1479   gboolean ok;
1480 
1481   GRL_DEBUG (__FUNCTION__);
1482 
1483   video_id = get_video_id_from_url (uri);
1484   ok = (video_id != NULL);
1485   g_free (video_id);
1486   return ok;
1487 }
1488 
1489 static void
grl_youtube_get_media_from_uri(GrlSource * source,GrlSourceMediaFromUriSpec * mfus)1490 grl_youtube_get_media_from_uri (GrlSource *source,
1491                                 GrlSourceMediaFromUriSpec *mfus)
1492 {
1493   gchar *video_id;
1494   GError *error;
1495   GCancellable *cancellable;
1496   GDataService *service;
1497   gchar *entry_id;
1498 
1499   GRL_DEBUG (__FUNCTION__);
1500 
1501   video_id = get_video_id_from_url (mfus->uri);
1502   if (video_id == NULL) {
1503     error = g_error_new (GRL_CORE_ERROR,
1504                          GRL_CORE_ERROR_MEDIA_FROM_URI_FAILED,
1505                          _("Cannot get media from %s"),
1506                          mfus->uri);
1507     mfus->callback (source, mfus->operation_id, NULL, mfus->user_data, error);
1508     g_error_free (error);
1509     return;
1510   }
1511 
1512   service = GRL_YOUTUBE_SOURCE (source)->priv->service;
1513 
1514   cancellable = g_cancellable_new ();
1515   grl_operation_set_data (mfus->operation_id, cancellable);
1516   entry_id = g_strconcat ("tag:youtube.com,2008:video:", video_id, NULL);
1517 
1518   gdata_service_query_single_entry_async (service,
1519                                           NULL,
1520                                           entry_id,
1521                                           NULL,
1522                                           GDATA_TYPE_YOUTUBE_VIDEO,
1523                                           cancellable,
1524                                           media_from_uri_cb,
1525                                           mfus);
1526 
1527   g_free (entry_id);
1528 }
1529 
1530 static void
grl_youtube_source_cancel(GrlSource * source,guint operation_id)1531 grl_youtube_source_cancel (GrlSource *source,
1532                            guint operation_id)
1533 {
1534   GCancellable *cancellable = NULL;
1535   gpointer data;
1536 
1537   GRL_DEBUG (__FUNCTION__);
1538 
1539   data = grl_operation_get_data (operation_id);
1540 
1541   if (data) {
1542     cancellable = G_CANCELLABLE (data);
1543   }
1544 
1545   if (cancellable) {
1546     g_cancellable_cancel (cancellable);
1547   }
1548 }
1549