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