[libgdata/wip/pwithnall/youtube-v3] youtube: WIP work to port to API v3
- From: Philip Withnall <pwithnall src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libgdata/wip/pwithnall/youtube-v3] youtube: WIP work to port to API v3
- Date: Sun, 19 Apr 2015 11:26:08 +0000 (UTC)
commit 847cfdcf35c3c7e8f8f62618675faace670ef30a
Author: Philip Withnall <philip tecnocode co uk>
Date: Thu Apr 16 00:47:43 2015 +0100
youtube: WIP work to port to API v3
https://bugzilla.gnome.org/show_bug.cgi?id=687597
configure.ac | 6 +-
demos/youtube/youtube-cli.c | 494 ++++++++++++-
gdata/app/gdata-app-categories.c | 76 ++
gdata/gdata-feed.c | 14 +
gdata/gdata-parser.c | 186 +++++-
gdata/gdata-parser.h | 17 +
gdata/gdata-upload-stream.c | 71 ++-
gdata/media/gdata-media-thumbnail.c | 31 +
gdata/services/youtube/gdata-youtube-query.c | 228 ++++--
gdata/services/youtube/gdata-youtube-service.c | 496 ++++++++-----
gdata/services/youtube/gdata-youtube-video.c | 995 +++++++++++++++++++-----
gdata/services/youtube/gdata-youtube-video.h | 1 +
12 files changed, 2125 insertions(+), 490 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index ae57c49..1ab705e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -33,9 +33,9 @@ AC_PATH_PROG([GLIB_GENMARSHAL],[glib-genmarshal])
AC_PATH_PROG([GLIB_MKENUMS],[glib-mkenums])
# Requirements
-GLIB_REQS=2.31.0
-GLIB_MIN_REQUIRED=GLIB_VERSION_2_32
-GLIB_MAX_ALLOWED='(G_ENCODE_VERSION(2, 38))'
+GLIB_REQS=2.44.0
+GLIB_MIN_REQUIRED=GLIB_VERSION_2_44
+GLIB_MAX_ALLOWED='(G_ENCODE_VERSION(2, 45))'
GIO_REQS=2.17.3
SOUP_REQS=2.42.0
SOUP_MIN_REQUIRED=SOUP_VERSION_2_42
diff --git a/demos/youtube/youtube-cli.c b/demos/youtube/youtube-cli.c
index 7bb23b4..7f3186f 100644
--- a/demos/youtube/youtube-cli.c
+++ b/demos/youtube/youtube-cli.c
@@ -21,7 +21,10 @@
#include <locale.h>
#include <string.h>
-#define DEVELOPER_KEY
"AI39si7Me3Q7zYs6hmkFvpRBD2nrkVjYYsUO5lh_3HdOkGRc9g6Z4nzxZatk_aAo2EsA21k7vrda0OO6oFg2rnhMedZXPyXoEw"
+#define CLIENT_ID "1074795795536-necvslvs0pchk65nf6ju4i6mniogg8fr.apps.googleusercontent.com"
+#define CLIENT_SECRET "8totRi50eo2Zfr3SD2DeNAzo"
+#define REDIRECT_URI "urn:ietf:wg:oauth:2.0:oob"
+#define DEVELOPER_KEY "AIzaSyCENhl8yDxDZbyhTF6p-ok-RefK07xdXUg"
static int
print_usage (char *argv[])
@@ -30,18 +33,125 @@ print_usage (char *argv[])
return -1;
}
+static void
+print_video (GDataYouTubeVideo *video)
+{
+ const gchar *title, *player_uri, *id, *description;
+ GList/*<unowned GDataMediaThumbnail>*/ *thumbnails;
+ GTimeVal date_published_tv = { 0, };
+ gchar *date_published = NULL; /* owned */
+ guint duration; /* seconds */
+ guint rating_min = 0, rating_max = 0, rating_count = 0;
+ gdouble rating_average = 0.0;
+
+ title = gdata_entry_get_title (GDATA_ENTRY (video));
+ player_uri = gdata_youtube_video_get_player_uri (video);
+ id = gdata_entry_get_id (GDATA_ENTRY (video));
+ description = gdata_youtube_video_get_description (video);
+ thumbnails = gdata_youtube_video_get_thumbnails (video);
+ date_published_tv.tv_sec = gdata_entry_get_published (GDATA_ENTRY (video));
+ date_published = g_time_val_to_iso8601 (&date_published_tv);
+ duration = gdata_youtube_video_get_duration (video);
+ gdata_youtube_video_get_rating (video, &rating_min, &rating_max,
+ &rating_count, &rating_average);
+
+ g_print ("%s — %s\n", player_uri, title);
+ g_print (" ID: %s\n", id);
+ g_print (" Published: %s\n", date_published);
+ g_print (" Duration: %us\n", duration);
+ g_print (" Rating: %.2f (min: %u, max: %u, count: %u)\n",
+ rating_average, rating_min, rating_max, rating_count);
+ g_print (" Description:\n %s\n", description);
+ g_print (" Thumbnails:\n");
+
+ for (; thumbnails != NULL; thumbnails = thumbnails->next) {
+ GDataMediaThumbnail *thumbnail;
+
+ thumbnail = GDATA_MEDIA_THUMBNAIL (thumbnails->data);
+ g_print (" • %s\n",
+ gdata_media_thumbnail_get_uri (thumbnail));
+ }
+
+ g_print ("\n");
+
+ g_free (date_published);
+}
+
+static void
+print_category (GDataCategory *category)
+{
+ const gchar *term, *label;
+
+ term = gdata_category_get_term (category);
+ label = gdata_category_get_label (category);
+
+ g_print ("%s — %s\n", term, label);
+}
+
+static GDataAuthorizer *
+create_authorizer (GError **error)
+{
+ GDataOAuth2Authorizer *authorizer = NULL; /* owned */
+ gchar *uri = NULL;
+ gchar code[100];
+ GError *child_error = NULL;
+
+ /* Go through the interactive OAuth dance. */
+ authorizer = gdata_oauth2_authorizer_new (CLIENT_ID, CLIENT_SECRET,
+ REDIRECT_URI,
+ GDATA_TYPE_YOUTUBE_SERVICE);
+
+ /* Get an authentication URI */
+ uri = gdata_oauth2_authorizer_build_authentication_uri (authorizer,
+ NULL, FALSE);
+
+ /* Wait for the user to retrieve and enter the verifier. */
+ g_print ("Please navigate to the following URI and grant access:\n"
+ " %s\n", uri);
+ g_print ("Enter verifier (EOF to abort): ");
+
+ g_free (uri);
+
+ if (scanf ("%100s", code) != 1) {
+ /* User chose to abort. */
+ g_print ("\n");
+ g_clear_object (&authorizer);
+ return NULL;
+ }
+
+ /* Authorise the token. */
+ gdata_oauth2_authorizer_request_authorization (authorizer, code, NULL,
+ &child_error);
+
+ if (child_error != NULL) {
+ g_propagate_error (error, child_error);
+ g_clear_object (&authorizer);
+ return NULL;
+ }
+
+ return GDATA_AUTHORIZER (authorizer);
+}
+
+/* Search for videos given a simple query string. */
static int
-command_search (char *argv[])
+command_search (int argc, char *argv[])
{
GDataYouTubeService *service = NULL;
GDataYouTubeQuery *query = NULL;
GDataFeed *feed = NULL;
- GList *entries;
+ GList/*<unowned GDataYouTubeVideo>*/ *entries;
GError *error = NULL;
gint retval = 0;
+ const gchar *query_string;
+
+ if (argc < 3) {
+ return print_usage (argv);
+ }
+
+ query_string = argv[2];
service = gdata_youtube_service_new (DEVELOPER_KEY, NULL);
- query = gdata_youtube_query_new (argv[2]);
+ query = gdata_youtube_query_new (query_string);
feed = gdata_youtube_service_query_videos (service, GDATA_QUERY (query),
NULL, NULL, NULL, &error);
@@ -57,22 +167,373 @@ command_search (char *argv[])
for (entries = gdata_feed_get_entries (feed); entries != NULL;
entries = entries->next) {
GDataYouTubeVideo *video;
- const gchar *title, *player_uri;
video = GDATA_YOUTUBE_VIDEO (entries->data);
- title = gdata_entry_get_title (GDATA_ENTRY (video));
- player_uri = gdata_youtube_video_get_player_uri (video);
+ print_video (video);
+ }
+
+ g_print ("Total of %u results.\n", gdata_feed_get_total_results (feed));
+
+done:
+ g_clear_object (&feed);
+ g_clear_object (&query);
+ g_clear_object (&service);
+
+ return retval;
+}
+
+/* Display information about a single video. */
+static int
+command_info (int argc, char *argv[])
+{
+ GDataYouTubeService *service = NULL;
+ GDataEntry *result = NULL;
+ GDataYouTubeVideo *video; /* unowned */
+ GError *error = NULL;
+ gint retval = 0;
+ const gchar *entry_id;
+
+ if (argc < 3) {
+ return print_usage (argv);
+ }
+
+ entry_id = argv[2];
+
+ service = gdata_youtube_service_new (DEVELOPER_KEY, NULL);
+ result = gdata_service_query_single_entry (GDATA_SERVICE (service),
+ NULL, entry_id, NULL,
+ GDATA_TYPE_YOUTUBE_VIDEO,
+ NULL, &error);
+
+ if (error != NULL) {
+ g_printerr ("%s: Error querying YouTube: %s\n",
+ argv[0], error->message);
+ g_error_free (error);
+ retval = 1;
+ goto done;
+ }
+
+ /* Print results. */
+ video = GDATA_YOUTUBE_VIDEO (result);
+ print_video (video);
+
+done:
+ g_clear_object (&result);
+ g_clear_object (&service);
+
+ return retval;
+}
+
+static gboolean
+standard_feed_type_from_name (const gchar *name,
+ GDataYouTubeStandardFeedType *out)
+{
+ /* Indexed by GDataYouTubeStandardFeedType. */
+ const gchar *feed_type_names[] = {
+ "top-rated",
+ "top-favorites",
+ "most-viewed",
+ "most-popular",
+ "most-recent",
+ "most-discussed",
+ "most-linked",
+ "most-responded",
+ "recently-featured",
+ "watch-on-mobile",
+ };
+ guint i;
+
+ G_STATIC_ASSERT (G_N_ELEMENTS (feed_type_names) ==
+ GDATA_YOUTUBE_WATCH_ON_MOBILE_FEED + 1);
+
+ for (i = 0; i < G_N_ELEMENTS (feed_type_names); i++) {
+ if (g_strcmp0 (feed_type_names[i], name) == 0) {
+ *out = (GDataYouTubeStandardFeedType) i;
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+/* List all videos in a standard feed. */
+static int
+command_standard_feed (int argc, char *argv[])
+{
+ GDataYouTubeService *service = NULL;
+ GDataFeed *feed = NULL;
+ GList/*<unowned GDataYouTubeVideo>*/ *entries;
+ GError *error = NULL;
+ gint retval = 0;
+ GDataYouTubeStandardFeedType feed_type;
+
+ if (argc < 3) {
+ return print_usage (argv);
+ }
+
+ if (!standard_feed_type_from_name (argv[2], &feed_type)) {
+ g_printerr ("%s: Invalid feed type ‘%s’.\n", argv[0], argv[2]);
+ retval = 1;
+ goto done;
+ }
+
+ service = gdata_youtube_service_new (DEVELOPER_KEY, NULL);
+ feed = gdata_youtube_service_query_standard_feed (service, feed_type,
+ NULL, NULL, NULL,
+ NULL, &error);
- g_print ("%s — %s\n", player_uri, title);
+ if (error != NULL) {
+ g_printerr ("%s: Error querying YouTube: %s\n",
+ argv[0], error->message);
+ g_error_free (error);
+ retval = 1;
+ goto done;
}
- if (gdata_feed_get_entries (feed) == NULL) {
- g_print ("No results.\n");
+ /* Print results. */
+ for (entries = gdata_feed_get_entries (feed); entries != NULL;
+ entries = entries->next) {
+ GDataYouTubeVideo *video;
+
+ video = GDATA_YOUTUBE_VIDEO (entries->data);
+ print_video (video);
}
+ g_print ("Total of %u results.\n", gdata_feed_get_total_results (feed));
+
done:
g_clear_object (&feed);
- g_clear_object (&query);
+ g_clear_object (&service);
+
+ return retval;
+}
+
+/* List videos related to a given one. */
+static int
+command_related (int argc, char *argv[])
+{
+ GDataYouTubeService *service = NULL;
+ GDataFeed *feed = NULL;
+ GList/*<unowned GDataYouTubeVideo>*/ *entries;
+ GError *error = NULL;
+ gint retval = 0;
+ const gchar *entry_id;
+ GDataYouTubeVideo *query_video = NULL;
+
+ if (argc < 3) {
+ return print_usage (argv);
+ }
+
+ entry_id = argv[2];
+ query_video = gdata_youtube_video_new (entry_id);
+
+ service = gdata_youtube_service_new (DEVELOPER_KEY, NULL);
+ feed = gdata_youtube_service_query_related (service, query_video, NULL,
+ NULL, NULL, NULL, &error);
+
+ if (error != NULL) {
+ g_printerr ("%s: Error querying YouTube: %s\n",
+ argv[0], error->message);
+ g_error_free (error);
+ retval = 1;
+ goto done;
+ }
+
+ /* Print results. */
+ for (entries = gdata_feed_get_entries (feed); entries != NULL;
+ entries = entries->next) {
+ GDataYouTubeVideo *video;
+
+ video = GDATA_YOUTUBE_VIDEO (entries->data);
+ print_video (video);
+ }
+
+ g_print ("Total of %u results.\n", gdata_feed_get_total_results (feed));
+
+done:
+ g_clear_object (&query_video);
+ g_clear_object (&feed);
+ g_clear_object (&service);
+
+ return retval;
+}
+
+/* List all available video categories. */
+static int
+command_categories (int argc, char *argv[])
+{
+ GDataYouTubeService *service = NULL;
+ GDataAPPCategories *app_categories = NULL;
+ GList/*<unowned GDataCategory>*/ *categories;
+ GError *error = NULL;
+ gint retval = 0;
+
+ service = gdata_youtube_service_new (DEVELOPER_KEY, NULL);
+ app_categories = gdata_youtube_service_get_categories (service, NULL,
+ &error);
+
+ if (error != NULL) {
+ g_printerr ("%s: Error querying YouTube: %s\n",
+ argv[0], error->message);
+ g_error_free (error);
+ retval = 1;
+ goto done;
+ }
+
+ /* Print results. */
+ for (categories = gdata_app_categories_get_categories (app_categories);
+ categories != NULL;
+ categories = categories->next) {
+ GDataCategory *category;
+
+ category = GDATA_CATEGORY (categories->data);
+ print_category (category);
+ }
+
+ g_print ("Total of %u results.\n",
+ g_list_length (gdata_app_categories_get_categories (app_categories)));
+
+done:
+ g_clear_object (&app_categories);
+ g_clear_object (&service);
+
+ return retval;
+}
+
+/* Upload a video. */
+static int
+command_upload (int argc, char *argv[])
+{
+ GDataYouTubeService *service = NULL;
+ GDataUploadStream *upload_stream = NULL;
+ GError *error = NULL;
+ gint retval = 0;
+ const gchar *filename;
+ GFile *video_file = NULL;
+ GFileInputStream *video_file_stream = NULL;
+ GFileInfo *video_file_info = NULL;
+ GDataYouTubeVideo *video = NULL;
+ GDataYouTubeVideo *uploaded_video = NULL;
+ gssize transfer_size;
+ const gchar *content_type, *slug;
+ GDataAuthorizer *authorizer = NULL;
+ const gchar *title, *description;
+
+ if (argc < 3) {
+ return print_usage (argv);
+ }
+
+ filename = argv[2];
+ title = (argc > 3) ? argv[3] : NULL;
+ description = (argc > 4) ? argv[4] : NULL;
+
+ /* Load the file and query its details. */
+ video_file = g_file_new_for_commandline_arg (filename);
+
+ video_file_info = g_file_query_info (video_file,
+ G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","
+ G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
+ G_FILE_QUERY_INFO_NONE, NULL,
+ &error);
+
+ if (error != NULL) {
+ g_printerr ("%s: Error loading video information ‘%s’: %s\n",
+ argv[0], filename, error->message);
+ g_error_free (error);
+ retval = 1;
+ goto done;
+ }
+
+ content_type = g_file_info_get_content_type (video_file_info);
+ slug = g_file_info_get_display_name (video_file_info);
+
+ video_file_stream = g_file_read (video_file, NULL, &error);
+
+ if (error != NULL) {
+ g_printerr ("%s: Error loading video ‘%s’: %s\n",
+ argv[0], filename, error->message);
+ g_error_free (error);
+ retval = 1;
+ goto done;
+ }
+
+ /* Build the video. */
+ video = gdata_youtube_video_new (NULL);
+ gdata_entry_set_title (GDATA_ENTRY (video), title);
+ gdata_entry_set_summary (GDATA_ENTRY (video), description);
+
+ /* Authenticate and create a service. */
+ authorizer = create_authorizer (&error);
+
+ if (error != NULL) {
+ g_printerr ("%s: Error authenticating: %s\n",
+ argv[0], error->message);
+ g_error_free (error);
+ retval = 1;
+ goto done;
+ } else if (authorizer == NULL) {
+ g_printerr ("%s: User chose to abort authentication.\n",
+ argv[0]);
+ retval = 1;
+ goto done;
+ }
+
+ service = gdata_youtube_service_new (DEVELOPER_KEY,
+ GDATA_AUTHORIZER (authorizer));
+
+ /* Start the upload. */
+ upload_stream = gdata_youtube_service_upload_video (service, video,
+ slug, content_type,
+ NULL, &error);
+
+ if (error != NULL) {
+ g_printerr ("%s: Error initializing upload with YouTube: %s\n",
+ argv[0], error->message);
+ g_error_free (error);
+ retval = 1;
+ goto done;
+ }
+
+ /* Upload the video */
+ transfer_size = g_output_stream_splice (G_OUTPUT_STREAM (upload_stream),
+ G_INPUT_STREAM (video_file_stream),
+ G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE |
+ G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
+ NULL, &error);
+
+ if (error != NULL) {
+ g_printerr ("%s: Error transferring file: %s\n",
+ argv[0], error->message);
+ g_error_free (error);
+ retval = 1;
+ goto done;
+ }
+
+ /* Finish off the upload */
+ uploaded_video = gdata_youtube_service_finish_video_upload (service,
+ upload_stream,
+ &error);
+
+ if (error != NULL) {
+ g_printerr ("%s: Error finishing upload with YouTube: %s\n",
+ argv[0], error->message);
+ g_error_free (error);
+ retval = 1;
+ goto done;
+ }
+
+ /* Print the uploaded video as confirmation. */
+ g_print ("Uploaded %" G_GSSIZE_FORMAT " bytes.\n", transfer_size);
+ print_video (uploaded_video);
+
+done:
+ g_clear_object (&authorizer);
+ g_clear_object (&uploaded_video);
+ g_clear_object (&video);
+ g_clear_object (&video_file_info);
+ g_clear_object (&video_file_stream);
+ g_clear_object (&video_file);
+ g_clear_object (&upload_stream);
g_clear_object (&service);
return retval;
@@ -80,9 +541,14 @@ done:
static const struct {
const gchar *command;
- int (*handler_fn) (char **argv);
+ int (*handler_fn) (int argc, char **argv);
} command_handlers[] = {
{ "search", command_search },
+ { "info", command_info },
+ { "standard-feed", command_standard_feed },
+ { "categories", command_categories },
+ { "related", command_related },
+ { "upload", command_upload },
};
int
@@ -93,13 +559,13 @@ main (int argc, char *argv[])
setlocale (LC_ALL, "");
- if (argc < 3) {
+ if (argc < 2) {
return print_usage (argv);
}
for (i = 0; i < G_N_ELEMENTS (command_handlers); i++) {
if (strcmp (argv[1], command_handlers[i].command) == 0) {
- retval = command_handlers[i].handler_fn (argv);
+ retval = command_handlers[i].handler_fn (argc, argv);
}
}
diff --git a/gdata/app/gdata-app-categories.c b/gdata/app/gdata-app-categories.c
index 375a1e2..d7da25b 100644
--- a/gdata/app/gdata-app-categories.c
+++ b/gdata/app/gdata-app-categories.c
@@ -44,6 +44,11 @@ static void gdata_app_categories_get_property (GObject *object, guint property_i
static gboolean pre_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *root_node, gpointer user_data,
GError **error);
static gboolean parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError
**error);
static gboolean post_parse_xml (GDataParsable *parsable, gpointer user_data, GError **error);
+static gboolean
+parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data,
+ GError **error);
+static gboolean
+post_parse_json (GDataParsable *parsable, gpointer user_data, GError **error);
struct _GDataAPPCategoriesPrivate {
GList *categories;
@@ -69,9 +74,14 @@ gdata_app_categories_class_init (GDataAPPCategoriesClass *klass)
gobject_class->dispose = gdata_app_categories_dispose;
gobject_class->finalize = gdata_app_categories_finalize;
+ /* TODO: eliminate */
parsable_class->pre_parse_xml = pre_parse_xml;
parsable_class->parse_xml = parse_xml;
parsable_class->post_parse_xml = post_parse_xml;
+
+ parsable_class->parse_json = parse_json;
+ parsable_class->post_parse_json = post_parse_json;
+
parsable_class->element_name = "categories";
parsable_class->element_namespace = "app";
@@ -195,6 +205,72 @@ post_parse_xml (GDataParsable *parsable, gpointer user_data, GError **error)
return TRUE;
}
+/* Reference: https://developers.google.com/youtube/v3/docs/videoCategories/list */
+static gboolean
+parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error)
+{
+ GDataAPPCategories *self = GDATA_APP_CATEGORIES (parsable);
+ GDataAPPCategoriesPrivate *priv = self->priv;
+
+ if (g_strcmp0 (json_reader_get_member_name (reader), "items") == 0) {
+ guint i, elements;
+
+ /* Loop through the elements array. */
+ for (i = 0, elements = (guint) json_reader_count_elements (reader); i < elements; i++) {
+ GDataCategory *category = NULL;
+ const gchar *id, *title;
+ const GError *child_error = NULL;
+
+ json_reader_read_element (reader, i);
+
+ json_reader_read_member (reader, "id");
+ id = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ json_reader_read_member (reader, "snippet");
+
+ json_reader_read_member (reader, "title");
+ title = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ child_error = json_reader_get_error (reader);
+
+ if (child_error != NULL) {
+ return gdata_parser_error_from_json_error (reader,
+ child_error,
+ error);
+ }
+
+ /* Create the category. */
+ category = gdata_category_new (id, NULL, title);
+ priv->categories = g_list_prepend (priv->categories,
+ category);
+
+ json_reader_end_member (reader); /* snippet */
+ json_reader_end_element (reader); /* category */
+ }
+ } else if (g_strcmp0 (json_reader_get_member_name (reader), "kind") == 0 ||
+ g_strcmp0 (json_reader_get_member_name (reader), "etag") == 0 ||
+ g_strcmp0 (json_reader_get_member_name (reader), "id") == 0) {
+ /* Ignore. */
+ } else {
+ return GDATA_PARSABLE_CLASS (gdata_app_categories_parent_class)->parse_json (parsable,
reader, user_data, error);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+post_parse_json (GDataParsable *parsable, gpointer user_data, GError **error)
+{
+ GDataAPPCategoriesPrivate *priv = GDATA_APP_CATEGORIES (parsable)->priv;
+
+ /* Reverse our lists of stuff. */
+ priv->categories = g_list_reverse (priv->categories);
+
+ return TRUE;
+}
+
/**
* gdata_app_categories_get_categories:
* @self: a #GDataAPPCategories
diff --git a/gdata/gdata-feed.c b/gdata/gdata-feed.c
index 9609c8f..8031586 100644
--- a/gdata/gdata-feed.c
+++ b/gdata/gdata-feed.c
@@ -629,6 +629,20 @@ parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GEr
/* Ignore. */
} else if (g_strcmp0 (json_reader_get_member_name (reader), "etag") == 0) {
GDATA_FEED (parsable)->priv->etag = g_strdup (json_reader_get_string_value (reader));
+ } else if (g_strcmp0 (json_reader_get_member_name (reader), "pageInfo") == 0) {
+ /* Check this is an object. */
+ if (!json_reader_is_object (reader)) {
+ return gdata_parser_error_required_json_content_missing (reader, error);
+ }
+
+ /* https://developers.google.com/youtube/v3/docs/playlists/list/ */
+ json_reader_read_member (reader, "totalResults");
+ self->priv->total_results = json_reader_get_int_value (reader);
+ json_reader_end_member (reader);
+
+ json_reader_read_member (reader, "resultsPerPage");
+ self->priv->items_per_page = json_reader_get_int_value (reader);
+ json_reader_end_member (reader);
} else {
return GDATA_PARSABLE_CLASS (gdata_feed_parent_class)->parse_json (parsable, reader,
user_data, error);
}
diff --git a/gdata/gdata-parser.c b/gdata/gdata-parser.c
index 0e655a5..19518eb 100644
--- a/gdata/gdata-parser.c
+++ b/gdata/gdata-parser.c
@@ -305,8 +305,9 @@ gdata_parser_error_not_iso8601_format_json (JsonReader *reader, const gchar *act
return FALSE;
}
-static gboolean
-parser_error_from_json_error (JsonReader *reader, const GError *json_error, GError **error)
+gboolean
+gdata_parser_error_from_json_error (JsonReader *reader,
+ const GError *json_error, GError **error)
{
g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
/* Translators: the parameter is an error message. */
@@ -776,7 +777,9 @@ gdata_parser_string_from_json_member (JsonReader *reader, const gchar *member_na
text = json_reader_get_string_value (reader);
child_error = json_reader_get_error (reader);
if (child_error != NULL) {
- *success = parser_error_from_json_error (reader, child_error, error);
+ *success = gdata_parser_error_from_json_error (reader,
+ child_error,
+ error);
return TRUE;
} else if ((options & P_REQUIRED && text == NULL) || (options & P_NON_EMPTY && text != NULL && *text
== '\0')) {
*success = gdata_parser_error_required_json_content_missing (reader, error);
@@ -794,6 +797,75 @@ gdata_parser_string_from_json_member (JsonReader *reader, const gchar *member_na
}
/*
+ * gdata_parser_int64_from_json_member:
+ * @reader: #JsonReader cursor object to read JSON node from
+ * @member_name: the name of the member to parse
+ * @options: a bitwise combination of parsing options from #GDataParserOptions,
+ * or %P_NONE
+ * @output: the return location for the parsed integer content
+ * @success: the return location for a value which is %TRUE if the integer was
+ * parsed successfully, %FALSE if an error was encountered, and undefined if
+ * @element didn't match @element_name
+ * @error: a #GError, or %NULL
+ *
+ * Gets the integer content of @element if its name is @element_name, subject to
+ * various checks specified by @options.
+ *
+ * If @element doesn't match @element_name, %FALSE will be returned, @error will
+ * be unset and @success will be unset.
+ *
+ * If @element matches @element_name but one of the checks specified by @options
+ * fails, %TRUE will be returned, @error will be set to a
+* %GDATA_SERVICE_ERROR_PROTOCOL_ERROR error and @success will be set to %FALSE.
+ *
+ * If @element matches @element_name and all of the checks specified by @options
+ * pass, %TRUE will be returned, @error will be unset and @success will be set
+ * to %TRUE.
+ *
+ * The reason for returning the success of the parsing in @success is so that
+ * calls to gdata_parser_int_from_element() can be chained together in a large
+ * "or" statement based on their return values, for the purposes of determining
+ * whether any of the calls matched a given @element. If any of the calls to
+ * gdata_parser_int_from_element() return %TRUE, the value of @success can be
+ * examined.
+ *
+ * Return value: %TRUE if @element matched @element_name, %FALSE otherwise
+ *
+ * Since: UNRELEASED
+ */
+gboolean
+gdata_parser_int_from_json_member (JsonReader *reader,
+ const gchar *member_name,
+ GDataParserOptions options,
+ gint64 *output, gboolean *success,
+ GError **error)
+{
+ gint64 value;
+ const GError *child_error = NULL;
+
+ /* Check if there's such element */
+ if (g_strcmp0 (json_reader_get_member_name (reader), member_name) != 0) {
+ return FALSE;
+ }
+
+ value = json_reader_get_int_value (reader);
+ child_error = json_reader_get_error (reader);
+
+ if (child_error != NULL) {
+ *success = gdata_parser_error_from_json_error (reader,
+ child_error,
+ error);
+ return TRUE;
+ }
+
+ /* Success! */
+ *output = value;
+ *success = TRUE;
+
+ return TRUE;
+}
+
+/*
* gdata_parser_int64_time_from_json_member:
* @reader: #JsonReader cursor object to read JSON node from
* @element_name: the name of the element to parse
@@ -842,7 +914,9 @@ gdata_parser_int64_time_from_json_member (JsonReader *reader, const gchar *membe
text = json_reader_get_string_value (reader);
child_error = json_reader_get_error (reader);
if (child_error != NULL) {
- *success = parser_error_from_json_error (reader, child_error, error);
+ *success = gdata_parser_error_from_json_error (reader,
+ child_error,
+ error);
return TRUE;
} else if (options & P_REQUIRED && (text == NULL || *text == '\0')) {
*success = gdata_parser_error_required_json_content_missing (reader, error);
@@ -905,7 +979,9 @@ gdata_parser_boolean_from_json_member (JsonReader *reader, const gchar *member_n
val = json_reader_get_boolean_value (reader);
child_error = json_reader_get_error (reader);
if (child_error != NULL) {
- *success = parser_error_from_json_error (reader, child_error, error);
+ *success = gdata_parser_error_from_json_error (reader,
+ child_error,
+ error);
return TRUE;
}
@@ -916,6 +992,106 @@ gdata_parser_boolean_from_json_member (JsonReader *reader, const gchar *member_n
return TRUE;
}
+/*
+ * gdata_parser_strv_from_json_member:
+ * @reader: #JsonReader cursor object to read JSON node from
+ * @element_name: the name of the element to parse
+ * @options: a bitwise combination of parsing options from #GDataParserOptions,
+ * or %P_NONE
+ * @output: (out callee-allocates) (transfer full): the return location for the
+ * parsed string array
+ * @success: the return location for a value which is %TRUE if the string array
+ * was parsed successfully, %FALSE if an error was encountered, and undefined
+ * if @element didn't match @element_name
+ * @error: a #GError, or %NULL
+ *
+ * Gets the string array of @element if its name is @element_name, subject to
+ * various checks specified by @options. It expects the @element to be an array
+ * of strings.
+ *
+ * If @element doesn't match @element_name, %FALSE will be returned, @error will
+ * be unset and @success will be unset.
+ *
+ * If @element matches @element_name but one of the checks specified by @options
+ * fails, %TRUE will be returned, @error will be set to a
+ * %GDATA_SERVICE_ERROR_PROTOCOL_ERROR error and @success will be set to %FALSE.
+ *
+ * If @element matches @element_name and all of the checks specified by @options
+ * pass, %TRUE will be returned, @error will be unset and @success will be set
+ * to %TRUE.
+ *
+ * The reason for returning the success of the parsing in @success is so that
+ * calls to gdata_parser_strv_from_element() can be chained together in a large
+ * "or" statement based on their return values, for the purposes of determining
+ * whether any of the calls matched a given @element. If any of the calls to
+ * gdata_parser_strv_from_element() return %TRUE, the value of @success can be
+ * examined.
+ *
+ * Return value: %TRUE if @element matched @element_name, %FALSE otherwise
+ *
+ * Since: UNRELEASED
+ */
+gboolean
+gdata_parser_strv_from_json_member (JsonReader *reader,
+ const gchar *member_name,
+ GDataParserOptions options,
+ gchar ***output, gboolean *success,
+ GError **error)
+{
+ guint i, len;
+ GPtrArray *out;
+ const GError *child_error = NULL;
+
+ /* Check if there's such element */
+ if (g_strcmp0 (json_reader_get_member_name (reader),
+ member_name) != 0) {
+ return FALSE;
+ }
+
+ /* Check if the output strv has already been set. The JSON parser
+ * guarantees this can't happen. */
+ g_assert (!(options & P_NO_DUPES) || *output == NULL);
+
+ len = json_reader_count_elements (reader);
+ child_error = json_reader_get_error (reader);
+
+ if (child_error != NULL) {
+ *success = gdata_parser_error_from_json_error (reader,
+ child_error,
+ error);
+ return TRUE;
+ }
+
+ out = g_ptr_array_new_full (len + 1 /* NULL terminator */, g_free);
+
+ for (i = 0; i < len; i++) {
+ const gchar *val;
+
+ json_reader_read_element (reader, i);
+ val = json_reader_get_string_value (reader);
+ child_error = json_reader_get_error (reader);
+
+ if (child_error != NULL) {
+ *success = gdata_parser_error_from_json_error (reader,
+ child_error,
+ error);
+ json_reader_end_element (reader);
+ g_ptr_array_unref (out);
+ return TRUE;
+ }
+
+ g_ptr_array_add (out, g_strdup (val));
+
+ json_reader_end_element (reader);
+ }
+
+ /* Success! */
+ *output = (gchar **) g_ptr_array_free (out, FALSE);
+ *success = TRUE;
+
+ return TRUE;
+}
+
void
gdata_parser_string_append_escaped (GString *xml_string, const gchar *pre, const gchar *element_content,
const gchar *post)
{
diff --git a/gdata/gdata-parser.h b/gdata/gdata-parser.h
index 2c5ad2f..4eae043 100644
--- a/gdata/gdata-parser.h
+++ b/gdata/gdata-parser.h
@@ -38,6 +38,9 @@ gboolean gdata_parser_error_duplicate_element (xmlNode *element, GError **error)
gboolean gdata_parser_error_duplicate_json_element (JsonReader *reader, GError **error);
gboolean gdata_parser_error_required_json_content_missing (JsonReader *reader, GError **error);
gboolean gdata_parser_error_not_iso8601_format_json (JsonReader *reader, const gchar *actual_value, GError
**error);
+gboolean
+gdata_parser_error_from_json_error (JsonReader *reader,
+ const GError *json_error, GError **error);
gboolean gdata_parser_int64_from_date (const gchar *date, gint64 *_time);
gchar *gdata_parser_date_from_int64 (gint64 _time) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
@@ -86,10 +89,24 @@ gboolean gdata_parser_object_from_element (xmlNode *element, const gchar *elemen
gpointer /* GDataParsable ** */ _output, gboolean *success,
GError **error);
gboolean gdata_parser_string_from_json_member (JsonReader *reader, const gchar *member_name,
GDataParserOptions options,
gchar **output, gboolean *success, GError **error);
+gboolean
+gdata_parser_int_from_json_member (JsonReader *reader,
+ const gchar *member_name,
+ GDataParserOptions options,
+ gint64 *output, gboolean *success,
+ GError **error);
+gboolean gdata_parser_int64_from_json_member (JsonReader *reader, const gchar *member_name,
GDataParserOptions options,
+ gint64 *output, gboolean *success, GError **error);
gboolean gdata_parser_int64_time_from_json_member (JsonReader *reader, const gchar *member_name,
GDataParserOptions options,
gint64 *output, gboolean *success, GError **error);
gboolean gdata_parser_boolean_from_json_member (JsonReader *reader, const gchar *member_name,
GDataParserOptions options,
gboolean *output, gboolean *success, GError **error);
+gboolean
+gdata_parser_strv_from_json_member (JsonReader *reader,
+ const gchar *member_name,
+ GDataParserOptions options,
+ gchar ***output, gboolean *success,
+ GError **error);
void gdata_parser_string_append_escaped (GString *xml_string, const gchar *pre, const gchar
*element_content, const gchar *post);
gchar *gdata_parser_utf8_trim_whitespace (const gchar *s) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
diff --git a/gdata/gdata-upload-stream.c b/gdata/gdata-upload-stream.c
index 87d8a31..468f7b8 100644
--- a/gdata/gdata-upload-stream.c
+++ b/gdata/gdata-upload-stream.c
@@ -451,22 +451,47 @@ gdata_upload_stream_constructor (GType type, guint n_construct_params, GObjectCo
/* The Content-Type should be multipart/related if we're also uploading the metadata (entry
!= NULL),
* and the given content_type otherwise. */
if (priv->entry != NULL) {
- const gchar *first_part_header;
- gchar *entry_xml, *second_part_header;
+ gchar *first_part_header, *upload_data;
+ gchar *second_part_header;
+ GDataParsableClass *klass;
+
+ klass = GDATA_PARSABLE_GET_CLASS (priv->entry);
+ g_assert (klass->get_content_type != NULL);
soup_message_headers_set_content_type (priv->message->request_headers,
"multipart/related; boundary=" BOUNDARY_STRING, NULL);
+ if (g_strcmp0 (klass->get_content_type (), "application/json") == 0) {
+ upload_data = gdata_parsable_get_json (GDATA_PARSABLE (priv->entry));
+ } else {
+ upload_data = gdata_parsable_get_xml (GDATA_PARSABLE (priv->entry));
+ }
+
/* Start by writing out the entry; then the thread has something to write to the
network when it's created */
- first_part_header = "--" BOUNDARY_STRING "\nContent-Type: application/atom+xml;
charset=UTF-8\n\n";
- entry_xml = gdata_parsable_get_xml (GDATA_PARSABLE (priv->entry));
- second_part_header = g_strdup_printf ("\n--" BOUNDARY_STRING "\nContent-Type:
%s\nContent-Transfer-Encoding: binary\n\n",
+ first_part_header = g_strdup_printf ("--" BOUNDARY_STRING "\n"
+ "Content-Type: %s; charset=UTF-8\n\n",
+ klass->get_content_type ());
+ second_part_header = g_strdup_printf ("\n--" BOUNDARY_STRING "\n"
+ "Content-Type: %s\n"
+ "Content-Transfer-Encoding: binary\n\n",
priv->content_type);
/* Push the message parts onto the message body; we can skip the buffer, since the
network thread hasn't yet been created,
* so we're the sole thread accessing the SoupMessage. */
- soup_message_body_append (priv->message->request_body, SOUP_MEMORY_STATIC,
first_part_header, strlen (first_part_header));
- soup_message_body_append (priv->message->request_body, SOUP_MEMORY_TAKE, entry_xml,
strlen (entry_xml));
- soup_message_body_append (priv->message->request_body, SOUP_MEMORY_TAKE,
second_part_header, strlen (second_part_header));
+ soup_message_body_append (priv->message->request_body,
+ SOUP_MEMORY_TAKE,
+ first_part_header,
+ strlen (first_part_header));
+ soup_message_body_append (priv->message->request_body,
+ SOUP_MEMORY_TAKE, upload_data,
+ strlen (upload_data));
+ soup_message_body_append (priv->message->request_body,
+ SOUP_MEMORY_TAKE,
+ second_part_header,
+ strlen (second_part_header));
+
+ first_part_header = NULL;
+ upload_data = NULL;
+ second_part_header = NULL;
priv->network_bytes_outstanding = priv->message->request_body->length;
} else {
@@ -487,12 +512,30 @@ gdata_upload_stream_constructor (GType type, guint n_construct_params, GObjectCo
g_free (content_length_str);
if (priv->entry != NULL) {
- const gchar *entry_xml;
-
- soup_message_headers_set_content_type (priv->message->request_headers,
"application/atom+xml; charset=UTF-8", NULL);
-
- entry_xml = gdata_parsable_get_xml (GDATA_PARSABLE (priv->entry));
- soup_message_body_append (priv->message->request_body, SOUP_MEMORY_TAKE, entry_xml,
strlen (entry_xml));
+ GDataParsableClass *klass;
+ gchar *content_type, *upload_data;
+
+ klass = GDATA_PARSABLE_GET_CLASS (priv->entry);
+ g_assert (klass->get_content_type != NULL);
+
+ if (g_strcmp0 (klass->get_content_type (), "application/json") == 0) {
+ upload_data = gdata_parsable_get_json (GDATA_PARSABLE (priv->entry));
+ } else {
+ upload_data = gdata_parsable_get_xml (GDATA_PARSABLE (priv->entry));
+ }
+
+ content_type = g_strdup_printf ("%s; charset=UTF-8",
+ klass->get_content_type ());
+ soup_message_headers_set_content_type (priv->message->request_headers,
+ content_type,
+ NULL);
+ g_free (content_type);
+
+ soup_message_body_append (priv->message->request_body,
+ SOUP_MEMORY_TAKE,
+ upload_data,
+ strlen (upload_data));
+ upload_data = NULL;
priv->network_bytes_outstanding = priv->message->request_body->length;
} else {
diff --git a/gdata/media/gdata-media-thumbnail.c b/gdata/media/gdata-media-thumbnail.c
index 413e0e9..cbd68ac 100644
--- a/gdata/media/gdata-media-thumbnail.c
+++ b/gdata/media/gdata-media-thumbnail.c
@@ -43,6 +43,9 @@ static void gdata_media_thumbnail_finalize (GObject *object);
static void gdata_media_thumbnail_get_property (GObject *object, guint property_id, GValue *value,
GParamSpec *pspec);
static gboolean pre_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *root_node, gpointer user_data,
GError **error);
static void get_namespaces (GDataParsable *parsable, GHashTable *namespaces);
+static gboolean
+parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data,
+ GError **error);
struct _GDataMediaThumbnailPrivate {
gchar *uri;
@@ -73,6 +76,7 @@ gdata_media_thumbnail_class_init (GDataMediaThumbnailClass *klass)
parsable_class->pre_parse_xml = pre_parse_xml;
parsable_class->get_namespaces = get_namespaces;
+ parsable_class->parse_json = parse_json;
parsable_class->element_name = "thumbnail";
parsable_class->element_namespace = "media";
@@ -293,6 +297,33 @@ get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
g_hash_table_insert (namespaces, (gchar*) "media", (gchar*) "http://search.yahoo.com/mrss/");
}
+/* Reference:
+ * https://developers.google.com/youtube/v3/docs/videos#snippet.thumbnails */
+static gboolean
+parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data,
+ GError **error)
+{
+ gboolean success;
+ GDataMediaThumbnail *self = GDATA_MEDIA_THUMBNAIL (parsable);
+ GDataMediaThumbnailPrivate *priv = self->priv;
+
+ if (gdata_parser_string_from_json_member (reader, "url", P_DEFAULT,
+ &priv->uri, &success,
+ error) ||
+ gdata_parser_int_from_json_member (reader, "width", P_DEFAULT,
+ (gint64 *) &priv->width,
+ &success, error) ||
+ gdata_parser_int_from_json_member (reader, "height", P_DEFAULT,
+ (gint64 *) &priv->height,
+ &success, error)) {
+ return success;
+ } else {
+ return GDATA_PARSABLE_CLASS (gdata_media_thumbnail_parent_class)->parse_json (parsable,
reader, user_data, error);
+ }
+
+ return TRUE;
+}
+
/**
* gdata_media_thumbnail_get_uri:
* @self: a #GDataMediaThumbnail
diff --git a/gdata/services/youtube/gdata-youtube-query.c b/gdata/services/youtube/gdata-youtube-query.c
index bf95730..ccc87a0 100644
--- a/gdata/services/youtube/gdata-youtube-query.c
+++ b/gdata/services/youtube/gdata-youtube-query.c
@@ -26,6 +26,11 @@
* #GDataYouTubeQuery represents a collection of query parameters specific to the YouTube service, which go
above and beyond
* those catered for by #GDataQuery.
*
+ * With the transition to version 3 of the YouTube, the #GDataQuery:author and
+ * #GDataQuery:start-index properties are no longer supported, and their values
+ * will be ignored. Use gdata_query_next_page() instead of the
+ * #GDataQuery:start-index API.
+ *
* For more information on the custom GData query parameters supported by #GDataYouTubeQuery, see the <ulink
type="http"
* url="http://code.google.com/apis/youtube/2.0/reference.html#Custom_parameters">online
documentation</ulink>.
*
@@ -101,12 +106,15 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
* retrieve videos irrespective of their format availability.
*
* Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ * this property will be unused in queries.
**/
g_object_class_install_property (gobject_class, PROP_FORMAT,
g_param_spec_enum ("format",
"Format", "Specifies that videos must be
available in a particular video format.",
GDATA_TYPE_YOUTUBE_FORMAT,
GDATA_YOUTUBE_FORMAT_UNKNOWN,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |
+ G_PARAM_DEPRECATED));
/**
* GDataYouTubeQuery:latitude:
@@ -119,9 +127,8 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
* If #GDataYouTubeQuery:location-radius is a non-<code class="literal">0</code> value, this will
define a circle from which videos should be
* found.
*
- * If #GDataYouTubeQuery:has-location is %TRUE, only videos which are associated with specific
coordinates in the search
- * circle will be returned. Otherwise, videos which have a relevant descriptive address (but no
specific coordinates) will
- * also be returned.
+ * As it is deprecated, the value of #GDataYouTubeQuery:has-location is
+ * ignored.
*
* For more information, see the <ulink type="http"
* url="http://code.google.com/apis/youtube/2.0/reference.html#locationsp">online
documentation</ulink>.
@@ -131,7 +138,7 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
g_object_class_install_property (gobject_class, PROP_LATITUDE,
g_param_spec_double ("latitude",
"Latitude", "The latitude of a particular
location of which videos should be found.",
- -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
+ -G_MAXDOUBLE, G_MAXDOUBLE, G_MAXDOUBLE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
/**
@@ -149,7 +156,7 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
g_object_class_install_property (gobject_class, PROP_LONGITUDE,
g_param_spec_double ("longitude",
"Longitude", "The longitude of a particular
location of which videos should be found.",
- -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
+ -G_MAXDOUBLE, G_MAXDOUBLE, G_MAXDOUBLE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
/**
@@ -181,12 +188,15 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
* For more information, see the documentation for #GDataYouTubeQuery:latitude.
*
* Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ * this property will be unused in queries.
**/
g_object_class_install_property (gobject_class, PROP_HAS_LOCATION,
g_param_spec_boolean ("has-location",
"Has location?", "Whether to restrict results
to videos with specific coordinates.",
FALSE,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |
+ G_PARAM_DEPRECATED));
/**
* GDataYouTubeQuery:language:
@@ -199,12 +209,15 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
* url="http://code.google.com/apis/youtube/2.0/reference.html#lrsp">online documentation</ulink>.
*
* Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ * this property will be unused in queries.
**/
g_object_class_install_property (gobject_class, PROP_LANGUAGE,
g_param_spec_string ("language",
"Language", "Restricts the search to videos
described in the given language.",
NULL,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |
+ G_PARAM_DEPRECATED));
/**
* GDataYouTubeQuery:order-by:
@@ -230,9 +243,13 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
/**
* GDataYouTubeQuery:restriction:
*
- * The IP address that should be used to filter videos playable only in specific countries. This
should be the
- * IP address of the client, if different to the IP address in use by the library. Alternatively, it
can be an ISO 3166
- * two-letter country code.
+ * An ISO 3166 two-letter country code that should be used to filter
+ * videos playable only in specific countries.
+ *
+ * Previously, this property could also accept the client’s IP address
+ * for country lookup. This feature is no longer supported by Google,
+ * and will result in an error from the server if used. Use a country
+ * code instead.
*
* For more information, see the <ulink type="http"
* url="http://code.google.com/apis/youtube/2.0/reference.html#restrictionsp">online
documentation</ulink>.
@@ -241,7 +258,7 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
**/
g_object_class_install_property (gobject_class, PROP_RESTRICTION,
g_param_spec_string ("restriction",
- "Restriction", "The IP address to filter videos
playable only in specific countries.",
+ "Restriction", "The country code to filter
videos playable only in specific countries.",
NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
@@ -267,12 +284,15 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
* Specifies the direction of sorting. To use the default sort order, set the property to
%GDATA_YOUTUBE_SORT_NONE.
*
* Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ * this property will be unused in queries.
**/
g_object_class_install_property (gobject_class, PROP_SORT_ORDER,
g_param_spec_enum ("sort-order",
"Sort order", "Specifies the direction of
sorting.",
GDATA_TYPE_YOUTUBE_SORT_ORDER,
GDATA_YOUTUBE_SORT_NONE,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |
+ G_PARAM_DEPRECATED));
/**
* GDataYouTubeQuery:age:
@@ -295,12 +315,15 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
* searches to videos from YouTube partners.
*
* Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ * this property will be unused in queries.
**/
g_object_class_install_property (gobject_class, PROP_UPLOADER,
g_param_spec_enum ("uploader",
"Uploader", "Restricts the search to videos from
the specified type of uploader.",
GDATA_TYPE_YOUTUBE_UPLOADER,
GDATA_YOUTUBE_UPLOADER_ALL,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |
+ G_PARAM_DEPRECATED));
/**
* GDataYouTubeQuery:license:
@@ -325,6 +348,9 @@ static void
gdata_youtube_query_init (GDataYouTubeQuery *self)
{
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_YOUTUBE_QUERY, GDataYouTubeQueryPrivate);
+
+ self->priv->latitude = G_MAXDOUBLE;
+ self->priv->longitude = G_MAXDOUBLE;
}
static void
@@ -445,6 +471,56 @@ gdata_youtube_query_set_property (GObject *object, guint property_id, const GVal
}
}
+/* Convert from a v2 order-by parameter value to a v3 order parameter value.
+ * Reference:
+ * v2: https://developers.google.com/youtube/2.0/developers_guide_protocol_api_query_parameters#orderbysp
+ * v3: https://developers.google.com/youtube/v3/docs/search/list#order
+ */
+static const gchar *
+get_v3_order (const gchar *v2_order_by)
+{
+ const struct {
+ const gchar *v2_order_by;
+ const gchar *v3_order;
+ } mapping[] = {
+ { "relevance", "relevance" },
+ { "published", "date" },
+ { "viewCount", "viewCount" },
+ { "rating", "rating" },
+ };
+ guint i;
+
+ for (i = 0; i < G_N_ELEMENTS (mapping); i++) {
+ if (g_strcmp0 (v2_order_by, mapping[i].v2_order_by) == 0) {
+ return mapping[i].v3_order;
+ }
+ }
+
+ /* Special case for ‘relevance_lang_*’. */
+ if (g_str_has_prefix (v2_order_by, "relevance_lang_")) {
+ return "relevance";
+ }
+
+ return NULL;
+}
+
+/* Convert from a v2 license parameter value to a v3 videoLicense parameter
+ * value. Reference:
+ * v2: https://developers.google.com/youtube/2.0/developers_guide_protocol_api_query_parameters#licensesp
+ * v3: https://developers.google.com/youtube/v3/docs/search/list#videoLicense
+ */
+static const gchar *
+get_v3_video_license (const gchar *v2_license)
+{
+ if (g_strcmp0 (v2_license, "cc") == 0) {
+ return "creativeCommon";
+ } else if (g_strcmp0 (v2_license, "youtube") == 0) {
+ return "youtube";
+ } else {
+ return NULL;
+ }
+}
+
static void
get_query_uri (GDataQuery *self, const gchar *feed_uri, GString *query_uri, gboolean *params_started)
{
@@ -452,91 +528,127 @@ get_query_uri (GDataQuery *self, const gchar *feed_uri, GString *query_uri, gboo
#define APPEND_SEP g_string_append_c (query_uri, (*params_started == FALSE) ? '?' : '&');
*params_started = TRUE;
- /* Chain up to the parent class */
- GDATA_QUERY_CLASS (gdata_youtube_query_parent_class)->get_query_uri (self, feed_uri, query_uri,
params_started);
+ /* NOTE: We do not chain up because the parent class implements a lot
+ * of deprecated API. */
+
+ /* Categories */
+ if (gdata_query_get_categories (self) != NULL) {
+ APPEND_SEP
+ g_string_append (query_uri, "videoCategoryId=");
+ g_string_append_uri_escaped (query_uri,
+ gdata_query_get_categories (self),
+ NULL,
+ FALSE);
+ }
- APPEND_SEP
- switch (priv->age) {
+ /* q param */
+ if (gdata_query_get_q (self) != NULL) {
+ APPEND_SEP
+ g_string_append (query_uri, "q=");
+ g_string_append_uri_escaped (query_uri,
+ gdata_query_get_q (self), NULL,
+ FALSE);
+ }
+
+ if (gdata_query_get_max_results (self) > 0) {
+ APPEND_SEP
+ g_string_append_printf (query_uri, "maxResults=%u",
+ gdata_query_get_max_results (self));
+ }
+
+ if (priv->age != GDATA_YOUTUBE_AGE_ALL_TIME) {
+ gchar *after;
+ GTimeVal tv = { 0, };
+
+ g_get_current_time (&tv);
+
+ switch (priv->age) {
case GDATA_YOUTUBE_AGE_TODAY:
- g_string_append (query_uri, "time=today");
+ tv.tv_sec -= 24 * 60 * 60;
break;
case GDATA_YOUTUBE_AGE_THIS_WEEK:
- g_string_append (query_uri, "time=this_week");
+ tv.tv_sec -= 7 * 24 * 60 * 60;
break;
case GDATA_YOUTUBE_AGE_THIS_MONTH:
- g_string_append (query_uri, "time=this_month");
+ tv.tv_sec -= 31 * 24 * 60 * 60;
break;
case GDATA_YOUTUBE_AGE_ALL_TIME:
- g_string_append (query_uri, "time=all_time");
- break;
default:
g_assert_not_reached ();
+ }
+
+ APPEND_SEP
+
+ after = g_time_val_to_iso8601 (&tv);
+ g_string_append_printf (query_uri, "publishedAfter=%s", after);
+ g_free (after);
}
- /* We don't need to use APPEND_SEP below here, as the "age" parameter is always included */
+ /* We don’t need to use APPEND_SEP below here, as this parameter is
+ * always included */
+ APPEND_SEP
switch (priv->safe_search) {
case GDATA_YOUTUBE_SAFE_SEARCH_NONE:
- g_string_append (query_uri, "&safeSearch=none");
+ g_string_append (query_uri, "safeSearch=none");
break;
case GDATA_YOUTUBE_SAFE_SEARCH_MODERATE:
- g_string_append (query_uri, "&safeSearch=moderate");
+ g_string_append (query_uri, "safeSearch=moderate");
break;
case GDATA_YOUTUBE_SAFE_SEARCH_STRICT:
- g_string_append (query_uri, "&safeSearch=strict");
+ g_string_append (query_uri, "safeSearch=strict");
break;
default:
g_assert_not_reached ();
}
- if (priv->format != GDATA_YOUTUBE_FORMAT_UNKNOWN)
- g_string_append_printf (query_uri, "&format=%u", priv->format);
-
if (priv->latitude >= -90.0 && priv->latitude <= 90.0 &&
priv->longitude >= -180.0 && priv->longitude <= 180.0) {
- gchar latitude[G_ASCII_DTOSTR_BUF_SIZE], longitude[G_ASCII_DTOSTR_BUF_SIZE];
+ gchar latitude[G_ASCII_DTOSTR_BUF_SIZE];
+ gchar longitude[G_ASCII_DTOSTR_BUF_SIZE];
- g_string_append_printf (query_uri, (priv->has_location == TRUE) ? "&location=%s,%s!" :
"&location=%s,%s",
- g_ascii_dtostr (latitude, sizeof (latitude), priv->latitude),
- g_ascii_dtostr (longitude, sizeof (longitude), priv->longitude));
+ g_string_append_printf (query_uri, "&location=%s,%s",
+ g_ascii_dtostr (latitude,
+ sizeof (latitude),
+ priv->latitude),
+ g_ascii_dtostr (longitude,
+ sizeof (longitude),
+ priv->longitude));
if (priv->location_radius >= 0.0) {
gchar radius[G_ASCII_DTOSTR_BUF_SIZE];
- g_string_append_printf (query_uri, "&location-radius=%sm", g_ascii_dtostr (radius,
sizeof (radius), priv->location_radius));
+ g_string_append_printf (query_uri, "&locationRadius=%sm",
+ g_ascii_dtostr (radius,
+ sizeof (radius),
+ priv->location_radius));
}
- } else if (priv->has_location == TRUE) {
- g_string_append (query_uri, "&location=!");
- }
-
- if (priv->language != NULL) {
- g_string_append (query_uri, "&lr=");
- g_string_append_uri_escaped (query_uri, priv->language, NULL, FALSE);
}
if (priv->order_by != NULL) {
- g_string_append (query_uri, "&orderby=");
- g_string_append_uri_escaped (query_uri, priv->order_by, NULL, FALSE);
+ const gchar *v3_order_by = get_v3_order (priv->order_by);
+
+ if (v3_order_by != NULL) {
+ g_string_append (query_uri, "&order=");
+ g_string_append_uri_escaped (query_uri, v3_order_by,
+ NULL, FALSE);
+ }
}
if (priv->restriction != NULL) {
- g_string_append (query_uri, "&restriction=");
+ g_string_append (query_uri, "®ionCode=");
g_string_append_uri_escaped (query_uri, priv->restriction, NULL, FALSE);
}
- if (priv->sort_order != GDATA_YOUTUBE_SORT_NONE) {
- if (priv->sort_order == GDATA_YOUTUBE_SORT_ASCENDING)
- g_string_append (query_uri, "&sortorder=ascending");
- else if (priv->sort_order == GDATA_YOUTUBE_SORT_DESCENDING)
- g_string_append (query_uri, "&sortorder=descending");
- else
- g_assert_not_reached ();
- }
+ if (priv->license != NULL) {
+ const gchar *v3_video_license;
- if (priv->uploader != GDATA_YOUTUBE_UPLOADER_ALL)
- g_string_append (query_uri, "&uploader=partner");
+ v3_video_license = get_v3_video_license (priv->license);
- if (priv->license != NULL) {
- g_string_append (query_uri, "&license=");
- g_string_append_uri_escaped (query_uri, priv->license, NULL, FALSE);
+ if (v3_video_license != NULL) {
+ g_string_append (query_uri, "&videoLicense=");
+ g_string_append_uri_escaped (query_uri,
+ v3_video_license, NULL,
+ FALSE);
+ }
}
}
diff --git a/gdata/services/youtube/gdata-youtube-service.c b/gdata/services/youtube/gdata-youtube-service.c
index e1210ae..2125b1c 100644
--- a/gdata/services/youtube/gdata-youtube-service.c
+++ b/gdata/services/youtube/gdata-youtube-service.c
@@ -1,7 +1,7 @@
/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/*
* GData Client
- * Copyright (C) Philip Withnall 2008–2010 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2008–2010, 2015 <philip tecnocode co uk>
*
* GData Client is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@@ -235,6 +235,8 @@
* g_object_unref (feed);
* </programlisting>
* </example>
+ *
+ * TODO: Update the above
**/
#include <config.h>
@@ -252,7 +254,7 @@
#include "gdata-youtube-category.h"
#include "gdata-batchable.h"
-/* Standards reference here: http://code.google.com/apis/youtube/2.0/reference.html */
+/* Standards reference here: http://code.google.com/apis/youtube/2.0/reference.html TODO */
GQuark
gdata_youtube_service_error_quark (void)
@@ -277,7 +279,9 @@ enum {
PROP_DEVELOPER_KEY = 1
};
-_GDATA_DEFINE_AUTHORIZATION_DOMAIN (youtube, "youtube", "http://gdata.youtube.com")
+/* Reference: https://developers.google.com/youtube/v3/guides/authentication */
+_GDATA_DEFINE_AUTHORIZATION_DOMAIN (youtube, "youtube",
+ "https://www.googleapis.com/auth/youtube")
G_DEFINE_TYPE_WITH_CODE (GDataYouTubeService, gdata_youtube_service, GDATA_TYPE_SERVICE,
G_IMPLEMENT_INTERFACE (GDATA_TYPE_BATCHABLE, NULL))
static void
@@ -300,7 +304,7 @@ gdata_youtube_service_class_init (GDataYouTubeServiceClass *klass)
* GDataYouTubeService:developer-key:
*
* The developer key your application has registered with the YouTube API. For more information, see
the <ulink type="http"
- * url="http://code.google.com/apis/youtube/2.0/developers_guide_protocol.html#Developer_Key">online
documentation</ulink>.
+ * url="https://developers.google.com/youtube/registering_an_application">online
documentation</ulink>.
**/
g_object_class_install_property (gobject_class, PROP_DEVELOPER_KEY,
g_param_spec_string ("developer-key",
@@ -359,161 +363,220 @@ gdata_youtube_service_set_property (GObject *object, guint property_id, const GV
}
static void
-append_query_headers (GDataService *self, GDataAuthorizationDomain *domain, SoupMessage *message)
+append_query_headers (GDataService *self, GDataAuthorizationDomain *domain,
+ SoupMessage *message)
{
GDataYouTubeServicePrivate *priv = GDATA_YOUTUBE_SERVICE (self)->priv;
- gchar *key_header;
g_assert (message != NULL);
- /* Dev key and client headers */
- key_header = g_strdup_printf ("key=%s", priv->developer_key);
- soup_message_headers_append (message->request_headers, "X-GData-Key", key_header);
- g_free (key_header);
+ if (priv->developer_key != NULL &&
+ !gdata_authorizer_is_authorized_for_domain (gdata_service_get_authorizer (GDATA_SERVICE (self)),
+ get_youtube_authorization_domain ())) {
+ const gchar *query;
+ SoupURI *uri;
+
+ uri = soup_message_get_uri (message);
+ query = soup_uri_get_query (uri);
+
+ /* Set the key on every unauthorised request:
+ * https://developers.google.com/youtube/v3/docs/standard_parameters#key */
+ if (query != NULL) {
+ GString *new_query;
+
+ new_query = g_string_new (query);
+
+ g_string_append (new_query, "&key=");
+ g_string_append_uri_escaped (new_query,
+ priv->developer_key, NULL,
+ FALSE);
+
+ soup_uri_set_query (uri, new_query->str);
+ g_string_free (new_query, TRUE);
+ }
+ }
/* Chain up to the parent class */
GDATA_SERVICE_CLASS (gdata_youtube_service_parent_class)->append_query_headers (self, domain,
message);
}
+/* Reference: https://developers.google.com/youtube/v3/docs/errors
+ *
+ * Example response:
+ * {
+ * "error": {
+ * "errors": [
+ * {
+ * "domain": "youtube.parameter",
+ * "reason": "missingRequiredParameter",
+ * "message": "No filter selected.",
+ * "locationType": "parameter",
+ * "location": ""
+ * }
+ * ],
+ * "code": 400,
+ * "message": "No filter selected."
+ * }
+ * }
+ */
+/* FIXME: Factor this out into a common JSON error parser helper which simply
+ * takes a map of expected error codes. */
static void
-parse_error_response (GDataService *self, GDataOperationType operation_type, guint status, const gchar
*reason_phrase, const gchar *response_body,
- gint length, GError **error)
+parse_error_response (GDataService *self, GDataOperationType operation_type,
+ guint status, const gchar *reason_phrase,
+ const gchar *response_body, gint length, GError **error)
{
- xmlDoc *doc;
- xmlNode *node;
+ JsonParser *parser = NULL; /* owned */
+ JsonReader *reader = NULL; /* owned */
+ gint i;
+ GError *child_error = NULL;
- if (response_body == NULL)
+ if (response_body == NULL) {
goto parent;
+ }
- if (length == -1)
+ if (length == -1) {
length = strlen (response_body);
+ }
- /* Parse the XML */
- doc = xmlReadMemory (response_body, length, "/dev/null", NULL, 0);
- if (doc == NULL)
+ parser = json_parser_new ();
+ if (!json_parser_load_from_data (parser, response_body, length,
+ &child_error)) {
goto parent;
+ }
- /* Get the root element */
- node = xmlDocGetRootElement (doc);
- if (node == NULL) {
- /* XML document's empty; chain up to the parent class */
- xmlFreeDoc (doc);
+ reader = json_reader_new (json_parser_get_root (parser));
+
+ /* Check that the outermost node is an object. */
+ if (!json_reader_is_object (reader)) {
goto parent;
}
- if (xmlStrcmp (node->name, (xmlChar*) "errors") != 0) {
- /* No <errors> element (required); chain up to the parent class */
- xmlFreeDoc (doc);
+ /* Grab the ‘error’ member, then its ‘errors’ member. */
+ if (!json_reader_read_member (reader, "error") ||
+ !json_reader_is_object (reader) ||
+ !json_reader_read_member (reader, "errors") ||
+ !json_reader_is_array (reader)) {
goto parent;
}
- /* Parse the actual errors */
- node = node->children;
- while (node != NULL) {
- xmlChar *domain = NULL, *code = NULL, *location = NULL;
- xmlNode *child_node = node->children;
+ /* Parse each of the errors. Return the first one, and print out any
+ * others. */
+ for (i = 0; i < json_reader_count_elements (reader); i++) {
+ const gchar *domain, *reason, *message, *extended_help;
+ const gchar *location_type, *location;
- if (node->type == XML_TEXT_NODE) {
- /* Skip text nodes; they're all whitespace */
- node = node->next;
- continue;
+ /* Parse the error. */
+ if (!json_reader_read_element (reader, i) ||
+ !json_reader_is_object (reader)) {
+ goto parent;
}
- /* Get the error data */
- while (child_node != NULL) {
- if (child_node->type == XML_TEXT_NODE) {
- /* Skip text nodes; they're all whitespace */
- child_node = child_node->next;
- continue;
- }
+ json_reader_read_member (reader, "domain");
+ domain = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
- if (xmlStrcmp (child_node->name, (xmlChar*) "domain") == 0)
- domain = xmlNodeListGetString (doc, child_node->children, TRUE);
- else if (xmlStrcmp (child_node->name, (xmlChar*) "code") == 0)
- code = xmlNodeListGetString (doc, child_node->children, TRUE);
- else if (xmlStrcmp (child_node->name, (xmlChar*) "location") == 0)
- location = xmlNodeListGetString (doc, child_node->children, TRUE);
- else if (xmlStrcmp (child_node->name, (xmlChar*) "internalReason") != 0) {
- /* Unknown element (ignore internalReason) */
- g_message ("Unhandled <error/%s> element.", child_node->name);
-
- xmlFree (domain);
- xmlFree (code);
- xmlFree (location);
- xmlFreeDoc (doc);
- goto check_error;
- }
+ json_reader_read_member (reader, "reason");
+ reason = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
- child_node = child_node->next;
- }
+ json_reader_read_member (reader, "message");
+ message = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ json_reader_read_member (reader, "extendedHelp");
+ extended_help = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ json_reader_read_member (reader, "locationType");
+ location_type = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ json_reader_read_member (reader, "location");
+ location = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ /* End the error element. */
+ json_reader_end_element (reader);
/* Create an error message, but only for the first error */
if (error == NULL || *error == NULL) {
- /* See
http://code.google.com/apis/youtube/2.0/developers_guide_protocol.html#Error_responses */
- if (xmlStrcmp (domain, (xmlChar*) "yt:service") == 0) {
- if (xmlStrcmp (code, (xmlChar*) "disabled_in_maintenance_mode") == 0) {
- /* Service disabled */
- g_set_error (error, GDATA_SERVICE_ERROR,
GDATA_SERVICE_ERROR_UNAVAILABLE,
- _("This service is not available at the moment."));
- } else if (xmlStrcmp (code, (xmlChar*) "youtube_signup_required") == 0) {
- /* Tried to authenticate with a Google Account which hasn't yet had a
YouTube channel created for it. */
- g_set_error (error, GDATA_YOUTUBE_SERVICE_ERROR,
GDATA_YOUTUBE_SERVICE_ERROR_CHANNEL_REQUIRED,
- /* Translators: the parameter is a URI. */
- _("Your Google Account must be associated with a YouTube
channel to do this. Visit %s to create one."),
- "https://www.youtube.com/create_channel");
- } else {
- /* Protocol error */
- g_set_error (error, GDATA_SERVICE_ERROR,
GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
- _("Unknown error code \"%s\" in domain \"%s\" received
with location \"%s\"."),
- code, domain, location);
- }
- } else if (xmlStrcmp (domain, (xmlChar*) "yt:authentication") == 0) {
+ if (g_strcmp0 (domain, "usageLimits") == 0 &&
+ g_strcmp0 (reason,
+ "dailyLimitExceededUnreg") == 0) {
+ /* Daily Limit for Unauthenticated Use
+ * Exceeded. */
+ g_set_error (error, GDATA_SERVICE_ERROR,
+ GDATA_SERVICE_ERROR_API_QUOTA_EXCEEDED,
+ _("You have made too many API "
+ "calls recently. Please wait a "
+ "few minutes and try again."));
+ } else if (g_strcmp0 (reason,
+ "rateLimitExceeded") == 0) {
+ g_set_error (error, GDATA_YOUTUBE_SERVICE_ERROR,
+ GDATA_YOUTUBE_SERVICE_ERROR_ENTRY_QUOTA_EXCEEDED,
+ _("You have exceeded your entry "
+ "quota. Please delete some "
+ "entries and try again."));
+ } else if (g_strcmp0 (domain, "global") == 0 &&
+ (g_strcmp0 (reason, "authError") == 0 ||
+ g_strcmp0 (reason, "required") == 0)) {
/* Authentication problem */
- g_set_error (error, GDATA_SERVICE_ERROR,
GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED,
- _("You must be authenticated to do this."));
- } else if (xmlStrcmp (domain, (xmlChar*) "yt:quota") == 0) {
- /* Quota errors */
- if (xmlStrcmp (code, (xmlChar*) "too_many_recent_calls") == 0) {
- g_set_error (error, GDATA_YOUTUBE_SERVICE_ERROR,
GDATA_YOUTUBE_SERVICE_ERROR_API_QUOTA_EXCEEDED,
- _("You have made too many API calls recently. Please
wait a few minutes and try again."));
- } else if (xmlStrcmp (code, (xmlChar*) "too_many_entries") == 0) {
- g_set_error (error, GDATA_YOUTUBE_SERVICE_ERROR,
GDATA_YOUTUBE_SERVICE_ERROR_ENTRY_QUOTA_EXCEEDED,
- _("You have exceeded your entry quota. Please delete
some entries and try again."));
- } else {
- /* Protocol error */
- g_set_error (error, GDATA_SERVICE_ERROR,
GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
- /* Translators: the first parameter is an error code,
which is a coded string.
- * The second parameter is an error domain, which is
another coded string.
- * The third parameter is the location of the error,
which is either a URI or an XPath. */
- _("Unknown error code \"%s\" in domain \"%s\" received
with location \"%s\"."),
- code, domain, location);
- }
+ g_set_error (error, GDATA_SERVICE_ERROR,
+ GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED,
+ _("You must be authenticated to "
+ "do this."));
+ } else if (g_strcmp0 (reason,
+ "youtubeSignupRequired") == 0) {
+ /* Tried to authenticate with a Google Account which hasn't yet had a YouTube
channel created for it. */
+ g_set_error (error, GDATA_YOUTUBE_SERVICE_ERROR,
+ GDATA_YOUTUBE_SERVICE_ERROR_CHANNEL_REQUIRED,
+ /* Translators: the parameter is a URI. */
+ _("Your Google Account must be "
+ "associated with a YouTube "
+ "channel to do this. Visit %s "
+ "to create one."),
+ "https://www.youtube.com/create_channel");
} else {
- /* Unknown or validation (protocol) error */
- g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
- _("Unknown error code \"%s\" in domain \"%s\" received with
location \"%s\"."),
- code, domain, location);
+ /* Unknown or validation (protocol) error. Fall
+ * back to working off the HTTP status code. */
+ g_warning ("Unknown error code ‘%s’ in domain "
+ "‘%s’ received with location type "
+ "‘%s’, location ‘%s’, extended help "
+ "‘%s’ and message ‘%s’.",
+ reason, domain, location_type,
+ location, extended_help, message);
+
+ goto parent;
}
} else {
- /* For all errors after the first, log the error in the terminal */
- g_debug ("Error message received in response: code \"%s\", domain \"%s\", location
\"%s\".", code, domain, location);
+ /* For all errors after the first, log the error in the
+ * terminal. */
+ g_debug ("Error message received in response: domain "
+ "‘%s’, reason ‘%s’, extended help ‘%s’, "
+ "message ‘%s’, location type ‘%s’, location "
+ "‘%s’.",
+ domain, reason, extended_help, message,
+ location_type, location);
}
+ }
- xmlFree (domain);
- xmlFree (code);
- xmlFree (location);
+ /* End the ‘errors’ and ‘error’ members. */
+ json_reader_end_element (reader);
+ json_reader_end_element (reader);
- node = node->next;
- }
+ g_clear_object (&reader);
+ g_clear_object (&parser);
-check_error:
- /* Ensure we're actually set an error message */
- if (error != NULL && *error == NULL)
- g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR, _("Unknown and
unparsable error received."));
+ /* Ensure we’ve actually set an error message. */
+ g_assert (error == NULL || *error != NULL);
return;
parent:
+ g_clear_object (&reader);
+ g_clear_object (&parser);
+
/* Chain up to the parent class */
GDATA_SERVICE_CLASS (gdata_youtube_service_parent_class)->parse_error_response (self, operation_type,
status, reason_phrase,
response_body,
length, error);
@@ -532,7 +595,7 @@ get_authorization_domains (void)
*
* Creates a new #GDataYouTubeService using the given #GDataAuthorizer. If @authorizer is %NULL, all
requests are made as an unauthenticated user.
* The @developer_key must be unique for your application, and as
- * <ulink type="http"
url="http://code.google.com/apis/youtube/2.0/developers_guide_protocol.html#Developer_Key">registered with
Google</ulink>.
+ * <ulink type="http" url="https://developers.google.com/youtube/registering_an_application">registered with
Google</ulink>.
*
* Return value: a new #GDataYouTubeService, or %NULL; unref with g_object_unref()
*
@@ -569,30 +632,42 @@ gdata_youtube_service_get_primary_authorization_domain (void)
return get_youtube_authorization_domain ();
}
-static const gchar *
+static gchar *
standard_feed_type_to_feed_uri (GDataYouTubeStandardFeedType feed_type)
{
switch (feed_type) {
+ case GDATA_YOUTUBE_MOST_POPULAR_FEED:
+ return g_strdup ("https://www.googleapis.com/youtube/v3/videos"
+ "?part=snippet"
+ "&chart=mostPopular");
case GDATA_YOUTUBE_TOP_RATED_FEED:
- return "https://gdata.youtube.com/feeds/api/standardfeeds/top_rated";
case GDATA_YOUTUBE_TOP_FAVORITES_FEED:
- return "https://gdata.youtube.com/feeds/api/standardfeeds/top_favorites";
case GDATA_YOUTUBE_MOST_VIEWED_FEED:
- return "https://gdata.youtube.com/feeds/api/standardfeeds/most_viewed";
- case GDATA_YOUTUBE_MOST_POPULAR_FEED:
- return "https://gdata.youtube.com/feeds/api/standardfeeds/most_popular";
case GDATA_YOUTUBE_MOST_RECENT_FEED:
- return "https://gdata.youtube.com/feeds/api/standardfeeds/most_recent";
case GDATA_YOUTUBE_MOST_DISCUSSED_FEED:
- return "https://gdata.youtube.com/feeds/api/standardfeeds/most_discussed";
case GDATA_YOUTUBE_MOST_LINKED_FEED:
- return "https://gdata.youtube.com/feeds/api/standardfeeds/most_linked";
case GDATA_YOUTUBE_MOST_RESPONDED_FEED:
- return "https://gdata.youtube.com/feeds/api/standardfeeds/most_responded";
case GDATA_YOUTUBE_RECENTLY_FEATURED_FEED:
- return "https://gdata.youtube.com/feeds/api/standardfeeds/recently_featured";
- case GDATA_YOUTUBE_WATCH_ON_MOBILE_FEED:
- return "https://gdata.youtube.com/feeds/api/standardfeeds/watch_on_mobile";
+ case GDATA_YOUTUBE_WATCH_ON_MOBILE_FEED: {
+ gchar *date, *out;
+ GTimeVal tv;
+
+ /* All feed types except MOST_POPULAR have been deprecated for
+ * a while, and fall back to MOST_POPULAR on the server anyway.
+ * See:
https://developers.google.com/youtube/2.0/developers_guide_protocol_video_feeds#Standard_feeds */
+ g_get_current_time (&tv);
+ tv.tv_sec -= 24 * 60 * 60; /* 1 day ago */
+ date = g_time_val_to_iso8601 (&tv);
+ out = g_strconcat ("https://www.googleapis.com/youtube/v3/videos"
+ "?part=snippet"
+ "&chart=mostPopular"
+ "&publishedAfter=",
+ date,
+ NULL);
+ g_free (date);
+
+ return out;
+ }
default:
g_assert_not_reached ();
}
@@ -612,6 +687,8 @@ standard_feed_type_to_feed_uri (GDataYouTubeStandardFeedType feed_type)
*
* Parameters and errors are as for gdata_service_query().
*
+ * TODO: Document deprecation of most feed types
+ *
* Return value: (transfer full): a #GDataFeed of query results, or %NULL; unref with g_object_unref()
**/
GDataFeed *
@@ -619,14 +696,24 @@ gdata_youtube_service_query_standard_feed (GDataYouTubeService *self, GDataYouTu
GCancellable *cancellable, GDataQueryProgressCallback
progress_callback, gpointer progress_user_data,
GError **error)
{
+ gchar *query_uri;
+ GDataFeed *feed;
+
g_return_val_if_fail (GDATA_IS_YOUTUBE_SERVICE (self), NULL);
g_return_val_if_fail (query == NULL || GDATA_IS_QUERY (query), NULL);
g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
/* TODO: Support the "time" parameter, as well as category- and region-specific feeds */
- return gdata_service_query (GDATA_SERVICE (self), get_youtube_authorization_domain (),
standard_feed_type_to_feed_uri (feed_type), query,
- GDATA_TYPE_YOUTUBE_VIDEO, cancellable, progress_callback,
progress_user_data, error);
+ query_uri = standard_feed_type_to_feed_uri (feed_type);
+ feed = gdata_service_query (GDATA_SERVICE (self),
+ get_youtube_authorization_domain (),
+ query_uri, query, GDATA_TYPE_YOUTUBE_VIDEO,
+ cancellable, progress_callback,
+ progress_user_data, error);
+ g_free (query_uri);
+
+ return feed;
}
/**
@@ -658,14 +745,22 @@ gdata_youtube_service_query_standard_feed_async (GDataYouTubeService *self, GDat
GDestroyNotify destroy_progress_user_data,
GAsyncReadyCallback callback, gpointer user_data)
{
+ gchar *query_uri;
+
g_return_if_fail (GDATA_IS_YOUTUBE_SERVICE (self));
g_return_if_fail (query == NULL || GDATA_IS_QUERY (query));
g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
g_return_if_fail (callback != NULL);
- gdata_service_query_async (GDATA_SERVICE (self), get_youtube_authorization_domain (),
standard_feed_type_to_feed_uri (feed_type), query,
- GDATA_TYPE_YOUTUBE_VIDEO, cancellable, progress_callback,
progress_user_data, destroy_progress_user_data,
- callback, user_data);
+ query_uri = standard_feed_type_to_feed_uri (feed_type);
+ gdata_service_query_async (GDATA_SERVICE (self),
+ get_youtube_authorization_domain (),
+ query_uri, query, GDATA_TYPE_YOUTUBE_VIDEO,
+ cancellable, progress_callback,
+ progress_user_data,
+ destroy_progress_user_data, callback,
+ user_data);
+ g_free (query_uri);
}
/**
@@ -694,8 +789,14 @@ gdata_youtube_service_query_videos (GDataYouTubeService *self, GDataQuery *query
g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
- return gdata_service_query (GDATA_SERVICE (self), get_youtube_authorization_domain (),
"https://gdata.youtube.com/feeds/api/videos", query,
- GDATA_TYPE_YOUTUBE_VIDEO, cancellable, progress_callback,
progress_user_data, error);
+ return gdata_service_query (GDATA_SERVICE (self),
+ get_youtube_authorization_domain (),
+ "https://www.googleapis.com/youtube/v3/search"
+ "?part=snippet"
+ "&type=video",
+ query, GDATA_TYPE_YOUTUBE_VIDEO,
+ cancellable, progress_callback,
+ progress_user_data, error);
}
/**
@@ -731,9 +832,15 @@ gdata_youtube_service_query_videos_async (GDataYouTubeService *self, GDataQuery
g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
g_return_if_fail (callback != NULL);
- gdata_service_query_async (GDATA_SERVICE (self), get_youtube_authorization_domain (),
"https://gdata.youtube.com/feeds/api/videos", query,
- GDATA_TYPE_YOUTUBE_VIDEO, cancellable, progress_callback,
progress_user_data, destroy_progress_user_data,
- callback, user_data);
+ gdata_service_query_async (GDATA_SERVICE (self),
+ get_youtube_authorization_domain (),
+ "https://www.googleapis.com/youtube/v3/search"
+ "?part=snippet"
+ "&type=video",
+ query, GDATA_TYPE_YOUTUBE_VIDEO, cancellable,
+ progress_callback, progress_user_data,
+ destroy_progress_user_data, callback,
+ user_data);
}
/**
@@ -748,8 +855,7 @@ gdata_youtube_service_query_videos_async (GDataYouTubeService *self, GDataQuery
*
* Queries the service for videos related to @video. The algorithm determining which videos are related is
on the server side.
*
- * If @video does not have a link with rel value
<literal>http://gdata.youtube.com/schemas/2007#video.related</literal>, a
- * %GDATA_SERVICE_ERROR_PROTOCOL_ERROR error will be thrown. Parameters and other errors are as for
gdata_service_query().
+ * Parameters and other errors are as for gdata_service_query().
*
* Return value: (transfer full): a #GDataFeed of query results; unref with g_object_unref()
**/
@@ -758,7 +864,6 @@ gdata_youtube_service_query_related (GDataYouTubeService *self, GDataYouTubeVide
GCancellable *cancellable, GDataQueryProgressCallback
progress_callback, gpointer progress_user_data,
GError **error)
{
- GDataLink *related_link;
GDataFeed *feed;
gchar *uri;
@@ -768,19 +873,17 @@ gdata_youtube_service_query_related (GDataYouTubeService *self, GDataYouTubeVide
g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
- /* See if the video already has a rel="http://gdata.youtube.com/schemas/2007#video.related" link */
- related_link = gdata_entry_look_up_link (GDATA_ENTRY (video),
"http://gdata.youtube.com/schemas/2007#video.related");
- if (related_link == NULL) {
- /* Erroring out is probably the safest thing to do */
- g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
- _("The video did not have a related videos <link>."));
- return NULL;
- }
-
/* Execute the query */
- uri = _gdata_service_fix_uri_scheme (gdata_link_get_uri (related_link));
- feed = gdata_service_query (GDATA_SERVICE (self), get_youtube_authorization_domain (), uri, query,
- GDATA_TYPE_YOUTUBE_VIDEO, cancellable, progress_callback,
progress_user_data, error);
+ uri = g_strdup_printf ("https://www.googleapis.com/youtube/v3/search"
+ "?part=snippet"
+ "&type=video"
+ "&relatedToVideoId=%s",
+ gdata_entry_get_id (GDATA_ENTRY (video)));
+ feed = gdata_service_query (GDATA_SERVICE (self),
+ get_youtube_authorization_domain (), uri,
+ query, GDATA_TYPE_YOUTUBE_VIDEO,
+ cancellable, progress_callback,
+ progress_user_data, error);
g_free (uri);
return feed;
@@ -815,7 +918,6 @@ gdata_youtube_service_query_related_async (GDataYouTubeService *self, GDataYouTu
GDestroyNotify destroy_progress_user_data,
GAsyncReadyCallback callback, gpointer user_data)
{
- GDataLink *related_link;
gchar *uri;
g_return_if_fail (GDATA_IS_YOUTUBE_SERVICE (self));
@@ -824,23 +926,17 @@ gdata_youtube_service_query_related_async (GDataYouTubeService *self, GDataYouTu
g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
g_return_if_fail (callback != NULL);
- /* See if the video already has a rel="http://gdata.youtube.com/schemas/2007#video.related" link */
- related_link = gdata_entry_look_up_link (GDATA_ENTRY (video),
"http://gdata.youtube.com/schemas/2007#video.related");
- if (related_link == NULL) {
- /* Erroring out is probably the safest thing to do */
- GSimpleAsyncResult *result = g_simple_async_result_new (G_OBJECT (self), callback, user_data,
gdata_service_query_async);
- g_simple_async_result_set_error (result, GDATA_SERVICE_ERROR,
GDATA_SERVICE_ERROR_PROTOCOL_ERROR, "%s",
- _("The video did not have a related videos <link>."));
- g_simple_async_result_complete_in_idle (result);
- g_object_unref (result);
-
- return;
- }
-
- uri = _gdata_service_fix_uri_scheme (gdata_link_get_uri (related_link));
- gdata_service_query_async (GDATA_SERVICE (self), get_youtube_authorization_domain (), uri, query,
- GDATA_TYPE_YOUTUBE_VIDEO, cancellable, progress_callback,
progress_user_data,
- destroy_progress_user_data, callback, user_data);
+ uri = g_strdup_printf ("https://www.googleapis.com/youtube/v3/search"
+ "?part=snippet"
+ "&type=video"
+ "&relatedToVideoId=%s",
+ gdata_entry_get_id (GDATA_ENTRY (video)));
+ gdata_service_query_async (GDATA_SERVICE (self),
+ get_youtube_authorization_domain (), uri,
+ query, GDATA_TYPE_YOUTUBE_VIDEO, cancellable,
+ progress_callback, progress_user_data,
+ destroy_progress_user_data, callback,
+ user_data);
g_free (uri);
}
@@ -876,6 +972,8 @@ GDataUploadStream *
gdata_youtube_service_upload_video (GDataYouTubeService *self, GDataYouTubeVideo *video, const gchar *slug,
const gchar *content_type,
GCancellable *cancellable, GError **error)
{
+ GOutputStream *stream = NULL; /* owned */
+
g_return_val_if_fail (GDATA_IS_YOUTUBE_SERVICE (self), NULL);
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (video), NULL);
g_return_val_if_fail (slug != NULL && *slug != '\0', NULL);
@@ -888,7 +986,7 @@ gdata_youtube_service_upload_video (GDataYouTubeService *self, GDataYouTubeVideo
_("The entry has already been inserted."));
return NULL;
}
-
+/* TODO: could be more cunning about domains here; see scope on
https://developers.google.com/youtube/v3/guides/authentication */
if (gdata_authorizer_is_authorized_for_domain (gdata_service_get_authorizer (GDATA_SERVICE (self)),
get_youtube_authorization_domain ()) == FALSE) {
g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED,
@@ -896,10 +994,22 @@ gdata_youtube_service_upload_video (GDataYouTubeService *self, GDataYouTubeVideo
return NULL;
}
- /* Streaming upload support using GDataUploadStream; automatically handles the XML and multipart
stuff for us */
- return GDATA_UPLOAD_STREAM (gdata_upload_stream_new (GDATA_SERVICE (self),
get_youtube_authorization_domain (), SOUP_METHOD_POST,
-
"https://uploads.gdata.youtube.com/feeds/api/users/default/uploads",
- GDATA_ENTRY (video), slug, content_type,
cancellable));
+ /* FIXME: Add support for resumable uploads. That means a new
+ * gdata_youtube_service_upload_video_resumable() method a la
+ * Documents. */
+ stream = gdata_upload_stream_new (GDATA_SERVICE (self),
+ get_youtube_authorization_domain (),
+ SOUP_METHOD_POST,
+ "https://www.googleapis.com/upload/youtube/v3/videos"
+ "?part=snippet,status,"
+ "recordingDetails,"
+ "contentDetails,id,"
+ "statistics",
+ GDATA_ENTRY (video), slug,
+ content_type,
+ cancellable);
+
+ return GDATA_UPLOAD_STREAM (stream);
}
/**
@@ -934,7 +1044,10 @@ gdata_youtube_service_finish_video_upload (GDataYouTubeService *self, GDataUploa
return NULL;
/* Parse the response to produce a GDataYouTubeVideo */
- return GDATA_YOUTUBE_VIDEO (gdata_parsable_new_from_xml (GDATA_TYPE_YOUTUBE_VIDEO, response_body,
(gint) response_length, error));
+ return GDATA_YOUTUBE_VIDEO (gdata_parsable_new_from_json (GDATA_TYPE_YOUTUBE_VIDEO,
+ response_body,
+ (gint) response_length,
+ error));
}
/**
@@ -970,6 +1083,8 @@ gdata_youtube_service_get_developer_key (GDataYouTubeService *self)
GDataAPPCategories *
gdata_youtube_service_get_categories (GDataYouTubeService *self, GCancellable *cancellable, GError **error)
{
+ const gchar *locale;
+ gchar *uri;
SoupMessage *message;
GDataAPPCategories *categories;
@@ -977,16 +1092,31 @@ gdata_youtube_service_get_categories (GDataYouTubeService *self, GCancellable *c
g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
- /* Download the category list. Note that this is (service) locale-dependent. */
- message = _gdata_service_query (GDATA_SERVICE (self), get_youtube_authorization_domain (),
- "https://gdata.youtube.com/schemas/2007/categories.cat", NULL,
cancellable, error);
+ /* Download the category list. Note that this is (service)
+ * locale-dependent, and a locale must always be specified. */
+ locale = gdata_service_get_locale (GDATA_SERVICE (self));
+ if (locale == NULL) {
+ locale = "US";
+ }
+
+ uri = g_strdup_printf ("https://www.googleapis.com/youtube/v3/videoCategories"
+ "?part=snippet"
+ "®ionCode=%s",
+ locale);
+ message = _gdata_service_query (GDATA_SERVICE (self),
+ get_youtube_authorization_domain (),
+ uri, NULL, cancellable, error);
+ g_free (uri);
+
if (message == NULL)
return NULL;
g_assert (message->response_body->data != NULL);
- categories = GDATA_APP_CATEGORIES (_gdata_parsable_new_from_xml (GDATA_TYPE_APP_CATEGORIES,
message->response_body->data,
- message->response_body->length,
- GSIZE_TO_POINTER
(GDATA_TYPE_YOUTUBE_CATEGORY), error));
+ categories = GDATA_APP_CATEGORIES (_gdata_parsable_new_from_json (GDATA_TYPE_APP_CATEGORIES,
+ message->response_body->data,
+ message->response_body->length,
+ GSIZE_TO_POINTER
(GDATA_TYPE_YOUTUBE_CATEGORY),
+ error));
g_object_unref (message);
return categories;
diff --git a/gdata/services/youtube/gdata-youtube-video.c b/gdata/services/youtube/gdata-youtube-video.c
index e93751d..3c3e020 100644
--- a/gdata/services/youtube/gdata-youtube-video.c
+++ b/gdata/services/youtube/gdata-youtube-video.c
@@ -1,7 +1,7 @@
/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/*
* GData Client
- * Copyright (C) Philip Withnall 2008–2010 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2008–2010, 2015 <philip tecnocode co uk>
*
* GData Client is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@@ -64,12 +64,15 @@
* g_object_unref (video);
* </programlisting>
* </example>
+ *
+ * TODO: update
+ * TODO: note that comments are no longer supported; some properties are now only retrieved for single
queries; which properties are supported for uploads
**/
#include <config.h>
#include <glib.h>
#include <glib/gi18n-lib.h>
-#include <libxml/parser.h>
+#include <json-glib/json-glib.h>
#include <string.h>
#include "gdata-youtube-video.h"
@@ -93,10 +96,10 @@ static void gdata_youtube_video_dispose (GObject *object);
static void gdata_youtube_video_finalize (GObject *object);
static void gdata_youtube_video_get_property (GObject *object, guint property_id, GValue *value, GParamSpec
*pspec);
static void gdata_youtube_video_set_property (GObject *object, guint property_id, const GValue *value,
GParamSpec *pspec);
-static gboolean parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError
**error);
-static gboolean post_parse_xml (GDataParsable *parsable, gpointer user_data, GError **error);
-static void get_xml (GDataParsable *parsable, GString *xml_string);
-static void get_namespaces (GDataParsable *parsable, GHashTable *namespaces);
+static gboolean parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error);
+static gboolean post_parse_json (GDataParsable *parsable, gpointer user_data, GError **error);
+static void get_json (GDataParsable *parsable, JsonBuilder *builder);
+static const gchar *get_content_type (void);
static gchar *get_entry_uri (const gchar *id) G_GNUC_WARN_UNUSED_RESULT;
static void gdata_youtube_video_commentable_init (GDataCommentableInterface *iface);
static GDataAuthorizationDomain *get_authorization_domain (GDataCommentable *self) G_GNUC_CONST;
@@ -118,8 +121,17 @@ struct _GDataYouTubeVideoPrivate {
gdouble average;
} rating;
- /* media:group */
- GDataMediaGroup *media_group; /* is actually a GDataYouTubeGroup */
+ gchar **keywords;
+ gchar *player_uri;
+ gchar **region_restriction_allowed;
+ gchar **region_restriction_blocked;
+ GHashTable *content_ratings; /* owned string → owned string */
+ GList *thumbnails; /* GDataMediaThumbnail */
+ GDataMediaCategory *category;
+ GDataMediaCredit *credit;
+ guint duration;
+ gboolean is_private;
+ gchar *aspect_ratio;
/* georss:where */
GDataGeoRSSWhere *georss_where;
@@ -128,8 +140,8 @@ struct _GDataYouTubeVideoPrivate {
GDataYouTubeControl *youtube_control;
gint64 recorded;
- /* Comments */
- GDataGDFeedLink *comments_feed_link;
+ /* State for parse_json(). */
+ gboolean parsing_in_video_list_response;
};
enum {
@@ -175,18 +187,18 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
gobject_class->dispose = gdata_youtube_video_dispose;
gobject_class->finalize = gdata_youtube_video_finalize;
- parsable_class->parse_xml = parse_xml;
- parsable_class->post_parse_xml = post_parse_xml;
- parsable_class->get_xml = get_xml;
- parsable_class->get_namespaces = get_namespaces;
+ parsable_class->parse_json = parse_json;
+ parsable_class->post_parse_json = post_parse_json;
+ parsable_class->get_json = get_json;
+ parsable_class->get_content_type = get_content_type;
entry_class->get_entry_uri = get_entry_uri;
- entry_class->kind_term = "http://gdata.youtube.com/schemas/2007#video";
+ entry_class->kind_term = "youtube#video"; /* also: youtube#searchResult */
/**
* GDataYouTubeVideo:view-count:
*
- * The number of times the video has been viewed.
+ * The number of times the video has been viewed. TODO: update docs links
*
* For more information, see the <ulink type="http"
*
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_yt:statistics">online
documentation</ulink>.
@@ -515,25 +527,12 @@ gdata_youtube_video_commentable_init (GDataCommentableInterface *iface)
}
static void
-notify_title_cb (GDataYouTubeVideo *self, GParamSpec *pspec, gpointer user_data)
-{
- /* Update our media:group title */
- if (self->priv->media_group != NULL)
- gdata_media_group_set_title (self->priv->media_group, gdata_entry_get_title (GDATA_ENTRY
(self)));
-}
-
-static void
gdata_youtube_video_init (GDataYouTubeVideo *self)
{
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_YOUTUBE_VIDEO, GDataYouTubeVideoPrivate);
self->priv->recorded = -1;
self->priv->access_controls = g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify)
g_free, NULL);
self->priv->georss_where = g_object_new (GDATA_TYPE_GEORSS_WHERE, NULL);
-
- /* The video's title is duplicated between atom:title and media:group/media:title, so listen for
change notifications on atom:title
- * and propagate them to media:group/media:title accordingly. Since the media group isn't publically
accessible, we don't need to
- * listen for notifications from it. */
- g_signal_connect (GDATA_ENTRY (self), "notify::title", G_CALLBACK (notify_title_cb), NULL);
}
static GObject *
@@ -545,9 +544,8 @@ gdata_youtube_video_constructor (GType type, guint n_construct_params, GObjectCo
object = G_OBJECT_CLASS (gdata_youtube_video_parent_class)->constructor (type, n_construct_params,
construct_params);
/* We can't create these in init, or they would collide with the group and control created when
parsing the XML */
- if (_gdata_parsable_is_constructed_from_xml (GDATA_PARSABLE (object)) == FALSE) {
+ if (_gdata_parsable_is_constructed_from_xml (GDATA_PARSABLE (object)) == FALSE) { /* TODO */
GDataYouTubeVideoPrivate *priv = GDATA_YOUTUBE_VIDEO (object)->priv;
- priv->media_group = g_object_new (GDATA_TYPE_YOUTUBE_GROUP, NULL);
priv->youtube_control = g_object_new (GDATA_TYPE_YOUTUBE_CONTROL, NULL);
}
@@ -559,10 +557,6 @@ gdata_youtube_video_dispose (GObject *object)
{
GDataYouTubeVideoPrivate *priv = GDATA_YOUTUBE_VIDEO (object)->priv;
- if (priv->media_group != NULL)
- g_object_unref (priv->media_group);
- priv->media_group = NULL;
-
if (priv->georss_where != NULL)
g_object_unref (priv->georss_where);
priv->georss_where = NULL;
@@ -571,10 +565,6 @@ gdata_youtube_video_dispose (GObject *object)
g_object_unref (priv->youtube_control);
priv->youtube_control = NULL;
- if (priv->comments_feed_link != NULL)
- g_object_unref (priv->comments_feed_link);
- priv->comments_feed_link = NULL;
-
/* Chain up to the parent class */
G_OBJECT_CLASS (gdata_youtube_video_parent_class)->dispose (object);
}
@@ -586,6 +576,15 @@ gdata_youtube_video_finalize (GObject *object)
g_free (priv->location);
g_hash_table_destroy (priv->access_controls);
+ g_strfreev (priv->keywords);
+ g_free (priv->player_uri);
+ g_strfreev (priv->region_restriction_allowed);
+ g_strfreev (priv->region_restriction_blocked);
+ g_clear_pointer (&priv->content_ratings, (GDestroyNotify) g_hash_table_unref);
+ g_list_free_full (priv->thumbnails, (GDestroyNotify) g_object_unref);
+ g_clear_object (&priv->category);
+ g_clear_object (&priv->credit);
+ g_free (priv->aspect_ratio);
/* Chain up to the parent class */
G_OBJECT_CLASS (gdata_youtube_video_parent_class)->finalize (object);
@@ -619,31 +618,31 @@ gdata_youtube_video_get_property (GObject *object, guint property_id, GValue *va
g_value_set_double (value, priv->rating.average);
break;
case PROP_KEYWORDS:
- g_value_set_boxed (value, gdata_media_group_get_keywords (priv->media_group));
+ g_value_set_boxed (value, priv->keywords);
break;
case PROP_PLAYER_URI:
- g_value_set_string (value, gdata_media_group_get_player_uri (priv->media_group));
+ g_value_set_string (value, gdata_youtube_video_get_player_uri (GDATA_YOUTUBE_VIDEO
(object)));
break;
case PROP_CATEGORY:
- g_value_set_object (value, gdata_media_group_get_category (priv->media_group));
+ g_value_set_object (value, priv->category);
break;
case PROP_CREDIT:
- g_value_set_object (value, gdata_media_group_get_credit (priv->media_group));
+ g_value_set_object (value, priv->credit);
break;
case PROP_DESCRIPTION:
- g_value_set_string (value, gdata_media_group_get_description (priv->media_group));
+ g_value_set_string (value, gdata_entry_get_summary (GDATA_ENTRY (object)));
break;
case PROP_DURATION:
- g_value_set_uint (value, gdata_youtube_group_get_duration (GDATA_YOUTUBE_GROUP
(priv->media_group)));
+ g_value_set_uint (value, priv->duration);
break;
case PROP_IS_PRIVATE:
- g_value_set_boolean (value, gdata_youtube_group_is_private (GDATA_YOUTUBE_GROUP
(priv->media_group)));
+ g_value_set_boolean (value, priv->is_private);
break;
case PROP_UPLOADED:
- g_value_set_int64 (value, gdata_youtube_group_get_uploaded (GDATA_YOUTUBE_GROUP
(priv->media_group)));
+ g_value_set_int64 (value, gdata_entry_get_published (GDATA_ENTRY (object)));
break;
case PROP_VIDEO_ID:
- g_value_set_string (value, gdata_youtube_group_get_video_id (GDATA_YOUTUBE_GROUP
(priv->media_group)));
+ g_value_set_string (value, gdata_entry_get_id (GDATA_ENTRY (object)));
break;
case PROP_IS_DRAFT:
g_value_set_boolean (value, gdata_youtube_control_is_draft (priv->youtube_control));
@@ -655,7 +654,7 @@ gdata_youtube_video_get_property (GObject *object, guint property_id, GValue *va
g_value_set_int64 (value, priv->recorded);
break;
case PROP_ASPECT_RATIO:
- g_value_set_string (value, gdata_youtube_group_get_aspect_ratio (GDATA_YOUTUBE_GROUP
(priv->media_group)));
+ g_value_set_string (value, priv->aspect_ratio);
break;
case PROP_LATITUDE:
g_value_set_double (value, gdata_georss_where_get_latitude (priv->georss_where));
@@ -715,12 +714,520 @@ gdata_youtube_video_set_property (GObject *object, guint property_id, const GVal
}
}
+/* https://developers.google.com/youtube/v3/docs/videos#contentDetails.duration
+ *
+ * Note that it can also include an ‘hours’ component, as specified in ISO 8601,
+ * but not in the Google documentation. */
+static gboolean
+duration_from_json_member (JsonReader *reader, const gchar *member_name,
+ GDataParserOptions options, guint *output,
+ gboolean *success, GError **error)
+{
+ gchar *duration_str = NULL, *i = NULL, *new_i = NULL;
+ guint64 seconds;
+ gboolean child_success = FALSE;
+
+ if (!gdata_parser_string_from_json_member (reader, member_name, options,
+ &duration_str,
+ &child_success, error)) {
+ return FALSE;
+ }
+
+ *success = child_success;
+ *output = 0;
+
+ if (!child_success) {
+ return TRUE;
+ }
+
+ /* Parse the string. Format: ‘PT(hH)?(mM)?(sS)?’, where ‘h’, ‘m’ and ‘s’
+ * are integer numbers of hours, minutes and seconds. Each element may
+ * not be present. */
+ i = duration_str;
+ if (strncmp (duration_str, "PT", 2) != 0) {
+ goto error;
+ }
+
+ i += 2; /* PT */
+
+ seconds = 0;
+
+ while (*i != '\0') {
+ guint64 element;
+ gchar designator;
+
+ element = g_ascii_strtoull (i, &new_i, 10);
+ if (new_i == i) {
+ goto error;
+ }
+
+ i = new_i;
+
+ designator = i[0];
+ if (designator == 'H') {
+ seconds += 60 * 60 * element;
+ } else if (designator == 'M') {
+ seconds += 60 * element;
+ } else if (designator == 'S') {
+ seconds += element;
+ } else {
+ goto error;
+ }
+
+ i += 1;
+ }
+
+ *output = seconds;
+ *success = child_success;
+
+ g_free (duration_str);
+
+ return TRUE;
+
+error:
+ gdata_parser_error_not_iso8601_format_json (reader, duration_str,
+ error);
+ g_free (duration_str);
+
+ return TRUE;
+}
+
+/* https://developers.google.com/youtube/v3/docs/videos#snippet.thumbnails */
+static gboolean
+thumbnails_from_json_member (JsonReader *reader, const gchar *member_name,
+ GDataParserOptions options, GList **output,
+ gboolean *success, GError **error)
+{
+ guint i, len;
+ GList *thumbnails = NULL;
+ const GError *child_error = NULL;
+
+ /* Check if there's such element */
+ if (g_strcmp0 (json_reader_get_member_name (reader),
+ member_name) != 0) {
+ return FALSE;
+ }
+
+ /* Check if the output string has already been set. The JSON parser
+ * guarantees this can't happen. */
+ g_assert (!(options & P_NO_DUPES) || *output == NULL);
+
+ len = json_reader_count_members (reader);
+ child_error = json_reader_get_error (reader);
+
+ if (child_error != NULL) {
+ *success = gdata_parser_error_from_json_error (reader,
+ child_error,
+ error);
+ goto done;
+ }
+
+ for (i = 0; i < len; i++) {
+ GDataParsable *thumbnail = NULL; /* GDataMediaThumbnail */
+
+ json_reader_read_element (reader, i);
+ thumbnail = _gdata_parsable_new_from_json_node (GDATA_TYPE_MEDIA_THUMBNAIL,
+ reader, NULL,
+ error);
+ json_reader_end_element (reader);
+
+ if (thumbnail == NULL) {
+ *success = FALSE;
+ goto done;
+ }
+
+ thumbnails = g_list_prepend (thumbnails, thumbnail);
+ }
+
+ /* Success! */
+ *output = thumbnails;
+ thumbnails = NULL;
+ *success = TRUE;
+
+done:
+ g_list_free_full (thumbnails, (GDestroyNotify) g_object_unref);
+
+ return TRUE;
+}
+
+/* https://developers.google.com/youtube/v3/docs/videos#contentDetails.regionRestriction */
+static gboolean
+restricted_countries_from_json_member (JsonReader *reader,
+ const gchar *member_name,
+ GDataParserOptions options,
+ gchar ***output_allowed,
+ gchar ***output_blocked,
+ gboolean *success, GError **error)
+{
+ guint i, len;
+ const GError *child_error = NULL;
+
+ /* Check if there's such element */
+ if (g_strcmp0 (json_reader_get_member_name (reader),
+ member_name) != 0) {
+ return FALSE;
+ }
+
+ /* Check if the output string has already been set. The JSON parser guarantees this can't happen. */
+ g_assert (!(options & P_NO_DUPES) ||
+ (*output_allowed == NULL && *output_blocked == NULL));
+
+ len = json_reader_count_members (reader);
+ child_error = json_reader_get_error (reader);
+
+ if (child_error != NULL) {
+ *success = gdata_parser_error_from_json_error (reader,
+ child_error,
+ error);
+ return TRUE;
+ }
+
+ for (i = 0; i < len; i++) {
+ json_reader_read_element (reader, i);
+
+ if (gdata_parser_strv_from_json_member (reader, "allowed",
+ P_DEFAULT,
+ output_allowed, success,
+ error) ||
+ gdata_parser_strv_from_json_member (reader, "blocked",
+ P_DEFAULT,
+ output_blocked, success,
+ error)) {
+ /* Nothing to do. */
+ }
+
+ json_reader_end_element (reader);
+ }
+
+ /* Success! */
+ *success = TRUE;
+
+ return TRUE;
+}
+
+/* https://developers.google.com/youtube/v3/docs/videos#contentDetails.contentRating */
static gboolean
-parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error)
+content_rating_from_json_member (JsonReader *reader,
+ const gchar *member_name,
+ GDataParserOptions options,
+ GHashTable **output,
+ gboolean *success, GError **error)
+{
+ guint i, len;
+ const GError *child_error = NULL;
+
+ /* Check if there's such element */
+ if (g_strcmp0 (json_reader_get_member_name (reader),
+ member_name) != 0) {
+ return FALSE;
+ }
+
+ /* Check if the output string has already been set. The JSON parser
+ * guarantees this can't happen. */
+ g_assert (!(options & P_NO_DUPES) || *output == NULL);
+
+ len = json_reader_count_members (reader);
+ child_error = json_reader_get_error (reader);
+
+ if (child_error != NULL) {
+ *success = gdata_parser_error_from_json_error (reader,
+ child_error,
+ error);
+ return TRUE;
+ }
+
+ *output = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, g_free);
+
+ for (i = 0; i < len; i++) {
+ const gchar *scheme, *rating;
+
+ json_reader_read_element (reader, i);
+
+ scheme = json_reader_get_member_name (reader);
+ rating = json_reader_get_string_value (reader);
+
+ /* Ignore errors. */
+ if (rating != NULL) {
+ g_hash_table_insert (*output, g_strdup (scheme),
+ g_strdup (rating));
+ }
+
+ json_reader_end_element (reader);
+ }
+
+ /* Success! */
+ *success = TRUE;
+
+ return TRUE;
+}
+
+static guint64
+parse_uint64_from_json_string_member (JsonReader *reader,
+ const gchar *member_name,
+ GError **error)
+{
+ const gchar *str_val, *end_ptr;
+ guint64 out;
+ const GError *child_error = NULL;
+
+ /* Grab the string. */
+ json_reader_read_member (reader, member_name);
+
+ str_val = json_reader_get_string_value (reader);
+ child_error = json_reader_get_error (reader);
+
+ if (child_error != NULL) {
+ gdata_parser_error_from_json_error (reader, child_error, error);
+ out = 0;
+ goto done;
+ }
+
+ /* Try and parse it as an integer. */
+ out = g_ascii_strtoull (str_val, (gchar **) &end_ptr, 10);
+
+ if (*end_ptr != '\0') {
+ gdata_parser_error_required_json_content_missing (reader,
+ error);
+ out = 0;
+ goto done;
+ }
+
+done:
+ json_reader_end_member (reader);
+
+ return out;
+}
+
+static gboolean
+parse_json (GDataParsable *parsable, JsonReader *reader, gpointer user_data, GError **error)
{
gboolean success;
GDataYouTubeVideo *self = GDATA_YOUTUBE_VIDEO (parsable);
+ GDataYouTubeVideoPrivate *priv = self->priv;
+
+/* TODO:
+ GDataMediaCategory *category;
+ GDataMediaCredit *credit;
+ gchar *aspect_ratio;
+*/
+
+ /* TODO: penitence
+ * Reference: https://developers.google.com/youtube/v3/docs/videos/list */
+ if (g_strcmp0 (json_reader_get_member_name (reader), "kind") == 0 &&
+ g_strcmp0 (json_reader_get_string_value (reader),
+ "youtube#videoListResponse") == 0) {
+ priv->parsing_in_video_list_response = TRUE;
+ return TRUE;
+ } else if (g_strcmp0 (json_reader_get_member_name (reader), "items") == 0 &&
+ priv->parsing_in_video_list_response) {
+ guint i;
+
+ /* Instead of a 404 when searching for an invalid ID, the server
+ * returns an empty results list. */
+ if (json_reader_count_elements (reader) != 1) {
+ g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_NOT_FOUND,
+ /* Translators: the parameter is an error message returned by the
server. */
+ _("The requested resource was not found: %s"),
+ "items");
+ return TRUE;
+ }
+
+ /* Parse the first (and only) array element. */
+ json_reader_read_element (reader, 0);
+ priv->parsing_in_video_list_response = FALSE;
+
+ /* Parse all its properties. */
+ for (i = 0; i < (guint) json_reader_count_members (reader); i++) {
+ g_return_val_if_fail (json_reader_read_element (reader, i), NULL);
+
+ if (GDATA_PARSABLE_GET_CLASS (self)->parse_json (GDATA_PARSABLE (self), reader,
user_data, error) == FALSE) {
+ json_reader_end_element (reader);
+ g_object_unref (parsable);
+ break;
+ }
+
+ json_reader_end_element (reader);
+ }
+
+ priv->parsing_in_video_list_response = TRUE;
+ json_reader_end_element (reader);
+
+ return TRUE; /* handled */
+ } else if (priv->parsing_in_video_list_response) {
+ /* Ignore the member. */
+ return TRUE;
+ }
+
+ /* Actual video property parsing. */
+ if (g_strcmp0 (json_reader_get_member_name (reader), "id") == 0) {
+ const gchar *id = NULL;
+
+ /* If this is a youtube#searchResult, the id will be an object:
+ * https://developers.google.com/youtube/v3/docs/search#resource
+ * If it is a youtube#video, the id will be a string:
+ * https://developers.google.com/youtube/v3/docs/videos#resource
+ */
+
+ if (json_reader_is_value (reader)) {
+ id = json_reader_get_string_value (reader);
+ } else if (json_reader_is_object (reader)) {
+ json_reader_read_member (reader, "videoId");
+ id = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+ }
+
+ /* Empty ID? */
+ if (id == NULL || *id == '\0') {
+ return gdata_parser_error_required_json_content_missing (reader, error);
+ }
+
+ _gdata_entry_set_id (GDATA_ENTRY (parsable), id);
+
+ return TRUE;
+ } else if (g_strcmp0 (json_reader_get_member_name (reader),
+ "snippet") == 0) {
+ guint i;
+
+ /* Check this is an object. */
+ if (!json_reader_is_object (reader)) {
+ return gdata_parser_error_required_json_content_missing (reader, error);
+ }
+ for (i = 0; i < (guint) json_reader_count_members (reader); i++) {
+ gint64 published_at;
+ gchar *title = NULL, *description = NULL;
+
+ json_reader_read_element (reader, i);
+
+ /* TODO */
+ if (gdata_parser_int64_time_from_json_member (reader, "publishedAt", P_DEFAULT,
&published_at, &success, error)) {
+ if (success) {
+ _gdata_entry_set_published (GDATA_ENTRY (parsable),
+ published_at);
+ }
+ } else if (gdata_parser_string_from_json_member (reader, "title", P_DEFAULT, &title,
&success, error)) {
+ if (success) {
+ gdata_entry_set_title (GDATA_ENTRY (parsable),
+ title);
+ }
+
+ g_free (title);
+ } else if (gdata_parser_string_from_json_member (reader, "description", P_DEFAULT,
&description, &success, error)) {
+ if (success) {
+ gdata_entry_set_summary (GDATA_ENTRY (parsable),
+ description);
+ }
+
+ g_free (description);
+ } else if (gdata_parser_strv_from_json_member (reader, "tags", P_DEFAULT,
&priv->keywords, &success, error) ||
+ thumbnails_from_json_member (reader, "thumbnails", P_DEFAULT,
&priv->thumbnails, &success, error)) {
+ /* TODO */
+ } else {
+ /* TODO */
+ }
+
+ json_reader_end_element (reader);
+ }
+ } else if (g_strcmp0 (json_reader_get_member_name (reader),
+ "contentDetails") == 0) {
+ guint i;
+
+ /* Check this is an object. */
+ if (!json_reader_is_object (reader)) {
+ return gdata_parser_error_required_json_content_missing (reader, error);
+ }
+
+ for (i = 0; i < (guint) json_reader_count_members (reader); i++) {
+ json_reader_read_element (reader, i);
+
+ /* TODO: Check formats for all of these match up with MediaGroup documentation */
+ if (duration_from_json_member (reader, "duration", P_DEFAULT, &priv->duration,
&success, error) ||
+ restricted_countries_from_json_member (reader, "regionRestriction", P_DEFAULT,
&priv->region_restriction_allowed, &priv->region_restriction_blocked, &success, error) ||
+ content_rating_from_json_member (reader, "contentRating", P_DEFAULT,
&priv->content_ratings, &success, error)) {
+ /* TODO */
+ }
+
+ json_reader_end_element (reader);
+ }
+ } else if (g_strcmp0 (json_reader_get_member_name (reader),
+ "status") == 0) {
+ guint i;
+
+ /* Check this is an object. */
+ if (!json_reader_is_object (reader)) {
+ return gdata_parser_error_required_json_content_missing (reader, error);
+ }
+
+ for (i = 0; i < (guint) json_reader_count_members (reader); i++) {
+ gchar *privacy_status = NULL;
+
+ json_reader_read_element (reader, i);
+
+ if (gdata_parser_string_from_json_member (reader, "privacyStatus", P_DEFAULT,
&privacy_status, &success, error)) {
+ priv->is_private = (success && g_strcmp0 (privacy_status, "private") == 0);
+ }
+
+ json_reader_end_element (reader);
+
+ g_free (privacy_status);
+ }
+ } else if (g_strcmp0 (json_reader_get_member_name (reader),
+ "statistics") == 0) {
+ gint64 likes, dislikes;
+ GError *child_error = NULL;
+
+ /* Check this is an object. */
+ if (!json_reader_is_object (reader)) {
+ return gdata_parser_error_required_json_content_missing (reader, error);
+ }
+
+ /* Views and favourites. For some unknown reason, the feed
+ * returns them as a string, even though they’re documented as
+ * being unsigned longs.
+ *
+ * Reference: https://developers.google.com/youtube/v3/docs/videos#statistics */
+ priv->view_count = parse_uint64_from_json_string_member (reader, "viewCount", &child_error);
+ if (child_error != NULL) {
+ g_propagate_error (error, child_error);
+ return TRUE;
+ }
+
+ priv->favorite_count = parse_uint64_from_json_string_member (reader, "favoriteCount",
&child_error);
+ if (child_error != NULL) {
+ g_propagate_error (error, child_error);
+ return TRUE;
+ }
+
+ /* The new ratings API (total likes, total dislikes) doesn’t
+ * really match with the old API (collection of integer ratings
+ * between 1 and 5). Try and return something appropriate. */
+ likes = parse_uint64_from_json_string_member (reader, "likeCount", &child_error);
+ if (child_error != NULL) {
+ g_propagate_error (error, child_error);
+ return TRUE;
+ }
+
+ dislikes = parse_uint64_from_json_string_member (reader, "dislikeCount", &child_error);
+ if (child_error != NULL) {
+ g_propagate_error (error, child_error);
+ return TRUE;
+ }
+
+ priv->rating.min = 0;
+ priv->rating.max = 1;
+ priv->rating.count = likes + dislikes;
+ if (likes + dislikes == 0) {
+ priv->rating.average = 0.0; /* basically undefined */
+ } else {
+ priv->rating.average = (gdouble) likes / (gdouble) (likes + dislikes);
+ }
+ } else {
+ return GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->parse_json (parsable, reader,
user_data, error);
+ }
+
+#if 0
+TODO
if (gdata_parser_is_namespace (node, "http://search.yahoo.com/mrss/") == TRUE &&
gdata_parser_object_from_element (node, "group", P_REQUIRED | P_NO_DUPES,
GDATA_TYPE_YOUTUBE_GROUP,
&(self->priv->media_group), &success, error) == TRUE) {
@@ -736,25 +1243,6 @@ parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_da
} else if (gdata_parser_is_namespace (node, "http://gdata.youtube.com/schemas/2007") == TRUE) {
if (gdata_parser_string_from_element (node, "location", P_NONE, &(self->priv->location),
&success, error) == TRUE) {
return success;
- } else if (xmlStrcmp (node->name, (xmlChar*) "statistics") == 0) {
- /* yt:statistics */
- xmlChar *view_count, *favorite_count;
-
- /* View count */
- view_count = xmlGetProp (node, (xmlChar*) "viewCount");
- if (view_count == NULL)
- return gdata_parser_error_required_property_missing (node, "viewCount",
error);
- self->priv->view_count = g_ascii_strtoull ((gchar*) view_count, NULL, 10);
- xmlFree (view_count);
-
- /* Favourite count */
- favorite_count = xmlGetProp (node, (xmlChar*) "favoriteCount");
- self->priv->favorite_count = (favorite_count != NULL) ? g_ascii_strtoull ((gchar*)
favorite_count, NULL, 10) : 0;
- xmlFree (favorite_count);
- } else if (xmlStrcmp (node->name, (xmlChar*) "noembed") == 0) {
- /* yt:noembed */
- /* Ignore this now; it's been superceded by yt:accessControl.
- * See http://apiblog.youtube.com/2010/02/extended-access-controls-available-via.html
*/
} else if (xmlStrcmp (node->name, (xmlChar*) "accessControl") == 0) {
/* yt:accessControl */
xmlChar *action, *permission;
@@ -798,77 +1286,25 @@ parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_da
}
xmlFree (recorded);
gdata_youtube_video_set_recorded (self, recorded_int64);
- } else {
- return GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->parse_xml (parsable,
doc, node, user_data, error);
- }
- } else if (gdata_parser_is_namespace (node, "http://schemas.google.com/g/2005") == TRUE) {
- if (xmlStrcmp (node->name, (xmlChar*) "rating") == 0) {
- /* gd:rating */
- xmlChar *min, *max, *num_raters, *average;
- guint num_raters_uint;
- gdouble average_double;
-
- min = xmlGetProp (node, (xmlChar*) "min");
- if (min == NULL)
- return gdata_parser_error_required_property_missing (node, "min", error);
-
- max = xmlGetProp (node, (xmlChar*) "max");
- if (max == NULL) {
- gdata_parser_error_required_property_missing (node, "max", error);
- xmlFree (min);
- return FALSE;
- }
-
- num_raters = xmlGetProp (node, (xmlChar*) "numRaters");
- if (num_raters == NULL)
- num_raters_uint = 0;
- else
- num_raters_uint = g_ascii_strtoull ((gchar*) num_raters, NULL, 10);
- xmlFree (num_raters);
-
- average = xmlGetProp (node, (xmlChar*) "average");
- if (average == NULL)
- average_double = 0;
- else
- average_double = g_ascii_strtod ((gchar*) average, NULL);
- xmlFree (average);
-
- self->priv->rating.min = g_ascii_strtoull ((gchar*) min, NULL, 10);
- self->priv->rating.max = g_ascii_strtoull ((gchar*) max, NULL, 10);
- self->priv->rating.count = num_raters_uint;
- self->priv->rating.average = average_double;
- } else if (xmlStrcmp (node->name, (xmlChar*) "comments") == 0) {
- /* gd:comments */
- xmlNode *child_node;
-
- /* This is actually the child of the <comments> element */
- child_node = node->children;
- if (child_node == NULL) {
- return gdata_parser_error_required_element_missing ("gd:feedLink",
"gd:comments", error);
- }
-
- if (gdata_parser_object_from_element (child_node, "feedLink", P_REQUIRED |
P_NO_DUPES, GDATA_TYPE_GD_FEED_LINK,
- &(self->priv->comments_feed_link), &success,
error) == TRUE) {
- return success;
- }
- } else {
- return GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->parse_xml (parsable,
doc, node, user_data, error);
- }
- } else {
- return GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->parse_xml (parsable, doc,
node, user_data, error);
- }
+#endif
return TRUE;
}
static gboolean
-post_parse_xml (GDataParsable *parsable, gpointer user_data, GError **error)
+post_parse_json (GDataParsable *parsable, gpointer user_data, GError **error)
{
GDataYouTubeVideoPrivate *priv = GDATA_YOUTUBE_VIDEO (parsable)->priv;
+ GDataParsableClass *parsable_class;
/* Chain up to the parent class */
- GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->post_parse_xml (parsable, user_data, error);
+ parsable_class = GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class);
+ if (parsable_class->post_parse_json != NULL) {
+ parsable_class->post_parse_json (parsable, user_data, error);
+ }
+
+/* TODO: check this */
/* This must always exist, so is_draft can be set on it */
if (priv->youtube_control == NULL)
priv->youtube_control = g_object_new (GDATA_TYPE_YOUTUBE_CONTROL, NULL);
@@ -901,78 +1337,130 @@ access_control_cb (const gchar *action, gpointer value, GString *xml_string)
}
static void
-get_xml (GDataParsable *parsable, GString *xml_string)
+get_json (GDataParsable *parsable, JsonBuilder *builder)
{
+ GDataEntry *entry = GDATA_ENTRY (parsable);
GDataYouTubeVideoPrivate *priv = GDATA_YOUTUBE_VIDEO (parsable)->priv;
/* Chain up to the parent class */
- GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->get_xml (parsable, xml_string);
+ GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->get_json (parsable, builder);
+
+ /* Add the video-specific JSON.
+ * Reference:
+ * https://developers.google.com/youtube/v3/docs/videos/insert#request_body */
+ /* snippet object. */
+ json_builder_set_member_name (builder, "snippet");
+ json_builder_begin_object (builder);
+
+ if (gdata_entry_get_title (entry) != NULL) {
+ json_builder_set_member_name (builder, "title");
+ json_builder_add_string_value (builder,
+ gdata_entry_get_title (entry));
+ }
- /* media:group */
- _gdata_parsable_get_xml (GDATA_PARSABLE (priv->media_group), xml_string, FALSE);
+ if (gdata_entry_get_summary (entry) != NULL) {
+ json_builder_set_member_name (builder, "description");
+ json_builder_add_string_value (builder,
+ gdata_entry_get_summary (entry));
+ }
+
+ /* TODO: tags[] */
+ /* TODO: categoryId */
+
+ json_builder_end_object (builder);
+
+ /* status object. */
+ json_builder_set_member_name (builder, "status");
+ json_builder_begin_object (builder);
+
+ json_builder_set_member_name (builder, "privacyStatus");
+ json_builder_add_string_value (builder,
+ priv->is_private ? "private" : "public");
+
+ /* TODO:
+ publicStatsViewable
+ publishAt
+ embeddable
+ license
+ */
+
+ json_builder_end_object (builder);
+
+ /* recordingDetails object. */
+ json_builder_set_member_name (builder, "recordingDetails");
+ json_builder_begin_object (builder);
+
+ if (priv->location != NULL) {
+ json_builder_set_member_name (builder, "locationDescription");
+ json_builder_add_string_value (builder, priv->location);
+ }
+
+ if (priv->georss_where != NULL &&
+ gdata_georss_where_get_latitude (priv->georss_where) != G_MAXDOUBLE &&
+ gdata_georss_where_get_longitude (priv->georss_where) != G_MAXDOUBLE) {
+ json_builder_set_member_name (builder, "location");
+ json_builder_begin_object (builder);
+
+ json_builder_set_member_name (builder, "latitude");
+ json_builder_add_double_value (builder,
+ gdata_georss_where_get_latitude (priv->georss_where));
- if (priv->location != NULL)
- gdata_parser_string_append_escaped (xml_string, "<yt:location>", priv->location,
"</yt:location>");
+ json_builder_set_member_name (builder, "longitude");
+ json_builder_add_double_value (builder,
+ gdata_georss_where_get_longitude (priv->georss_where));
+
+ json_builder_end_object (builder);
+ }
if (priv->recorded != -1) {
gchar *recorded = gdata_parser_date_from_int64 (priv->recorded);
- g_string_append_printf (xml_string, "<yt:recorded>%s</yt:recorded>", recorded);
+ json_builder_set_member_name (builder, "recordingDate");
+ json_builder_add_string_value (builder, recorded);
g_free (recorded);
}
+ json_builder_end_object (builder);
+
+#if 0
+TODO
+ /* media:group */
+ _gdata_parsable_get_xml (GDATA_PARSABLE (priv->media_group), xml_string, FALSE);
+
/* yt:accessControl */
g_hash_table_foreach (priv->access_controls, (GHFunc) access_control_cb, xml_string);
/* app:control */
_gdata_parsable_get_xml (GDATA_PARSABLE (priv->youtube_control), xml_string, FALSE);
-
- /* georss:where */
- if (priv->georss_where != NULL && gdata_georss_where_get_latitude (priv->georss_where) != G_MAXDOUBLE
&&
- gdata_georss_where_get_longitude (priv->georss_where) != G_MAXDOUBLE) {
- _gdata_parsable_get_xml (GDATA_PARSABLE (priv->georss_where), xml_string, FALSE);
- }
+#endif
}
-static void
-get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
+static const gchar *
+get_content_type (void)
{
- GDataYouTubeVideoPrivate *priv = GDATA_YOUTUBE_VIDEO (parsable)->priv;
-
- /* Chain up to the parent class */
- GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->get_namespaces (parsable, namespaces);
-
- g_hash_table_insert (namespaces, (gchar*) "yt", (gchar*) "http://gdata.youtube.com/schemas/2007");
-
- /* Add the media:group, app:control and georss:where namespaces */
- GDATA_PARSABLE_GET_CLASS (priv->media_group)->get_namespaces (GDATA_PARSABLE (priv->media_group),
namespaces);
- GDATA_PARSABLE_GET_CLASS (priv->youtube_control)->get_namespaces (GDATA_PARSABLE
(priv->youtube_control), namespaces);
- GDATA_PARSABLE_GET_CLASS (priv->georss_where)->get_namespaces (GDATA_PARSABLE (priv->georss_where),
namespaces);
+ return "application/json";
}
static gchar *
get_entry_uri (const gchar *id)
{
- /* The entry ID is in the format: "tag:youtube.com,2008:video:QjA5faZF1A8"; we want the bit after
"video" */
- const gchar *video_id = NULL;
- gchar **parts, *uri;
- guint i;
+ const gchar *old_prefix = "tag:youtube.com,2008:video:";
- parts = g_strsplit (id, ":", -1);
-
- for (i = 0; parts[i] != NULL && parts[i + 1] != NULL; i += 2) {
- if (strcmp (parts[i], "video") == 0) {
- video_id = parts[i + 1];
- break;
- }
+ /* For compatibility with previous video ID formats, strip off the v2
+ * ID prefix. */
+ if (g_str_has_prefix (id, old_prefix)) {
+ id += strlen (old_prefix);
}
- g_assert (video_id != NULL);
-
- /* Build the URI using the video ID */
- uri = g_strconcat ("https://gdata.youtube.com/feeds/api/videos/", video_id, NULL);
- g_strfreev (parts);
-
- return uri;
+ /* Build the query URI for a single video. This is a bit of a pain,
+ * because it actually returns a list containing a single video, but
+ * there seems no other way to do it. See parsing_in_video_list_response
+ * in parse_json() for the fallout.
+ *
+ * Reference: https://developers.google.com/youtube/v3/docs/videos/list#part */
+ return g_strdup_printf ("https://www.googleapis.com/youtube/v3/videos"
+ "?part=contentDetails,id,recordingDetails,"
+ "snippet,status,statistics"
+ "&id=%s", id);
}
static GDataAuthorizationDomain *
@@ -984,35 +1472,24 @@ get_authorization_domain (GDataCommentable *self)
static gchar *
get_query_comments_uri (GDataCommentable *self)
{
- GDataGDFeedLink *feed_link;
-
- feed_link = GDATA_YOUTUBE_VIDEO (self)->priv->comments_feed_link;
-
- if (feed_link == NULL) {
- return NULL;
- }
-
- return _gdata_service_fix_uri_scheme (gdata_gd_feed_link_get_uri (feed_link));
+ /* FIXME: Currently unsupported:
+ * https://developers.google.com/youtube/v3/migration-guide#to_be_migrated */
+ return NULL;
}
static gchar *
get_insert_comment_uri (GDataCommentable *self, GDataComment *comment_)
{
- GDataGDFeedLink *feed_link;
-
- feed_link = GDATA_YOUTUBE_VIDEO (self)->priv->comments_feed_link;
-
- if (feed_link == NULL) {
- return NULL;
- }
-
- return _gdata_service_fix_uri_scheme (gdata_gd_feed_link_get_uri (feed_link));
+ /* FIXME: Currently unsupported:
+ * https://developers.google.com/youtube/v3/migration-guide#to_be_migrated */
+ return NULL;
}
static gboolean
is_comment_deletable (GDataCommentable *self, GDataComment *comment_)
{
- /* Deletion of comments is unsupported. */
+ /* FIXME: Currently unsupported:
+ * https://developers.google.com/youtube/v3/migration-guide#to_be_migrated */
return FALSE;
}
@@ -1150,6 +1627,10 @@ gdata_youtube_video_set_access_control (GDataYouTubeVideo *self, const gchar *ac
* @average: (out caller-allocates) (allow-none): return location for the average rating value, or %NULL
*
* Gets various properties of the ratings on the video.
+ *
+ * Note that this property may not be retrieved when querying for multiple
+ * videos at once, but is guaranteed to be retrieved when querying with
+ * gdata_service_query_single_entry_async().
**/
void
gdata_youtube_video_get_rating (GDataYouTubeVideo *self, guint *min, guint *max, guint *count, gdouble
*average)
@@ -1177,7 +1658,7 @@ const gchar * const *
gdata_youtube_video_get_keywords (GDataYouTubeVideo *self)
{
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
- return gdata_media_group_get_keywords (self->priv->media_group);
+ return (const gchar * const *) self->priv->keywords;
}
/**
@@ -1196,7 +1677,8 @@ gdata_youtube_video_set_keywords (GDataYouTubeVideo *self, const gchar * const *
g_return_if_fail (GDATA_IS_YOUTUBE_VIDEO (self));
g_return_if_fail (keywords != NULL);
- gdata_media_group_set_keywords (self->priv->media_group, keywords);
+ g_strfreev (self->priv->keywords);
+ self->priv->keywords = g_strdupv ((gchar **) keywords);
g_object_notify (G_OBJECT (self), "keywords");
}
@@ -1211,8 +1693,20 @@ gdata_youtube_video_set_keywords (GDataYouTubeVideo *self, const gchar * const *
const gchar *
gdata_youtube_video_get_player_uri (GDataYouTubeVideo *self)
{
+ GDataYouTubeVideoPrivate *priv;
+
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
- return gdata_media_group_get_player_uri (self->priv->media_group);
+
+ priv = self->priv;
+
+ /* Generate and cache the player URI. */
+ if (priv->player_uri == NULL) {
+ priv->player_uri = g_strconcat ("https://www.youtube.com/watch?v=",
+ gdata_entry_get_id (GDATA_ENTRY (self)),
+ NULL);
+ }
+
+ return priv->player_uri;
}
/**
@@ -1230,10 +1724,57 @@ gdata_youtube_video_get_player_uri (GDataYouTubeVideo *self)
gboolean
gdata_youtube_video_is_restricted_in_country (GDataYouTubeVideo *self, const gchar *country)
{
+ GDataYouTubeVideoPrivate *priv;
+
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), FALSE);
g_return_val_if_fail (country != NULL && *country != '\0', FALSE);
- return gdata_media_group_is_restricted_in_country (self->priv->media_group, country);
+ priv = self->priv;
+
+ return (!g_strv_contains ((const gchar * const *) priv->region_restriction_allowed, country) &&
+ (g_strv_contains ((const gchar * const *) priv->region_restriction_blocked, country) ||
+ priv->region_restriction_allowed == NULL ||
+ priv->region_restriction_allowed[0] == NULL));
+}
+
+static const gchar *
+convert_mpaa_rating (const gchar *v3_rating)
+{
+ if (g_strcmp0 (v3_rating, "mpaaG") == 0) {
+ return "g";
+ } else if (g_strcmp0 (v3_rating, "mpaaNc17") == 0) {
+ return "nc-17";
+ } else if (g_strcmp0 (v3_rating, "mpaaPg") == 0) {
+ return "pg";
+ } else if (g_strcmp0 (v3_rating, "mpaaPg13") == 0) {
+ return "pg-13";
+ } else if (g_strcmp0 (v3_rating, "mpaaR") == 0) {
+ return "r";
+ } else {
+ return NULL;
+ }
+}
+
+static const gchar *
+convert_tvpg_rating (const gchar *v3_rating)
+{
+ if (g_strcmp0 (v3_rating, "pg14") == 0) {
+ return "tv-14";
+ } else if (g_strcmp0 (v3_rating, "tvpgG") == 0) {
+ return "tv-g";
+ } else if (g_strcmp0 (v3_rating, "tvpgMa") == 0) {
+ return "tv-ma";
+ } else if (g_strcmp0 (v3_rating, "tvpgPg") == 0) {
+ return "tv-pg";
+ } else if (g_strcmp0 (v3_rating, "tvpgY") == 0) {
+ return "tv-y";
+ } else if (g_strcmp0 (v3_rating, "tvpgY7") == 0) {
+ return "tv-y7";
+ } else if (g_strcmp0 (v3_rating, "tvpgY7Fv") == 0) {
+ return "tv-y7-fv";
+ } else {
+ return NULL;
+ }
}
/**
@@ -1257,10 +1798,31 @@ gdata_youtube_video_is_restricted_in_country (GDataYouTubeVideo *self, const gch
const gchar *
gdata_youtube_video_get_media_rating (GDataYouTubeVideo *self, const gchar *rating_type)
{
+ const gchar *rating;
+
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
g_return_val_if_fail (rating_type != NULL && *rating_type != '\0', NULL);
- return gdata_media_group_get_media_rating (self->priv->media_group, rating_type);
+ /* All ratings are unknown. */
+ if (self->priv->content_ratings == NULL) {
+ return NULL;
+ }
+
+ /* Compatibility with the old API. */
+ if (g_strcmp0 (rating_type, "simple") == 0) {
+ /* Not supported any more. */
+ return NULL;
+ } else if (g_strcmp0 (rating_type, "mpaa") == 0) {
+ rating = g_hash_table_lookup (self->priv->content_ratings,
+ "mpaaRating");
+ return convert_mpaa_rating (rating);
+ } else if (g_strcmp0 (rating_type, "v-chip") == 0) {
+ rating = g_hash_table_lookup (self->priv->content_ratings,
+ "tvpgRating");
+ return convert_tvpg_rating (rating);
+ }
+
+ return g_hash_table_lookup (self->priv->content_ratings, rating_type);
}
/**
@@ -1275,7 +1837,7 @@ GDataMediaCategory *
gdata_youtube_video_get_category (GDataYouTubeVideo *self)
{
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
- return gdata_media_group_get_category (self->priv->media_group);
+ return self->priv->category;
}
/**
@@ -1294,7 +1856,9 @@ gdata_youtube_video_set_category (GDataYouTubeVideo *self, GDataMediaCategory *c
g_return_if_fail (GDATA_IS_YOUTUBE_VIDEO (self));
g_return_if_fail (GDATA_IS_MEDIA_CATEGORY (category));
- gdata_media_group_set_category (self->priv->media_group, category);
+ g_object_ref (category);
+ g_object_unref (self->priv->category);
+ self->priv->category = category;
g_object_notify (G_OBJECT (self), "category");
}
@@ -1310,7 +1874,7 @@ GDataYouTubeCredit *
gdata_youtube_video_get_credit (GDataYouTubeVideo *self)
{
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
- return GDATA_YOUTUBE_CREDIT (gdata_media_group_get_credit (self->priv->media_group));
+ return GDATA_YOUTUBE_CREDIT (self->priv->credit);
}
/**
@@ -1325,7 +1889,7 @@ const gchar *
gdata_youtube_video_get_description (GDataYouTubeVideo *self)
{
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
- return gdata_media_group_get_description (self->priv->media_group);
+ return gdata_entry_get_summary (GDATA_ENTRY (self));
}
/**
@@ -1341,8 +1905,7 @@ void
gdata_youtube_video_set_description (GDataYouTubeVideo *self, const gchar *description)
{
g_return_if_fail (GDATA_IS_YOUTUBE_VIDEO (self));
-
- gdata_media_group_set_description (self->priv->media_group, description);
+ gdata_entry_set_summary (GDATA_ENTRY (self), description);
g_object_notify (G_OBJECT (self), "description");
}
@@ -1355,6 +1918,8 @@ gdata_youtube_video_set_description (GDataYouTubeVideo *self, const gchar *descr
* a list of URIs to various formats of the video itself, such as its SWF URI or RTSP stream.
*
* Return value: (transfer none): a #GDataYouTubeContent matching @type, or %NULL
+ * Deprecated: UNRELEASED: This is no longer supported by Google, and will
+ * always return %NULL.
**/
GDataYouTubeContent *
gdata_youtube_video_look_up_content (GDataYouTubeVideo *self, const gchar *type)
@@ -1362,7 +1927,8 @@ gdata_youtube_video_look_up_content (GDataYouTubeVideo *self, const gchar *type)
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
g_return_val_if_fail (type != NULL, NULL);
- return GDATA_YOUTUBE_CONTENT (gdata_media_group_look_up_content (self->priv->media_group, type));
+ /* Not supported in the v3 API. */
+ return NULL;
}
/**
@@ -1377,7 +1943,7 @@ GList *
gdata_youtube_video_get_thumbnails (GDataYouTubeVideo *self)
{
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
- return gdata_media_group_get_thumbnails (self->priv->media_group);
+ return self->priv->thumbnails;
}
/**
@@ -1392,7 +1958,7 @@ guint
gdata_youtube_video_get_duration (GDataYouTubeVideo *self)
{
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), 0);
- return gdata_youtube_group_get_duration (GDATA_YOUTUBE_GROUP (self->priv->media_group));
+ return self->priv->duration;
}
/**
@@ -1407,7 +1973,7 @@ gboolean
gdata_youtube_video_is_private (GDataYouTubeVideo *self)
{
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), FALSE);
- return gdata_youtube_group_is_private (GDATA_YOUTUBE_GROUP (self->priv->media_group));
+ return self->priv->is_private;
}
/**
@@ -1421,7 +1987,7 @@ void
gdata_youtube_video_set_is_private (GDataYouTubeVideo *self, gboolean is_private)
{
g_return_if_fail (GDATA_IS_YOUTUBE_VIDEO (self));
- gdata_youtube_group_set_is_private (GDATA_YOUTUBE_GROUP (self->priv->media_group), is_private);
+ self->priv->is_private = is_private;
g_object_notify (G_OBJECT (self), "is-private");
}
@@ -1437,7 +2003,7 @@ gint64
gdata_youtube_video_get_uploaded (GDataYouTubeVideo *self)
{
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), -1);
- return gdata_youtube_group_get_uploaded (GDATA_YOUTUBE_GROUP (self->priv->media_group));
+ return gdata_entry_get_published (GDATA_ENTRY (self));
}
/**
@@ -1447,12 +2013,13 @@ gdata_youtube_video_get_uploaded (GDataYouTubeVideo *self)
* Gets the #GDataYouTubeVideo:video-id property.
*
* Return value: the video's unique and permanent ID
+ * Deprecated: UNRELEASED: This is now equal to #GDataEntry:id.
**/
const gchar *
gdata_youtube_video_get_video_id (GDataYouTubeVideo *self)
{
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
- return gdata_youtube_group_get_video_id (GDATA_YOUTUBE_GROUP (self->priv->media_group));
+ return gdata_entry_get_id (GDATA_ENTRY (self));
}
/**
@@ -1622,7 +2189,7 @@ const gchar *
gdata_youtube_video_get_aspect_ratio (GDataYouTubeVideo *self)
{
g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
- return gdata_youtube_group_get_aspect_ratio (GDATA_YOUTUBE_GROUP (self->priv->media_group));
+ return self->priv->aspect_ratio;
}
/**
@@ -1639,7 +2206,9 @@ void
gdata_youtube_video_set_aspect_ratio (GDataYouTubeVideo *self, const gchar *aspect_ratio)
{
g_return_if_fail (GDATA_IS_YOUTUBE_VIDEO (self));
- gdata_youtube_group_set_aspect_ratio (GDATA_YOUTUBE_GROUP (self->priv->media_group), aspect_ratio);
+
+ g_free (self->priv->aspect_ratio);
+ self->priv->aspect_ratio = g_strdup (aspect_ratio);
g_object_notify (G_OBJECT (self), "aspect-ratio");
}
diff --git a/gdata/services/youtube/gdata-youtube-video.h b/gdata/services/youtube/gdata-youtube-video.h
index 8fc5ce5..68dd91d 100644
--- a/gdata/services/youtube/gdata-youtube-video.h
+++ b/gdata/services/youtube/gdata-youtube-video.h
@@ -103,6 +103,7 @@ G_BEGIN_DECLS
* A rating type to pass to gdata_youtube_video_get_media_rating() for “simple” ratings. The values which
can be returned for such ratings are:
* <code class="literal">adult</code> and <code class="literal">nonadult</code>.
*
+ * Deprecated: TODO
* Since: 0.10.0
*/
#define GDATA_YOUTUBE_RATING_TYPE_SIMPLE "simple"
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]