[libgdata/wip/pwithnall/youtube-v3: 2/2] youtube: WIP work to port to API v3



commit 7314b219e086536be663b0ee530e4050b3abdfaf
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                    |    2 +-
 gdata/gdata-parser.c                           |  186 +++++++-
 gdata/gdata-parser.h                           |   17 +
 gdata/media/gdata-media-thumbnail.c            |   31 ++
 gdata/services/youtube/gdata-youtube-query.c   |    3 +
 gdata/services/youtube/gdata-youtube-service.c |  451 ++++++++++-------
 gdata/services/youtube/gdata-youtube-video.c   |  640 +++++++++++++++++++-----
 gdata/services/youtube/gdata-youtube-video.h   |    1 +
 9 files changed, 1029 insertions(+), 308 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..b33ebb6 100644
--- a/demos/youtube/youtube-cli.c
+++ b/demos/youtube/youtube-cli.c
@@ -21,7 +21,7 @@
 #include <locale.h>
 #include <string.h>
 
-#define DEVELOPER_KEY 
"AI39si7Me3Q7zYs6hmkFvpRBD2nrkVjYYsUO5lh_3HdOkGRc9g6Z4nzxZatk_aAo2EsA21k7vrda0OO6oFg2rnhMedZXPyXoEw"
+#define DEVELOPER_KEY "AIzaSyCENhl8yDxDZbyhTF6p-ok-RefK07xdXUg"
 
 static int
 print_usage (char *argv[])
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/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..24e99b9 100644
--- a/gdata/services/youtube/gdata-youtube-query.c
+++ b/gdata/services/youtube/gdata-youtube-query.c
@@ -455,6 +455,8 @@ get_query_uri (GDataQuery *self, const gchar *feed_uri, GString *query_uri, gboo
        /* Chain up to the parent class */
        GDATA_QUERY_CLASS (gdata_youtube_query_parent_class)->get_query_uri (self, feed_uri, query_uri, 
params_started);
 
+#if 0
+TODO
        APPEND_SEP
        switch (priv->age) {
                case GDATA_YOUTUBE_AGE_TODAY:
@@ -538,6 +540,7 @@ get_query_uri (GDataQuery *self, const gchar *feed_uri, GString *query_uri, gboo
                g_string_append (query_uri, "&license=");
                g_string_append_uri_escaped (query_uri, priv->license, NULL, FALSE);
        }
+#endif
 }
 
 /**
diff --git a/gdata/services/youtube/gdata-youtube-service.c b/gdata/services/youtube/gdata-youtube-service.c
index e1210ae..c1b5d95 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."
+ *      }
+ *     }
+ */
+/* TODO: 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;
+       }
+
+       reader = json_reader_new (json_parser_get_root (parser));
 
-       /* Get the root element */
-       node = xmlDocGetRootElement (doc);
-       if (node == NULL) {
-               /* XML document's empty; chain up to the parent class */
-               xmlFreeDoc (doc);
+       /* 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()
  *
@@ -550,6 +613,22 @@ gdata_youtube_service_new (const gchar *developer_key, GDataAuthorizer *authoriz
                             NULL);
 }
 
+/* Standard list of ‘part’ parameter values used for most queries.
+ * Reference: https://developers.google.com/youtube/v3/docs/videos/list#part */
+#define STANDARD_VIDEO_PART \
+       "contentDetails," \
+       "fileDetails," \
+       "id," \
+       "liveStreamingDetails," \
+       "player," \
+       "processingDetails," \
+       "recordingDetails," \
+       "snippet," \
+       "statistics," \
+       "status," \
+       "suggestions," \
+       "topicDetails"
+
 /**
  * gdata_youtube_service_get_primary_authorization_domain:
  *
@@ -573,26 +652,22 @@ static const gchar *
 standard_feed_type_to_feed_uri (GDataYouTubeStandardFeedType feed_type)
 {
        switch (feed_type) {
+       case GDATA_YOUTUBE_MOST_POPULAR_FEED:
+               return "https://www.googleapis.com/youtube/v3/videos?part="; STANDARD_VIDEO_PART 
"&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";;
+       /* TODO: Maybe see 'My most-viewed videos' on 
https://developers.google.com/youtube/v3/sample_requests */
+       /* TODO: test */
+               g_warning ("The %u standard feed type is deprecated. Returning "
+                          "a generic feed instead.", feed_type);
+               return "https://www.googleapis.com/youtube/v3/videos?part="; STANDARD_VIDEO_PART;
        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 *
@@ -694,8 +771,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 +814,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 +837,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 +846,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 +855,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 +900,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 +908,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 +954,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,18 +968,24 @@ 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,
                                     _("You must be authenticated to upload a video."));
                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));
+/* TODO: check against https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol */
+/* TODO: deprecate this method and create a upload_video_resumable() version */
+       stream = gdata_upload_stream_new_resumable (GDATA_SERVICE (self),
+                                                   get_youtube_authorization_domain (),
+                                                   SOUP_METHOD_POST,
+                                                   
"https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part="; STANDARD_VIDEO_PART,
+                                                   GDATA_ENTRY (video), slug,
+                                                   content_type,
+                                                   -1,  /* content_length, */
+                                                   cancellable);
+       return GDATA_UPLOAD_STREAM (stream);
 }
 
 /**
@@ -934,7 +1020,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 +1059,7 @@ gdata_youtube_service_get_developer_key (GDataYouTubeService *self)
 GDataAPPCategories *
 gdata_youtube_service_get_categories (GDataYouTubeService *self, GCancellable *cancellable, GError **error)
 {
+       gchar *uri;
        SoupMessage *message;
        GDataAPPCategories *categories;
 
@@ -978,15 +1068,24 @@ gdata_youtube_service_get_categories (GDataYouTubeService *self, GCancellable *c
        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);
+       uri = g_strdup_printf ("https://www.googleapis.com/youtube/v3/videoCategories";
+                              "?part=" STANDARD_VIDEO_PART
+                              "&regionCode=%s",
+                              gdata_service_get_locale (GDATA_SERVICE (self)));
+       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..a956ef8 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,14 @@
  *     g_object_unref (video);
  *     </programlisting>
  * </example>
+ *
+ * TODO: update
  **/
 
 #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 +95,9 @@ 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 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 +119,18 @@ 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;
+       GList *contents; /* GDataMediaContent */
+       GDataMediaCredit *credit;
+       guint duration;
+       gboolean is_private;
+       gchar *aspect_ratio;
 
        /* georss:where */
        GDataGeoRSSWhere *georss_where;
@@ -175,18 +186,17 @@ 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;
 
        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";  /* TODO: 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 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 +525,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 +542,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 +555,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;
@@ -586,6 +578,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_hash_table_unref (priv->content_ratings);
+       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 +620,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, priv->player_uri);
                        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 +656,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 +716,363 @@ gdata_youtube_video_set_property (GObject *object, guint property_id, const GVal
        }
 }
 
+/* https://developers.google.com/youtube/v3/docs/videos#contentDetails.duration */
+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 minutes, 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: ‘PTmMsS’, where ‘m’ is an integer number
+        * of minutes, and ‘s’ is seconds. */
+       i = duration_str;
+       if (strlen (duration_str) < 6 || strncmp (duration_str, "PT", 2) != 0) {
+               goto error;
+       }
+
+       i += 2;  /* PT */
+
+       minutes = g_ascii_strtoull (i, &new_i, 10);
+       if (new_i == i || new_i[0] != 'M') {
+               goto error;
+       }
+
+       i = new_i;
+       i += 1;  /* M */
+
+       seconds = g_ascii_strtoull (i, &i, 10);
+       if (new_i == i || new_i[0] != 'S' || new_i[1] != '\0') {
+               goto error;
+       }
+
+       *output = minutes * 60 + 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 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:
+       gchar *player_uri;
+       GDataMediaCategory *category;
+       GList *contents; /* GDataMediaContent * /
+       GDataMediaCredit *credit;
+       gchar *aspect_ratio;
+*/
+
+       if (g_strcmp0 (json_reader_get_member_name (reader), "id") == 0) {
+               const gchar *id;
+
+               /* 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)) {
+                       if (!json_reader_read_member (reader, "videoId")) {
+                               id = json_reader_get_string_value (reader);
+                               json_reader_end_member (reader);
+                               return gdata_parser_error_required_json_content_missing (reader, error);
+                       }
+
+                       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)) {
+                               _gdata_entry_set_published (GDATA_ENTRY (parsable),
+                                                           published_at);
+                       } else if (gdata_parser_string_from_json_member (reader, "title", P_DEFAULT, &title, 
&success, error)) {
+                               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)) {
+                               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++) {
+                       json_reader_read_element (reader, i);
+
+                       if (privacy_status_from_json_member (reader, "privacyStatus", P_DEFAULT, 
&priv->is_private, &success, error) ||
+                           
+                           ) {
+                               /* TODO */
+                       }
+
+                       json_reader_end_element (reader);
+               }
+       } 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) {
@@ -857,17 +1209,23 @@ parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_da
        } 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);
+       }
 
        /* This must always exist, so is_draft can be set on it */
        if (priv->youtube_control == NULL)
@@ -901,13 +1259,15 @@ 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)
 {
        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);
 
+#if 0
+TODO
        /* media:group */
        _gdata_parsable_get_xml (GDATA_PARSABLE (priv->media_group), xml_string, FALSE);
 
@@ -931,48 +1291,26 @@ get_xml (GDataParsable *parsable, GString *xml_string)
            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)
-{
-       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);
-}
+/* Standard list of ‘part’ parameter values used for most queries.
+ * Reference: https://developers.google.com/youtube/v3/docs/videos/list#part */
+/* TODO: update */
+#define STANDARD_VIDEO_PART \
+       "contentDetails," \
+       "id," \
+       "recordingDetails," \
+       "snippet," \
+       "status"
 
 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;
-
-       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;
-               }
-       }
-
-       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;
+{/* TODO: test query_single_entry() */
+       /* Build the query URI for a single video. */
+       return g_strdup_printf ("https://www.googleapis.com/youtube/v3/videos";
+                               "?part=" STANDARD_VIDEO_PART
+                               "&id=%s", id);
 }
 
 static GDataAuthorizationDomain *
@@ -984,35 +1322,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;
 }
 
@@ -1177,7 +1504,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 +1523,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");
 }
 
@@ -1212,7 +1540,7 @@ const gchar *
 gdata_youtube_video_get_player_uri (GDataYouTubeVideo *self)
 {
        g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
-       return gdata_media_group_get_player_uri (self->priv->media_group);
+       return self->priv->player_uri;
 }
 
 /**
@@ -1230,10 +1558,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 +1632,26 @@ 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);
+       /* 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 +1666,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 +1685,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 +1703,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 +1718,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 +1734,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");
 }
 
@@ -1377,7 +1769,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 +1784,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 +1799,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 +1813,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 +1829,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));
 }
 
 /**
@@ -1452,7 +1844,7 @@ 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 +2014,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 +2031,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]