[libgdata] youtube: Port to the YouTube API version 3



commit 727c13a632fbc6a9fd60d9b66921e12b618b55f5
Author: Philip Withnall <philip tecnocode co uk>
Date:   Sun Apr 19 20:43:17 2015 +0100

    youtube: Port to the YouTube API version 3
    
    Deprecated API:
     • GDataYouTubeQuery:format, gdata_youtube_query_get_format(),
       gdata_youtube_query_set_format()
     • GDataYouTubeQuery:has-location
     • GDataYouTubeQuery:language, gdata_youtube_query_get_language(),
       gdata_youtube_query_set_language()
     • GDataYouTubeQuery:sort-order, gdata_youtube_query_get_sort_order(),
       gdata_youtube_query_set_sort_order()
     • GDataYouTubeQuery:uploader, gdata_youtube_query_get_uploader(),
       gdata_youtube_query_set_uploader()
     • GDataYouTubeSortOrder
     • GDataYouTubeUploader
     • GDataYouTubeContent
     • GDataYouTubeFormat
     • GDataYouTubeVideo:video-id, gdata_youtube_video_get_video_id()
     • gdata_youtube_video_look_up_content()
       (use gdata_youtube_video_get_player_uri() instead)
     • GDATA_YOUTUBE_RATING_TYPE_SIMPLE
     • GDataYouTubeCredit, GDATA_YOUTUBE_CREDIT_ENTITY_PARTNER
     • GDataYouTubeVideo:credit, gdata_youtube_video_get_credit()
     • GDataYouTubeVideo:is-draft, gdata_youtube_video_is_draft(),
       gdata_youtube_video_set_is_draft()
     • GDATA_YOUTUBE_*_FEED except GDATA_YOUTUBE_MOST_POPULAR_FEED (all
       others have been deprecated online and will transparently return the
       most popular videos from the past 24 hours)
    
    API changes:
     • GDataYouTubeQuery:restriction no longer accepts IP addresses
     • All GDataCommentable functionality on GDataYouTubeVideo is disabled
       at runtime until the v3 YouTube API for comments is finished online
     • Not all properties of a GDataYouTubeVideo may be retrieved when doing
       searches, retrieving related videos, or listing standard feeds — to
       guarantee getting all properties, use
       gdata_service_query_single_entry() for the video in question; common
       properties such as title, description, player URI and thumbnails
       will always be retrieved
    
    Internally, the GDataYouTubeControl and GDataYouTubeGroup APIs have been
    removed, but they were never visible publicly, so this is not an API
    break.
    
    Due to the transition from v2 to v3 of the YouTube API, your client’s
    developer key may stop working. Refresh it here:
       https://developers.google.com/youtube/registering_an_application
    
    https://bugzilla.gnome.org/show_bug.cgi?id=687597

 Makefile.am                                    |    5 -
 docs/reference/Makefile.am                     |    2 -
 gdata/services/youtube/gdata-youtube-content.c |   15 +-
 gdata/services/youtube/gdata-youtube-content.h |   31 +-
 gdata/services/youtube/gdata-youtube-control.c |  183 ---
 gdata/services/youtube/gdata-youtube-control.h |   75 --
 gdata/services/youtube/gdata-youtube-credit.c  |   14 +-
 gdata/services/youtube/gdata-youtube-credit.h  |   28 +-
 gdata/services/youtube/gdata-youtube-group.c   |  261 -----
 gdata/services/youtube/gdata-youtube-group.h   |   80 --
 gdata/services/youtube/gdata-youtube-query.c   |  276 ++++-
 gdata/services/youtube/gdata-youtube-query.h   |   37 +-
 gdata/services/youtube/gdata-youtube-service.c |  511 ++++++---
 gdata/services/youtube/gdata-youtube-service.h |   59 +-
 gdata/services/youtube/gdata-youtube-state.c   |   50 +-
 gdata/services/youtube/gdata-youtube-video.c   | 1495 ++++++++++++++++++------
 gdata/services/youtube/gdata-youtube-video.h   |   22 +-
 gdata/tests/youtube.c                          |   20 +
 18 files changed, 1823 insertions(+), 1341 deletions(-)
---
diff --git a/Makefile.am b/Makefile.am
index 1e1e009..34c9539 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -323,9 +323,6 @@ gdata_youtube_headers = \
        gdata/services/youtube/gdata-youtube-category.h \
        gdata/services/youtube/gdata-youtube-comment.h  \
        gdata/services/youtube/gdata-youtube-feed.h
-private_headers += \
-       gdata/services/youtube/gdata-youtube-group.h    \
-       gdata/services/youtube/gdata-youtube-control.h
 gdatayoutubeinclude_HEADERS = \
        $(gdata_youtube_headers)                        \
        gdata/services/youtube/gdata-youtube-enums.h
@@ -449,9 +446,7 @@ gdata_sources = \
        gdata/services/youtube/gdata-youtube-content.c          \
        gdata/services/youtube/gdata-youtube-credit.c           \
        gdata/services/youtube/gdata-youtube-query.c            \
-       gdata/services/youtube/gdata-youtube-group.c            \
        gdata/services/youtube/gdata-youtube-state.c            \
-       gdata/services/youtube/gdata-youtube-control.c          \
        gdata/services/youtube/gdata-youtube-category.c         \
        gdata/services/youtube/gdata-youtube-comment.c          \
        gdata/services/youtube/gdata-youtube-feed.c             \
diff --git a/docs/reference/Makefile.am b/docs/reference/Makefile.am
index d5b020b..0eff964 100644
--- a/docs/reference/Makefile.am
+++ b/docs/reference/Makefile.am
@@ -56,8 +56,6 @@ IGNORE_HFILES = \
        gdata-enums.h           \
        gdata-media-enums.h     \
        gdata-media-group.h     \
-       gdata-youtube-group.h   \
-       gdata-youtube-control.h \
        gdata-youtube-enums.h   \
        gdata-picasaweb-enums.h \
        gdata-documents-enums.h \
diff --git a/gdata/services/youtube/gdata-youtube-content.c b/gdata/services/youtube/gdata-youtube-content.c
index 06c0aa0..dd0de51 100644
--- a/gdata/services/youtube/gdata-youtube-content.c
+++ b/gdata/services/youtube/gdata-youtube-content.c
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009–2010 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009–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
@@ -28,6 +28,8 @@
  * online documentation</ulink>.
  *
  * Since: 0.4.0
+ * Deprecated: UNRELEASED: Accessing YouTube video content directly is no longer
+ *   supported by Google. There is no replacement.
  **/
 
 #include <glib.h>
@@ -38,6 +40,8 @@
 #include "gdata-parser.h"
 #include "gdata-youtube-enums.h"
 
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
 static void gdata_youtube_content_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);
@@ -75,12 +79,15 @@ gdata_youtube_content_class_init (GDataYouTubeContentClass *klass)
         * YouTube documentation</ulink>.
         *
         * Since: 0.4.0
+        * Deprecated: UNRELEASED: Accessing YouTube video content directly is
+        *   no longer supported by Google. There is no replacement.
         **/
        g_object_class_install_property (gobject_class, PROP_FORMAT,
                                         g_param_spec_enum ("format",
                                                            "Format", "The video format of the video object.",
                                                            GDATA_TYPE_YOUTUBE_FORMAT, 
GDATA_YOUTUBE_FORMAT_UNKNOWN,
-                                                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+                                                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS |
+                                                           G_PARAM_DEPRECATED));
 }
 
 static void
@@ -137,6 +144,8 @@ get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
  * Return value: the video format, or %GDATA_YOUTUBE_FORMAT_UNKNOWN
  *
  * Since: 0.4.0
+ * Deprecated: UNRELEASED: Accessing YouTube video content directly is no longer
+ *   supported by Google. There is no replacement.
  **/
 GDataYouTubeFormat
 gdata_youtube_content_get_format (GDataYouTubeContent *self)
@@ -144,3 +153,5 @@ gdata_youtube_content_get_format (GDataYouTubeContent *self)
        g_return_val_if_fail (GDATA_IS_YOUTUBE_CONTENT (self), GDATA_YOUTUBE_FORMAT_UNKNOWN);
        return self->priv->format;
 }
+
+G_GNUC_END_IGNORE_DEPRECATIONS
diff --git a/gdata/services/youtube/gdata-youtube-content.h b/gdata/services/youtube/gdata-youtube-content.h
index dbb7216..9fb5c7b 100644
--- a/gdata/services/youtube/gdata-youtube-content.h
+++ b/gdata/services/youtube/gdata-youtube-content.h
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009–2010 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009–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
@@ -28,6 +28,8 @@
 
 G_BEGIN_DECLS
 
+#ifndef LIBGDATA_DISABLE_DEPRECATED
+
 /**
  * GDataYouTubeFormat:
  * @GDATA_YOUTUBE_FORMAT_UNKNOWN: retrieve videos in all formats when querying the service
@@ -39,13 +41,15 @@ G_BEGIN_DECLS
  * <ulink type="http" url="http://code.google.com/apis/youtube/2.0/reference.html#formatsp";>online 
documentation</ulink>.
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: Accessing YouTube video content directly is no longer
+ *   supported by Google. There is no replacement.
  **/
 typedef enum {
        GDATA_YOUTUBE_FORMAT_UNKNOWN = 0,
        GDATA_YOUTUBE_FORMAT_RTSP_H263_AMR = 1,
        GDATA_YOUTUBE_FORMAT_HTTP_SWF = 5,
        GDATA_YOUTUBE_FORMAT_RTSP_MPEG4_AAC = 6
-} GDataYouTubeFormat;
+} GDataYouTubeFormat G_GNUC_DEPRECATED;
 
 #define GDATA_TYPE_YOUTUBE_CONTENT             (gdata_youtube_content_get_type ())
 #define GDATA_YOUTUBE_CONTENT(o)               (G_TYPE_CHECK_INSTANCE_CAST ((o), GDATA_TYPE_YOUTUBE_CONTENT, 
GDataYouTubeContent))
@@ -54,7 +58,7 @@ typedef enum {
 #define GDATA_IS_YOUTUBE_CONTENT_CLASS(k)      (G_TYPE_CHECK_CLASS_TYPE ((k), GDATA_TYPE_YOUTUBE_CONTENT))
 #define GDATA_YOUTUBE_CONTENT_GET_CLASS(o)     (G_TYPE_INSTANCE_GET_CLASS ((o), GDATA_TYPE_YOUTUBE_CONTENT, 
GDataYouTubeContentClass))
 
-typedef struct _GDataYouTubeContentPrivate     GDataYouTubeContentPrivate;
+typedef struct _GDataYouTubeContentPrivate     GDataYouTubeContentPrivate G_GNUC_DEPRECATED;
 
 /**
  * GDataYouTubeContent:
@@ -62,11 +66,15 @@ typedef struct _GDataYouTubeContentPrivate  GDataYouTubeContentPrivate;
  * All the fields in the #GDataYouTubeContent structure are private and should never be accessed directly.
  *
  * Since: 0.4.0
+ * Deprecated: UNRELEASED: Accessing YouTube video content directly is no longer
+ *   supported by Google. There is no replacement.
  **/
 typedef struct {
+       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
        GDataMediaContent parent;
        GDataYouTubeContentPrivate *priv;
-} GDataYouTubeContent;
+       G_GNUC_END_IGNORE_DEPRECATIONS
+} GDataYouTubeContent G_GNUC_DEPRECATED;
 
 /**
  * GDataYouTubeContentClass:
@@ -74,19 +82,28 @@ typedef struct {
  * All the fields in the #GDataYouTubeContentClass structure are private and should never be accessed 
directly.
  *
  * Since: 0.4.0
+ * Deprecated: UNRELEASED: Accessing YouTube video content directly is no longer
+ *   supported by Google. There is no replacement.
  **/
 typedef struct {
+       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
        /*< private >*/
        GDataMediaContentClass parent;
+       G_GNUC_END_IGNORE_DEPRECATIONS
 
        /*< private >*/
        /* Padding for future expansion */
        void (*_g_reserved0) (void);
        void (*_g_reserved1) (void);
-} GDataYouTubeContentClass;
+} GDataYouTubeContentClass G_GNUC_DEPRECATED;
+
+GType gdata_youtube_content_get_type (void) G_GNUC_CONST G_GNUC_DEPRECATED;
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+GDataYouTubeFormat gdata_youtube_content_get_format (GDataYouTubeContent *self) G_GNUC_PURE 
G_GNUC_DEPRECATED;
+G_GNUC_END_IGNORE_DEPRECATIONS
 
-GType gdata_youtube_content_get_type (void) G_GNUC_CONST;
-GDataYouTubeFormat gdata_youtube_content_get_format (GDataYouTubeContent *self) G_GNUC_PURE;
+#endif /* !LIBGDATA_DISABLE_DEPRECATED */
 
 G_END_DECLS
 
diff --git a/gdata/services/youtube/gdata-youtube-credit.c b/gdata/services/youtube/gdata-youtube-credit.c
index bfaf7ed..47a1040 100644
--- a/gdata/services/youtube/gdata-youtube-credit.c
+++ b/gdata/services/youtube/gdata-youtube-credit.c
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009–2010 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009–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
@@ -28,6 +28,8 @@
  * online documentation</ulink>.
  *
  * Since: 0.4.0
+ * Deprecated: UNRELEASED: This is no longer supported by Google. There is no
+ *   replacement.
  **/
 
 #include <glib.h>
@@ -37,6 +39,8 @@
 #include "gdata-parsable.h"
 #include "gdata-parser.h"
 
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
 static void gdata_youtube_credit_finalize (GObject *object);
 static void gdata_youtube_credit_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);
@@ -78,12 +82,14 @@ gdata_youtube_credit_class_init (GDataYouTubeCreditClass *klass)
         * YouTube documentation</ulink>.
         *
         * Since: 0.4.0
+        * Deprecated: UNRELEASED: This is no longer supported by Google. There
+        *   is no replacement.
         **/
        g_object_class_install_property (gobject_class, PROP_ENTITY_TYPE,
                                         g_param_spec_string ("entity-type",
                                                              "Entity type", "The type of entity who is 
credited.",
                                                              NULL,
-                                                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+                                                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | 
G_PARAM_DEPRECATED));
 }
 
 static void
@@ -160,6 +166,8 @@ get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
  * Return value: the type of the credited user (e.g. %GDATA_YOUTUBE_CREDIT_ENTITY_PARTNER), or %NULL
  *
  * Since: 0.4.0
+ * Deprecated: UNRELEASED: This is no longer supported by Google. There is no
+ *   replacement.
  **/
 const gchar *
 gdata_youtube_credit_get_entity_type (GDataYouTubeCredit *self)
@@ -167,3 +175,5 @@ gdata_youtube_credit_get_entity_type (GDataYouTubeCredit *self)
        g_return_val_if_fail (GDATA_IS_YOUTUBE_CREDIT (self), NULL);
        return self->priv->entity_type;
 }
+
+G_GNUC_END_IGNORE_DEPRECATIONS
diff --git a/gdata/services/youtube/gdata-youtube-credit.h b/gdata/services/youtube/gdata-youtube-credit.h
index b140ff1..4462d95 100644
--- a/gdata/services/youtube/gdata-youtube-credit.h
+++ b/gdata/services/youtube/gdata-youtube-credit.h
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009–2010 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009–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
@@ -28,12 +28,16 @@
 
 G_BEGIN_DECLS
 
+#ifndef LIBGDATA_DISABLE_DEPRECATED
+
 /**
  * GDATA_YOUTUBE_CREDIT_ENTITY_PARTNER:
  *
  * The credited entity is a YouTube partner.
  *
  * Since: 0.7.0
+ * Deprecated: UNRELEASED: This is no longer supported by Google. There is no
+ *   replacement.
  **/
 #define GDATA_YOUTUBE_CREDIT_ENTITY_PARTNER "partner"
 
@@ -44,7 +48,7 @@ G_BEGIN_DECLS
 #define GDATA_IS_YOUTUBE_CREDIT_CLASS(k)       (G_TYPE_CHECK_CLASS_TYPE ((k), GDATA_TYPE_YOUTUBE_CREDIT))
 #define GDATA_YOUTUBE_CREDIT_GET_CLASS(o)      (G_TYPE_INSTANCE_GET_CLASS ((o), GDATA_TYPE_YOUTUBE_CREDIT, 
GDataYouTubeCreditClass))
 
-typedef struct _GDataYouTubeCreditPrivate      GDataYouTubeCreditPrivate;
+typedef struct _GDataYouTubeCreditPrivate      GDataYouTubeCreditPrivate G_GNUC_DEPRECATED;
 
 /**
  * GDataYouTubeCredit:
@@ -52,11 +56,15 @@ typedef struct _GDataYouTubeCreditPrivate   GDataYouTubeCreditPrivate;
  * All the fields in the #GDataYouTubeCredit structure are private and should never be accessed directly.
  *
  * Since: 0.4.0
+ * Deprecated: UNRELEASED: This is no longer supported by Google. There is no
+ *   replacement.
  **/
 typedef struct {
+       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
        GDataMediaCredit parent;
        GDataYouTubeCreditPrivate *priv;
-} GDataYouTubeCredit;
+       G_GNUC_END_IGNORE_DEPRECATIONS
+} GDataYouTubeCredit G_GNUC_DEPRECATED;
 
 /**
  * GDataYouTubeCreditClass:
@@ -64,19 +72,27 @@ typedef struct {
  * All the fields in the #GDataYouTubeCreditClass structure are private and should never be accessed 
directly.
  *
  * Since: 0.4.0
+ * Deprecated: UNRELEASED: This is no longer supported by Google. There is no
+ *   replacement.
  **/
 typedef struct {
+       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
        /*< private >*/
        GDataMediaCreditClass parent;
+       G_GNUC_END_IGNORE_DEPRECATIONS
 
        /*< private >*/
        /* Padding for future expansion */
        void (*_g_reserved0) (void);
        void (*_g_reserved1) (void);
-} GDataYouTubeCreditClass;
+} GDataYouTubeCreditClass G_GNUC_DEPRECATED;
+
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+GType gdata_youtube_credit_get_type (void) G_GNUC_CONST G_GNUC_DEPRECATED;
+const gchar *gdata_youtube_credit_get_entity_type (GDataYouTubeCredit *self) G_GNUC_PURE G_GNUC_DEPRECATED;
+G_GNUC_END_IGNORE_DEPRECATIONS
 
-GType gdata_youtube_credit_get_type (void) G_GNUC_CONST;
-const gchar *gdata_youtube_credit_get_entity_type (GDataYouTubeCredit *self) G_GNUC_PURE;
+#endif /* !LIBGDATA_DISABLE_DEPRECATED */
 
 G_END_DECLS
 
diff --git a/gdata/services/youtube/gdata-youtube-query.c b/gdata/services/youtube/gdata-youtube-query.c
index fefc8f3..f55ff86 100644
--- a/gdata/services/youtube/gdata-youtube-query.c
+++ b/gdata/services/youtube/gdata-youtube-query.c
@@ -26,8 +26,13 @@
  * #GDataYouTubeQuery represents a collection of query parameters specific to the YouTube service, which go 
above and beyond
  * those catered for by #GDataQuery.
  *
+ * With the transition to version 3 of the YouTube, the #GDataQuery:author and
+ * #GDataQuery:start-index properties are no longer supported, and their values
+ * will be ignored. Use gdata_query_next_page() instead of the
+ * #GDataQuery:start-index API.
+ *
  * For more information on the custom GData query parameters supported by #GDataYouTubeQuery, see the <ulink 
type="http"
- * url="http://code.google.com/apis/youtube/2.0/reference.html#Custom_parameters";>online 
documentation</ulink>.
+ * url="https://developers.google.com/youtube/v3/docs/search/list#parameters";>online documentation</ulink>.
  *
  * Since: 0.3.0
  **/
@@ -47,7 +52,12 @@ static void gdata_youtube_query_set_property (GObject *object, guint property_id
 static void get_query_uri (GDataQuery *self, const gchar *feed_uri, GString *query_uri, gboolean 
*params_started);
 
 struct _GDataYouTubeQueryPrivate {
+       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
        GDataYouTubeFormat format;
+       GDataYouTubeSortOrder sort_order;
+       GDataYouTubeUploader uploader;
+       G_GNUC_END_IGNORE_DEPRECATIONS
+
        gdouble latitude;
        gdouble longitude;
        gdouble location_radius;
@@ -56,9 +66,7 @@ struct _GDataYouTubeQueryPrivate {
        gchar *order_by;
        gchar *restriction;
        GDataYouTubeSafeSearch safe_search;
-       GDataYouTubeSortOrder sort_order;
        GDataYouTubeAge age;
-       GDataYouTubeUploader uploader;
        gchar *license;
 };
 
@@ -101,12 +109,15 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
         * retrieve videos irrespective of their format availability.
         *
         * Since: 0.3.0
+        * Deprecated: UNRELEASED: No longer supported by Google. The value of
+        *   this property will be unused in queries. There is no replacement.
         **/
        g_object_class_install_property (gobject_class, PROP_FORMAT,
                                         g_param_spec_enum ("format",
                                                            "Format", "Specifies that videos must be 
available in a particular video format.",
                                                            GDATA_TYPE_YOUTUBE_FORMAT, 
GDATA_YOUTUBE_FORMAT_UNKNOWN,
-                                                           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                                                           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |
+                                                           G_PARAM_DEPRECATED));
 
        /**
         * GDataYouTubeQuery:latitude:
@@ -119,12 +130,11 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
         * If #GDataYouTubeQuery:location-radius is a non-<code class="literal">0</code> value, this will 
define a circle from which videos should be
         * found.
         *
-        * If #GDataYouTubeQuery:has-location is %TRUE, only videos which are associated with specific 
coordinates in the search
-        * circle will be returned. Otherwise, videos which have a relevant descriptive address (but no 
specific coordinates) will
-        * also be returned.
+        * As it is deprecated, the value of #GDataYouTubeQuery:has-location is
+        * ignored.
         *
         * For more information, see the <ulink type="http"
-        * url="http://code.google.com/apis/youtube/2.0/reference.html#locationsp";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/search/list#location";>online 
documentation</ulink>.
         *
         * Since: 0.3.0
         **/
@@ -181,12 +191,15 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
         * For more information, see the documentation for #GDataYouTubeQuery:latitude.
         *
         * Since: 0.3.0
+        * Deprecated: UNRELEASED: No longer supported by Google. The value of
+        *   this property will be unused in queries.
         **/
        g_object_class_install_property (gobject_class, PROP_HAS_LOCATION,
                                         g_param_spec_boolean ("has-location",
                                                               "Has location?", "Whether to restrict results 
to videos with specific coordinates.",
                                                               FALSE,
-                                                              G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                                                              G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |
+                                                              G_PARAM_DEPRECATED));
 
        /**
         * GDataYouTubeQuery:language:
@@ -199,12 +212,15 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
         * url="http://code.google.com/apis/youtube/2.0/reference.html#lrsp";>online documentation</ulink>.
         *
         * Since: 0.3.0
+        * Deprecated: UNRELEASED: No longer supported by Google. The value of
+        *   this property will be unused in queries. There is no replacement.
         **/
        g_object_class_install_property (gobject_class, PROP_LANGUAGE,
                                         g_param_spec_string ("language",
                                                              "Language", "Restricts the search to videos 
described in the given language.",
                                                              NULL,
-                                                             G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                                                             G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |
+                                                             G_PARAM_DEPRECATED));
 
        /**
         * GDataYouTubeQuery:order-by:
@@ -217,7 +233,7 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
         * <replaceable>languageCode</replaceable> is an ISO 639-1 language code, as used in 
#GDataYouTubeQuery:language.
         *
         * For more information, see the <ulink type="http"
-        * url="http://code.google.com/apis/youtube/2.0/reference.html#orderbysp";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/search/list#order";>online documentation</ulink>.
         *
         * Since: 0.3.0
         **/
@@ -230,18 +246,22 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
        /**
         * GDataYouTubeQuery:restriction:
         *
-        * The IP address that should be used to filter videos playable only in specific countries. This 
should be the
-        * IP address of the client, if different to the IP address in use by the library. Alternatively, it 
can be an ISO 3166
-        * two-letter country code.
+        * An ISO 3166 two-letter country code that should be used to filter
+        * videos playable only in specific countries.
+        *
+        * Previously, this property could also accept the client’s IP address
+        * for country lookup. This feature is no longer supported by Google,
+        * and will result in an error from the server if used. Use a country
+        * code instead.
         *
         * For more information, see the <ulink type="http"
-        * url="http://code.google.com/apis/youtube/2.0/reference.html#restrictionsp";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/search/list#regionCode";>online 
documentation</ulink>.
         *
         * Since: 0.3.0
         **/
        g_object_class_install_property (gobject_class, PROP_RESTRICTION,
                                         g_param_spec_string ("restriction",
-                                                             "Restriction", "The IP address to filter videos 
playable only in specific countries.",
+                                                             "Restriction", "The country code to filter 
videos playable only in specific countries.",
                                                              NULL,
                                                              G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 
@@ -251,7 +271,7 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
         * Whether the search results should include restricted content as well as standard content.
         *
         * For more information, see the <ulink type="http"
-        * url="http://code.google.com/apis/youtube/2.0/reference.html#safeSearchsp";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/search/list#safeSearch";>online 
documentation</ulink>.
         *
         * Since: 0.3.0
         **/
@@ -267,12 +287,15 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
         * Specifies the direction of sorting. To use the default sort order, set the property to 
%GDATA_YOUTUBE_SORT_NONE.
         *
         * Since: 0.3.0
+        * Deprecated: UNRELEASED: No longer supported by Google. The value of
+        *   this property will be unused in queries. There is no replacement.
         **/
        g_object_class_install_property (gobject_class, PROP_SORT_ORDER,
                                         g_param_spec_enum ("sort-order",
                                                            "Sort order", "Specifies the direction of 
sorting.",
                                                            GDATA_TYPE_YOUTUBE_SORT_ORDER, 
GDATA_YOUTUBE_SORT_NONE,
-                                                           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                                                           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |
+                                                           G_PARAM_DEPRECATED));
 
        /**
         * GDataYouTubeQuery:age:
@@ -295,12 +318,15 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
         * searches to videos from YouTube partners.
         *
         * Since: 0.3.0
+        * Deprecated: UNRELEASED: No longer supported by Google. The value of
+        *   this property will be unused in queries. There is no replacement.
         **/
        g_object_class_install_property (gobject_class, PROP_UPLOADER,
                                         g_param_spec_enum ("uploader",
                                                            "Uploader", "Restricts the search to videos from 
the specified type of uploader.",
                                                            GDATA_TYPE_YOUTUBE_UPLOADER, 
GDATA_YOUTUBE_UPLOADER_ALL,
-                                                           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                                                           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |
+                                                           G_PARAM_DEPRECATED));
 
        /**
         * GDataYouTubeQuery:license:
@@ -309,7 +335,7 @@ gdata_youtube_query_class_init (GDataYouTubeQueryClass *klass)
         * are Creative Commons licensed will be returned in search results. Set this to %NULL to return 
videos under any license.
         *
         * For more information, see the <ulink type="http"
-        * url="http://code.google.com/apis/youtube/2.0/reference.html#licensesp";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/search/list#videoLicense";>online 
documentation</ulink>.
         *
         * Since: 0.11.0
         */
@@ -403,7 +429,9 @@ gdata_youtube_query_set_property (GObject *object, guint property_id, const GVal
 
        switch (property_id) {
                case PROP_FORMAT:
+                       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
                        gdata_youtube_query_set_format (self, g_value_get_enum (value));
+                       G_GNUC_END_IGNORE_DEPRECATIONS
                        break;
                case PROP_LATITUDE:
                        self->priv->latitude = g_value_get_double (value);
@@ -418,7 +446,9 @@ gdata_youtube_query_set_property (GObject *object, guint property_id, const GVal
                        self->priv->has_location = g_value_get_boolean (value);
                        break;
                case PROP_LANGUAGE:
+                       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
                        gdata_youtube_query_set_language (self, g_value_get_string (value));
+                       G_GNUC_END_IGNORE_DEPRECATIONS
                        break;
                case PROP_ORDER_BY:
                        gdata_youtube_query_set_order_by (self, g_value_get_string (value));
@@ -430,13 +460,17 @@ gdata_youtube_query_set_property (GObject *object, guint property_id, const GVal
                        gdata_youtube_query_set_safe_search (self, g_value_get_enum (value));
                        break;
                case PROP_SORT_ORDER:
+                       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
                        gdata_youtube_query_set_sort_order (self, g_value_get_enum (value));
+                       G_GNUC_END_IGNORE_DEPRECATIONS
                        break;
                case PROP_AGE:
                        gdata_youtube_query_set_age (self, g_value_get_enum (value));
                        break;
                case PROP_UPLOADER:
+                       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
                        gdata_youtube_query_set_uploader (self, g_value_get_enum (value));
+                       G_GNUC_END_IGNORE_DEPRECATIONS
                        break;
                case PROP_LICENSE:
                        gdata_youtube_query_set_license (self, g_value_get_string (value));
@@ -448,6 +482,56 @@ gdata_youtube_query_set_property (GObject *object, guint property_id, const GVal
        }
 }
 
+/* Convert from a v2 order-by parameter value to a v3 order parameter value.
+ * Reference:
+ * v2: https://developers.google.com/youtube/2.0/developers_guide_protocol_api_query_parameters#orderbysp
+ * v3: https://developers.google.com/youtube/v3/docs/search/list#order
+ */
+static const gchar *
+get_v3_order (const gchar *v2_order_by)
+{
+       const struct {
+               const gchar *v2_order_by;
+               const gchar *v3_order;
+       } mapping[] = {
+               { "relevance", "relevance" },
+               { "published", "date" },
+               { "viewCount", "viewCount" },
+               { "rating", "rating" },
+       };
+       guint i;
+
+       for (i = 0; i < G_N_ELEMENTS (mapping); i++) {
+               if (g_strcmp0 (v2_order_by, mapping[i].v2_order_by) == 0) {
+                       return mapping[i].v3_order;
+               }
+       }
+
+       /* Special case for ‘relevance_lang_*’. */
+       if (g_str_has_prefix (v2_order_by, "relevance_lang_")) {
+               return "relevance";
+       }
+
+       return NULL;
+}
+
+/* Convert from a v2 license parameter value to a v3 videoLicense parameter
+ * value. Reference:
+ * v2: https://developers.google.com/youtube/2.0/developers_guide_protocol_api_query_parameters#licensesp
+ * v3: https://developers.google.com/youtube/v3/docs/search/list#videoLicense
+ */
+static const gchar *
+get_v3_video_license (const gchar *v2_license)
+{
+       if (g_strcmp0 (v2_license, "cc") == 0) {
+               return "creativeCommon";
+       } else if (g_strcmp0 (v2_license, "youtube") == 0) {
+               return "youtube";
+       } else {
+               return NULL;
+       }
+}
+
 static void
 get_query_uri (GDataQuery *self, const gchar *feed_uri, GString *query_uri, gboolean *params_started)
 {
@@ -455,91 +539,127 @@ get_query_uri (GDataQuery *self, const gchar *feed_uri, GString *query_uri, gboo
 
        #define APPEND_SEP g_string_append_c (query_uri, (*params_started == FALSE) ? '?' : '&'); 
*params_started = TRUE;
 
-       /* Chain up to the parent class */
-       GDATA_QUERY_CLASS (gdata_youtube_query_parent_class)->get_query_uri (self, feed_uri, query_uri, 
params_started);
+       /* NOTE: We do not chain up because the parent class implements a lot
+        * of deprecated API. */
+
+       /* Categories */
+       if (gdata_query_get_categories (self) != NULL) {
+               APPEND_SEP
+               g_string_append (query_uri, "videoCategoryId=");
+               g_string_append_uri_escaped (query_uri,
+                                            gdata_query_get_categories (self),
+                                            NULL,
+                                            FALSE);
+       }
 
-       APPEND_SEP
-       switch (priv->age) {
+       /* q param */
+       if (gdata_query_get_q (self) != NULL) {
+               APPEND_SEP
+               g_string_append (query_uri, "q=");
+               g_string_append_uri_escaped (query_uri,
+                                            gdata_query_get_q (self), NULL,
+                                            FALSE);
+       }
+
+       if (gdata_query_get_max_results (self) > 0) {
+               APPEND_SEP
+               g_string_append_printf (query_uri, "maxResults=%u",
+                                       gdata_query_get_max_results (self));
+       }
+
+       if (priv->age != GDATA_YOUTUBE_AGE_ALL_TIME) {
+               gchar *after;
+               GTimeVal tv = { 0, };
+
+               g_get_current_time (&tv);
+
+               switch (priv->age) {
                case GDATA_YOUTUBE_AGE_TODAY:
-                       g_string_append (query_uri, "time=today");
+                       tv.tv_sec -= 24 * 60 * 60;
                        break;
                case GDATA_YOUTUBE_AGE_THIS_WEEK:
-                       g_string_append (query_uri, "time=this_week");
+                       tv.tv_sec -= 7 * 24 * 60 * 60;
                        break;
                case GDATA_YOUTUBE_AGE_THIS_MONTH:
-                       g_string_append (query_uri, "time=this_month");
+                       tv.tv_sec -= 31 * 24 * 60 * 60;
                        break;
                case GDATA_YOUTUBE_AGE_ALL_TIME:
-                       g_string_append (query_uri, "time=all_time");
-                       break;
                default:
                        g_assert_not_reached ();
+               }
+
+               APPEND_SEP
+
+               after = g_time_val_to_iso8601 (&tv);
+               g_string_append_printf (query_uri, "publishedAfter=%s", after);
+               g_free (after);
        }
 
-       /* We don't need to use APPEND_SEP below here, as the "age" parameter is always included */
+       /* We don’t need to use APPEND_SEP below here, as this parameter is
+        * always included */
+       APPEND_SEP
        switch (priv->safe_search) {
                case GDATA_YOUTUBE_SAFE_SEARCH_NONE:
-                       g_string_append (query_uri, "&safeSearch=none");
+                       g_string_append (query_uri, "safeSearch=none");
                        break;
                case GDATA_YOUTUBE_SAFE_SEARCH_MODERATE:
-                       g_string_append (query_uri, "&safeSearch=moderate");
+                       g_string_append (query_uri, "safeSearch=moderate");
                        break;
                case GDATA_YOUTUBE_SAFE_SEARCH_STRICT:
-                       g_string_append (query_uri, "&safeSearch=strict");
+                       g_string_append (query_uri, "safeSearch=strict");
                        break;
                default:
                        g_assert_not_reached ();
        }
 
-       if (priv->format != GDATA_YOUTUBE_FORMAT_UNKNOWN)
-               g_string_append_printf (query_uri, "&format=%u", priv->format);
-
        if (priv->latitude >= -90.0 && priv->latitude <= 90.0 &&
            priv->longitude >= -180.0 && priv->longitude <= 180.0) {
-               gchar latitude[G_ASCII_DTOSTR_BUF_SIZE], longitude[G_ASCII_DTOSTR_BUF_SIZE];
+               gchar latitude[G_ASCII_DTOSTR_BUF_SIZE];
+               gchar longitude[G_ASCII_DTOSTR_BUF_SIZE];
 
-               g_string_append_printf (query_uri, (priv->has_location == TRUE) ? "&location=%s,%s!" : 
"&location=%s,%s",
-                                       g_ascii_dtostr (latitude, sizeof (latitude), priv->latitude),
-                                       g_ascii_dtostr (longitude, sizeof (longitude), priv->longitude));
+               g_string_append_printf (query_uri, "&location=%s,%s",
+                                       g_ascii_dtostr (latitude,
+                                                       sizeof (latitude),
+                                                       priv->latitude),
+                                       g_ascii_dtostr (longitude,
+                                                       sizeof (longitude),
+                                                       priv->longitude));
 
                if (priv->location_radius >= 0.0) {
                        gchar radius[G_ASCII_DTOSTR_BUF_SIZE];
-                       g_string_append_printf (query_uri, "&location-radius=%sm", g_ascii_dtostr (radius, 
sizeof (radius), priv->location_radius));
+                       g_string_append_printf (query_uri, "&locationRadius=%sm",
+                                               g_ascii_dtostr (radius,
+                                                               sizeof (radius),
+                                                               priv->location_radius));
                }
-       } else if (priv->has_location == TRUE) {
-               g_string_append (query_uri, "&location=!");
-       }
-
-       if (priv->language != NULL) {
-               g_string_append (query_uri, "&lr=");
-               g_string_append_uri_escaped (query_uri, priv->language, NULL, FALSE);
        }
 
        if (priv->order_by != NULL) {
-               g_string_append (query_uri, "&orderby=");
-               g_string_append_uri_escaped (query_uri, priv->order_by, NULL, FALSE);
+               const gchar *v3_order_by = get_v3_order (priv->order_by);
+
+               if (v3_order_by != NULL) {
+                       g_string_append (query_uri, "&order=");
+                       g_string_append_uri_escaped (query_uri, v3_order_by,
+                                                    NULL, FALSE);
+               }
        }
 
        if (priv->restriction != NULL) {
-               g_string_append (query_uri, "&restriction=");
+               g_string_append (query_uri, "&regionCode=");
                g_string_append_uri_escaped (query_uri, priv->restriction, NULL, FALSE);
        }
 
-       if (priv->sort_order != GDATA_YOUTUBE_SORT_NONE) {
-               if (priv->sort_order == GDATA_YOUTUBE_SORT_ASCENDING)
-                       g_string_append (query_uri, "&sortorder=ascending");
-               else if (priv->sort_order == GDATA_YOUTUBE_SORT_DESCENDING)
-                       g_string_append (query_uri, "&sortorder=descending");
-               else
-                       g_assert_not_reached ();
-       }
+       if (priv->license != NULL) {
+               const gchar *v3_video_license;
 
-       if (priv->uploader != GDATA_YOUTUBE_UPLOADER_ALL)
-               g_string_append (query_uri, "&uploader=partner");
+               v3_video_license = get_v3_video_license (priv->license);
 
-       if (priv->license != NULL) {
-               g_string_append (query_uri, "&license=");
-               g_string_append_uri_escaped (query_uri, priv->license, NULL, FALSE);
+               if (v3_video_license != NULL) {
+                       g_string_append (query_uri, "&videoLicense=");
+                       g_string_append_uri_escaped (query_uri,
+                                                    v3_video_license, NULL,
+                                                    FALSE);
+               }
        }
 }
 
@@ -559,6 +679,8 @@ gdata_youtube_query_new (const gchar *q)
        return g_object_new (GDATA_TYPE_YOUTUBE_QUERY, "q", q, NULL);
 }
 
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
 /**
  * gdata_youtube_query_get_format:
  * @self: a #GDataYouTubeQuery
@@ -568,6 +690,8 @@ gdata_youtube_query_new (const gchar *q)
  * Return value: the format property
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ *   this property will be unused in queries. There is no replacement.
  **/
 GDataYouTubeFormat
 gdata_youtube_query_get_format (GDataYouTubeQuery *self)
@@ -584,6 +708,8 @@ gdata_youtube_query_get_format (GDataYouTubeQuery *self)
  * Sets the #GDataYouTubeQuery:format property of the #GDataYouTubeQuery to @format.
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ *   this property will be unused in queries. There is no replacement.
  **/
 void
 gdata_youtube_query_set_format (GDataYouTubeQuery *self, GDataYouTubeFormat format)
@@ -596,6 +722,8 @@ gdata_youtube_query_set_format (GDataYouTubeQuery *self, GDataYouTubeFormat form
        gdata_query_set_etag (GDATA_QUERY (self), NULL);
 }
 
+G_GNUC_END_IGNORE_DEPRECATIONS
+
 /**
  * gdata_youtube_query_get_location:
  * @self: a #GDataYouTubeQuery
@@ -668,6 +796,8 @@ gdata_youtube_query_set_location (GDataYouTubeQuery *self, gdouble latitude, gdo
  * Return value: the language property, or %NULL if it is unset
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ *   this property will be unused in queries. There is no replacement.
  **/
 const gchar *
 gdata_youtube_query_get_language (GDataYouTubeQuery *self)
@@ -686,6 +816,8 @@ gdata_youtube_query_get_language (GDataYouTubeQuery *self)
  * Set @language to %NULL to unset the property in the query URI.
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ *   this property will be unused in queries. There is no replacement.
  **/
 void
 gdata_youtube_query_set_language (GDataYouTubeQuery *self, const gchar *language)
@@ -817,6 +949,8 @@ gdata_youtube_query_set_safe_search (GDataYouTubeQuery *self, GDataYouTubeSafeSe
        gdata_query_set_etag (GDATA_QUERY (self), NULL);
 }
 
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
 /**
  * gdata_youtube_query_get_sort_order:
  * @self: a #GDataYouTubeQuery
@@ -826,6 +960,8 @@ gdata_youtube_query_set_safe_search (GDataYouTubeQuery *self, GDataYouTubeSafeSe
  * Return value: the sort order property
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ *   this property will be unused in queries. There is no replacement.
  **/
 GDataYouTubeSortOrder
 gdata_youtube_query_get_sort_order (GDataYouTubeQuery *self)
@@ -844,6 +980,8 @@ gdata_youtube_query_get_sort_order (GDataYouTubeQuery *self)
  * Set @sort_order to %GDATA_YOUTUBE_SORT_NONE to unset the property in the query URI.
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ *   this property will be unused in queries. There is no replacement.
  **/
 void
 gdata_youtube_query_set_sort_order (GDataYouTubeQuery *self, GDataYouTubeSortOrder sort_order)
@@ -856,6 +994,8 @@ gdata_youtube_query_set_sort_order (GDataYouTubeQuery *self, GDataYouTubeSortOrd
        gdata_query_set_etag (GDATA_QUERY (self), NULL);
 }
 
+G_GNUC_END_IGNORE_DEPRECATIONS
+
 /**
  * gdata_youtube_query_get_age:
  * @self: a #GDataYouTubeQuery
@@ -893,6 +1033,8 @@ gdata_youtube_query_set_age (GDataYouTubeQuery *self, GDataYouTubeAge age)
        gdata_query_set_etag (GDATA_QUERY (self), NULL);
 }
 
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
 /**
  * gdata_youtube_query_get_uploader:
  * @self: a #GDataYouTubeQuery
@@ -902,6 +1044,8 @@ gdata_youtube_query_set_age (GDataYouTubeQuery *self, GDataYouTubeAge age)
  * Return value: the uploader property
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ *   this property will be unused in queries. There is no replacement.
  **/
 GDataYouTubeUploader
 gdata_youtube_query_get_uploader (GDataYouTubeQuery *self)
@@ -918,6 +1062,8 @@ gdata_youtube_query_get_uploader (GDataYouTubeQuery *self)
  * Sets the #GDataYouTubeQuery:uploader property of the #GDataYouTubeQuery to @uploader.
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. The value of
+ *   this property will be unused in queries. There is no replacement.
  **/
 void
 gdata_youtube_query_set_uploader (GDataYouTubeQuery *self, GDataYouTubeUploader uploader)
@@ -930,6 +1076,8 @@ gdata_youtube_query_set_uploader (GDataYouTubeQuery *self, GDataYouTubeUploader
        gdata_query_set_etag (GDATA_QUERY (self), NULL);
 }
 
+G_GNUC_END_IGNORE_DEPRECATIONS
+
 /**
  * gdata_youtube_query_get_license:
  * @self: a #GDataYouTubeQuery
diff --git a/gdata/services/youtube/gdata-youtube-query.h b/gdata/services/youtube/gdata-youtube-query.h
index c262027..ae27300 100644
--- a/gdata/services/youtube/gdata-youtube-query.h
+++ b/gdata/services/youtube/gdata-youtube-query.h
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009, 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
@@ -38,7 +38,7 @@ G_BEGIN_DECLS
  * @GDATA_YOUTUBE_SAFE_SEARCH_STRICT: YouTube will try to exclude all restricted content from the search 
result set
  *
  * Safe search levels for removing restricted entries from query results. For more information, see the
- * <ulink type="http" url="http://code.google.com/apis/youtube/2.0/reference.html#safeSearchsp";>online 
documentation</ulink>.
+ * <ulink type="http" url="https://developers.google.com/youtube/v3/docs/search/list#safeSearch";>online 
documentation</ulink>.
  *
  * Since: 0.3.0
  **/
@@ -48,6 +48,7 @@ typedef enum {
        GDATA_YOUTUBE_SAFE_SEARCH_STRICT
 } GDataYouTubeSafeSearch;
 
+#ifndef LIBGDATA_DISABLE_DEPRECATED
 /**
  * GDataYouTubeSortOrder:
  * @GDATA_YOUTUBE_SORT_NONE: do not explicitly sort in any sense
@@ -57,12 +58,15 @@ typedef enum {
  * Sort orders for the search results from queries. They specify the order of the designated order field.
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. There is no
+ *   replacement.
  **/
 typedef enum {
        GDATA_YOUTUBE_SORT_NONE = 0,
        GDATA_YOUTUBE_SORT_ASCENDING,
        GDATA_YOUTUBE_SORT_DESCENDING
-} GDataYouTubeSortOrder;
+} GDataYouTubeSortOrder G_GNUC_DEPRECATED;
+#endif /* !LIBGDATA_DISABLE_DEPRECATED */
 
 /**
  * GDataYouTubeAge:
@@ -82,6 +86,7 @@ typedef enum {
        GDATA_YOUTUBE_AGE_THIS_MONTH
 } GDataYouTubeAge;
 
+#ifndef LIBGDATA_DISABLE_DEPRECATED
 /**
  * GDataYouTubeUploader:
  * @GDATA_YOUTUBE_UPLOADER_ALL: retrieve all videos, regardless of who uploaded them
@@ -90,11 +95,14 @@ typedef enum {
  * Video uploaders, allowing queries to be limited to returning videos uploaded by YouTube partners.
  *
  * Since: 0.3.0
+ * Deprecated: UNRELEASED: No longer supported by Google. There is no
+ *   replacement.
  **/
 typedef enum {
        GDATA_YOUTUBE_UPLOADER_ALL = 0,
        GDATA_YOUTUBE_UPLOADER_PARTNER
-} GDataYouTubeUploader;
+} GDataYouTubeUploader G_GNUC_DEPRECATED;
+#endif /* !LIBGDATA_DISABLE_DEPRECATED */
 
 /**
  * GDATA_YOUTUBE_LICENSE_CC:
@@ -158,27 +166,32 @@ GType gdata_youtube_query_get_type (void) G_GNUC_CONST;
 
 GDataYouTubeQuery *gdata_youtube_query_new (const gchar *q) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
 
-GDataYouTubeFormat gdata_youtube_query_get_format (GDataYouTubeQuery *self) G_GNUC_PURE;
-void gdata_youtube_query_set_format (GDataYouTubeQuery *self, GDataYouTubeFormat format);
 void gdata_youtube_query_get_location (GDataYouTubeQuery *self, gdouble *latitude, gdouble *longitude, 
gdouble *radius, gboolean *has_location);
 void gdata_youtube_query_set_location (GDataYouTubeQuery *self, gdouble latitude, gdouble longitude, gdouble 
radius, gboolean has_location);
-const gchar *gdata_youtube_query_get_language (GDataYouTubeQuery *self) G_GNUC_PURE;
-void gdata_youtube_query_set_language (GDataYouTubeQuery *self, const gchar *language);
 const gchar *gdata_youtube_query_get_order_by (GDataYouTubeQuery *self) G_GNUC_PURE;
 void gdata_youtube_query_set_order_by (GDataYouTubeQuery *self, const gchar *order_by);
 const gchar *gdata_youtube_query_get_restriction (GDataYouTubeQuery *self) G_GNUC_PURE;
 void gdata_youtube_query_set_restriction (GDataYouTubeQuery *self, const gchar *restriction);
 GDataYouTubeSafeSearch gdata_youtube_query_get_safe_search (GDataYouTubeQuery *self) G_GNUC_PURE;
 void gdata_youtube_query_set_safe_search (GDataYouTubeQuery *self, GDataYouTubeSafeSearch safe_search);
-GDataYouTubeSortOrder gdata_youtube_query_get_sort_order (GDataYouTubeQuery *self) G_GNUC_PURE;
-void gdata_youtube_query_set_sort_order (GDataYouTubeQuery *self, GDataYouTubeSortOrder sort_order);
 GDataYouTubeAge gdata_youtube_query_get_age (GDataYouTubeQuery *self) G_GNUC_PURE;
 void gdata_youtube_query_set_age (GDataYouTubeQuery *self, GDataYouTubeAge age);
-GDataYouTubeUploader gdata_youtube_query_get_uploader (GDataYouTubeQuery *self) G_GNUC_PURE;
-void gdata_youtube_query_set_uploader (GDataYouTubeQuery *self, GDataYouTubeUploader uploader);
 const gchar *gdata_youtube_query_get_license (GDataYouTubeQuery *self) G_GNUC_PURE;
 void gdata_youtube_query_set_license (GDataYouTubeQuery *self, const gchar *license);
 
+#ifndef LIBGDATA_DISABLE_DEPRECATED
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+GDataYouTubeFormat gdata_youtube_query_get_format (GDataYouTubeQuery *self) G_GNUC_PURE G_GNUC_DEPRECATED;
+void gdata_youtube_query_set_format (GDataYouTubeQuery *self, GDataYouTubeFormat format) G_GNUC_DEPRECATED;
+const gchar *gdata_youtube_query_get_language (GDataYouTubeQuery *self) G_GNUC_PURE G_GNUC_DEPRECATED;
+void gdata_youtube_query_set_language (GDataYouTubeQuery *self, const gchar *language) G_GNUC_DEPRECATED;
+GDataYouTubeSortOrder gdata_youtube_query_get_sort_order (GDataYouTubeQuery *self) G_GNUC_PURE 
G_GNUC_DEPRECATED;
+void gdata_youtube_query_set_sort_order (GDataYouTubeQuery *self, GDataYouTubeSortOrder sort_order) 
G_GNUC_DEPRECATED;
+GDataYouTubeUploader gdata_youtube_query_get_uploader (GDataYouTubeQuery *self) G_GNUC_PURE 
G_GNUC_DEPRECATED;
+void gdata_youtube_query_set_uploader (GDataYouTubeQuery *self, GDataYouTubeUploader uploader) 
G_GNUC_DEPRECATED;
+G_GNUC_END_IGNORE_DEPRECATIONS
+#endif /* !LIBGDATA_DISABLE_DEPRECATED */
+
 G_END_DECLS
 
 #endif /* !GDATA_YOUTUBE_QUERY_H */
diff --git a/gdata/services/youtube/gdata-youtube-service.c b/gdata/services/youtube/gdata-youtube-service.c
index 7e54faf..0a3a957 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
@@ -24,9 +24,16 @@
  * @include: gdata/services/youtube/gdata-youtube-service.h
  *
  * #GDataYouTubeService is a subclass of #GDataService for communicating with the GData API of YouTube. It 
supports querying for and
- * uploading videos.
+ * uploading videos using version 3 of the API.
  *
- * For more details of YouTube's GData API, see the <ulink type="http" 
url="http://code.google.com/apis/youtube/2.0/reference.html";>
+ * The YouTube API supports returning different sets of properties for
+ * #GDataYouTubeVideos depending on the specific query. For search results, only
+ * ‘snippet’ properties are returned (including #GDataEntry:title,
+ * #GDataEntry:summary and the set of thumbnails). For querying single videos,
+ * a more complete set of properties are returned — so use
+ * gdata_service_query_single_entry_async() to get further details on a video.
+ *
+ * For more details of YouTube's GData API, see the <ulink type="http" 
url="https://developers.google.com/youtube/v3/docs/";>
  * online documentation</ulink>.
  *
  * <example>
@@ -253,7 +260,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: https://developers.google.com/youtube/v3/docs/ */
 
 GQuark
 gdata_youtube_service_error_quark (void)
@@ -278,7 +285,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
@@ -302,7 +311,11 @@ 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>.
+        *
+        * With the port from v2 to v3 of the YouTube API in libgdata
+        * UNRELEASED, it might be necessary to update your application’s
+        * developer key.
         **/
        g_object_class_install_property (gobject_class, PROP_DEVELOPER_KEY,
                                         g_param_spec_string ("developer-key",
@@ -361,161 +374,220 @@ gdata_youtube_service_set_property (GObject *object, guint property_id, const GV
 }
 
 static void
-append_query_headers (GDataService *self, GDataAuthorizationDomain *domain, SoupMessage *message)
+append_query_headers (GDataService *self, GDataAuthorizationDomain *domain,
+                      SoupMessage *message)
 {
        GDataYouTubeServicePrivate *priv = GDATA_YOUTUBE_SERVICE (self)->priv;
-       gchar *key_header;
 
        g_assert (message != NULL);
 
-       /* Dev key and client headers */
-       key_header = g_strdup_printf ("key=%s", priv->developer_key);
-       soup_message_headers_append (message->request_headers, "X-GData-Key", key_header);
-       g_free (key_header);
+       if (priv->developer_key != NULL &&
+           !gdata_authorizer_is_authorized_for_domain (gdata_service_get_authorizer (GDATA_SERVICE (self)),
+                                                       get_youtube_authorization_domain ())) {
+               const gchar *query;
+               SoupURI *uri;
+
+               uri = soup_message_get_uri (message);
+               query = soup_uri_get_query (uri);
+
+               /* Set the key on every unauthorised request:
+                * https://developers.google.com/youtube/v3/docs/standard_parameters#key */
+               if (query != NULL) {
+                       GString *new_query;
+
+                       new_query = g_string_new (query);
+
+                       g_string_append (new_query, "&key=");
+                       g_string_append_uri_escaped (new_query,
+                                                    priv->developer_key, NULL,
+                                                    FALSE);
+
+                       soup_uri_set_query (uri, new_query->str);
+                       g_string_free (new_query, TRUE);
+               }
+       }
 
        /* Chain up to the parent class */
        GDATA_SERVICE_CLASS (gdata_youtube_service_parent_class)->append_query_headers (self, domain, 
message);
 }
 
+/* Reference: https://developers.google.com/youtube/v3/docs/errors
+ *
+ * Example response:
+ *     {
+ *      "error": {
+ *       "errors": [
+ *        {
+ *         "domain": "youtube.parameter",
+ *         "reason": "missingRequiredParameter",
+ *         "message": "No filter selected.",
+ *         "locationType": "parameter",
+ *         "location": ""
+ *        }
+ *       ],
+ *       "code": 400,
+ *       "message": "No filter selected."
+ *      }
+ *     }
+ */
+/* FIXME: Factor this out into a common JSON error parser helper which simply
+ * takes a map of expected error codes. */
 static void
-parse_error_response (GDataService *self, GDataOperationType operation_type, guint status, const gchar 
*reason_phrase, const gchar *response_body,
-                      gint length, GError **error)
+parse_error_response (GDataService *self, GDataOperationType operation_type,
+                      guint status, const gchar *reason_phrase,
+                      const gchar *response_body, gint length, GError **error)
 {
-       xmlDoc *doc;
-       xmlNode *node;
+       JsonParser *parser = NULL;  /* owned */
+       JsonReader *reader = NULL;  /* owned */
+       gint i;
+       GError *child_error = NULL;
 
-       if (response_body == NULL)
+       if (response_body == NULL) {
                goto parent;
+       }
 
-       if (length == -1)
+       if (length == -1) {
                length = strlen (response_body);
+       }
 
-       /* Parse the XML */
-       doc = xmlReadMemory (response_body, length, "/dev/null", NULL, 0);
-       if (doc == NULL)
+       parser = json_parser_new ();
+       if (!json_parser_load_from_data (parser, response_body, length,
+                                        &child_error)) {
                goto parent;
+       }
+
+       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);
@@ -534,7 +606,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()
  *
@@ -571,30 +643,40 @@ gdata_youtube_service_get_primary_authorization_domain (void)
        return get_youtube_authorization_domain ();
 }
 
-static const gchar *
+static gchar *
 standard_feed_type_to_feed_uri (GDataYouTubeStandardFeedType feed_type)
 {
        switch (feed_type) {
+       case GDATA_YOUTUBE_MOST_POPULAR_FEED:
+               return _gdata_service_build_uri ("https://www.googleapis.com/youtube/v3/videos";
+                                                "?part=snippet"
+                                                "&chart=mostPopular");
        case GDATA_YOUTUBE_TOP_RATED_FEED:
-               return "https://gdata.youtube.com/feeds/api/standardfeeds/top_rated";;
        case GDATA_YOUTUBE_TOP_FAVORITES_FEED:
-               return "https://gdata.youtube.com/feeds/api/standardfeeds/top_favorites";;
        case GDATA_YOUTUBE_MOST_VIEWED_FEED:
-               return "https://gdata.youtube.com/feeds/api/standardfeeds/most_viewed";;
-       case GDATA_YOUTUBE_MOST_POPULAR_FEED:
-               return "https://gdata.youtube.com/feeds/api/standardfeeds/most_popular";;
        case GDATA_YOUTUBE_MOST_RECENT_FEED:
-               return "https://gdata.youtube.com/feeds/api/standardfeeds/most_recent";;
        case GDATA_YOUTUBE_MOST_DISCUSSED_FEED:
-               return "https://gdata.youtube.com/feeds/api/standardfeeds/most_discussed";;
        case GDATA_YOUTUBE_MOST_LINKED_FEED:
-               return "https://gdata.youtube.com/feeds/api/standardfeeds/most_linked";;
        case GDATA_YOUTUBE_MOST_RESPONDED_FEED:
-               return "https://gdata.youtube.com/feeds/api/standardfeeds/most_responded";;
        case GDATA_YOUTUBE_RECENTLY_FEATURED_FEED:
-               return "https://gdata.youtube.com/feeds/api/standardfeeds/recently_featured";;
-       case GDATA_YOUTUBE_WATCH_ON_MOBILE_FEED:
-               return "https://gdata.youtube.com/feeds/api/standardfeeds/watch_on_mobile";;
+       case GDATA_YOUTUBE_WATCH_ON_MOBILE_FEED: {
+               gchar *date, *out;
+               GTimeVal tv;
+
+               /* All feed types except MOST_POPULAR have been deprecated for
+                * a while, and fall back to MOST_POPULAR on the server anyway.
+                * See: 
https://developers.google.com/youtube/2.0/developers_guide_protocol_video_feeds#Standard_feeds */
+               g_get_current_time (&tv);
+               tv.tv_sec -= 24 * 60 * 60;  /* 1 day ago */
+               date = g_time_val_to_iso8601 (&tv);
+               out = _gdata_service_build_uri ("https://www.googleapis.com/youtube/v3/videos";
+                                               "?part=snippet"
+                                               "&chart=mostPopular"
+                                               "&publishedAfter=%s", date);
+               g_free (date);
+
+               return out;
+       }
        default:
                g_assert_not_reached ();
        }
@@ -612,6 +694,11 @@ standard_feed_type_to_feed_uri (GDataYouTubeStandardFeedType feed_type)
  *
  * Queries the service's standard @feed_type feed to build a #GDataFeed.
  *
+ * Note that with the port from v2 to v3 of the YouTube API in libgdata
+ * UNRELEASED, all feed types except %GDATA_YOUTUBE_MOST_POPULAR_FEED have been
+ * deprecated. Other feed types will now transparently return
+ * %GDATA_YOUTUBE_MOST_POPULAR_FEED, limited to the past 24 hours.
+ *
  * Parameters and errors are as for gdata_service_query().
  *
  * Return value: (transfer full): a #GDataFeed of query results, or %NULL; unref with g_object_unref()
@@ -621,14 +708,24 @@ gdata_youtube_service_query_standard_feed (GDataYouTubeService *self, GDataYouTu
                                            GCancellable *cancellable, GDataQueryProgressCallback 
progress_callback, gpointer progress_user_data,
                                            GError **error)
 {
+       gchar *query_uri;
+       GDataFeed *feed;
+
        g_return_val_if_fail (GDATA_IS_YOUTUBE_SERVICE (self), NULL);
        g_return_val_if_fail (query == NULL || GDATA_IS_QUERY (query), NULL);
        g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
        g_return_val_if_fail (error == NULL || *error == NULL, NULL);
 
        /* TODO: Support the "time" parameter, as well as category- and region-specific feeds */
-       return gdata_service_query (GDATA_SERVICE (self), get_youtube_authorization_domain (), 
standard_feed_type_to_feed_uri (feed_type), query,
-                                   GDATA_TYPE_YOUTUBE_VIDEO, cancellable, progress_callback, 
progress_user_data, error);
+       query_uri = standard_feed_type_to_feed_uri (feed_type);
+       feed = gdata_service_query (GDATA_SERVICE (self),
+                                   get_youtube_authorization_domain (),
+                                   query_uri, query, GDATA_TYPE_YOUTUBE_VIDEO,
+                                   cancellable, progress_callback,
+                                   progress_user_data, error);
+       g_free (query_uri);
+
+       return feed;
 }
 
 /**
@@ -660,14 +757,22 @@ gdata_youtube_service_query_standard_feed_async (GDataYouTubeService *self, GDat
                                                  GDestroyNotify destroy_progress_user_data,
                                                  GAsyncReadyCallback callback, gpointer user_data)
 {
+       gchar *query_uri;
+
        g_return_if_fail (GDATA_IS_YOUTUBE_SERVICE (self));
        g_return_if_fail (query == NULL || GDATA_IS_QUERY (query));
        g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
        g_return_if_fail (callback != NULL);
 
-       gdata_service_query_async (GDATA_SERVICE (self), get_youtube_authorization_domain (), 
standard_feed_type_to_feed_uri (feed_type), query,
-                                  GDATA_TYPE_YOUTUBE_VIDEO, cancellable, progress_callback, 
progress_user_data, destroy_progress_user_data,
-                                  callback, user_data);
+       query_uri = standard_feed_type_to_feed_uri (feed_type);
+       gdata_service_query_async (GDATA_SERVICE (self),
+                                  get_youtube_authorization_domain (),
+                                  query_uri, query, GDATA_TYPE_YOUTUBE_VIDEO,
+                                  cancellable, progress_callback,
+                                  progress_user_data,
+                                  destroy_progress_user_data, callback,
+                                  user_data);
+       g_free (query_uri);
 }
 
 /**
@@ -696,8 +801,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);
 }
 
 /**
@@ -733,9 +844,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);
 }
 
 /**
@@ -750,8 +867,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()
  **/
@@ -760,7 +876,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;
 
@@ -770,19 +885,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 = _gdata_service_build_uri ("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;
@@ -817,7 +930,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));
@@ -826,23 +938,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 = _gdata_service_build_uri ("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);
 }
 
@@ -878,6 +984,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);
@@ -891,6 +999,8 @@ gdata_youtube_service_upload_video (GDataYouTubeService *self, GDataYouTubeVideo
                return NULL;
        }
 
+       /* FIXME: 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,
@@ -898,10 +1008,23 @@ gdata_youtube_service_upload_video (GDataYouTubeService *self, GDataYouTubeVideo
                return NULL;
        }
 
-       /* Streaming upload support using GDataUploadStream; automatically handles the XML and multipart 
stuff for us */
-       return GDATA_UPLOAD_STREAM (gdata_upload_stream_new (GDATA_SERVICE (self), 
get_youtube_authorization_domain (), SOUP_METHOD_POST,
-                                                            
"https://uploads.gdata.youtube.com/feeds/api/users/default/uploads";,
-                                                             GDATA_ENTRY (video), slug, content_type, 
cancellable));
+       /* FIXME: Add support for resumable uploads. That means a new
+        * gdata_youtube_service_upload_video_resumable() method a la
+        * Documents. */
+       stream = gdata_upload_stream_new (GDATA_SERVICE (self),
+                                                   get_youtube_authorization_domain (),
+                                                   SOUP_METHOD_POST,
+                                                   "https://www.googleapis.com/upload/youtube/v3/videos";
+                                                   "?part=snippet,status,"
+                                                         "recordingDetails,"
+                                                         "contentDetails,id,"
+                                                         "statistics,"
+                                                         "processingDetails",
+                                                   GDATA_ENTRY (video), slug,
+                                                   content_type,
+                                                   cancellable);
+
+       return GDATA_UPLOAD_STREAM (stream);
 }
 
 /**
@@ -936,7 +1059,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));
 }
 
 /**
@@ -972,6 +1098,8 @@ gdata_youtube_service_get_developer_key (GDataYouTubeService *self)
 GDataAPPCategories *
 gdata_youtube_service_get_categories (GDataYouTubeService *self, GCancellable *cancellable, GError **error)
 {
+       const gchar *locale;
+       gchar *uri;
        SoupMessage *message;
        GDataAPPCategories *categories;
 
@@ -979,16 +1107,31 @@ gdata_youtube_service_get_categories (GDataYouTubeService *self, GCancellable *c
        g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
        g_return_val_if_fail (error == NULL || *error == NULL, NULL);
 
-       /* Download the category list. Note that this is (service) locale-dependent. */
-       message = _gdata_service_query (GDATA_SERVICE (self), get_youtube_authorization_domain (),
-                                       "https://gdata.youtube.com/schemas/2007/categories.cat";, NULL, 
cancellable, error);
+       /* Download the category list. Note that this is (service)
+        * locale-dependent, and a locale must always be specified. */
+       locale = gdata_service_get_locale (GDATA_SERVICE (self));
+       if (locale == NULL) {
+               locale = "US";
+       }
+
+       uri = _gdata_service_build_uri ("https://www.googleapis.com/youtube/v3/videoCategories";
+                                       "?part=snippet"
+                                       "&regionCode=%s",
+                                       locale);
+       message = _gdata_service_query (GDATA_SERVICE (self),
+                                       get_youtube_authorization_domain (),
+                                       uri, NULL, cancellable, error);
+       g_free (uri);
+
        if (message == NULL)
                return NULL;
 
        g_assert (message->response_body->data != NULL);
-       categories = GDATA_APP_CATEGORIES (_gdata_parsable_new_from_xml (GDATA_TYPE_APP_CATEGORIES, 
message->response_body->data,
-                                                                        message->response_body->length,
-                                                                        GSIZE_TO_POINTER 
(GDATA_TYPE_YOUTUBE_CATEGORY), error));
+       categories = GDATA_APP_CATEGORIES (_gdata_parsable_new_from_json (GDATA_TYPE_APP_CATEGORIES,
+                                                                         message->response_body->data,
+                                                                         message->response_body->length,
+                                                                         GSIZE_TO_POINTER 
(GDATA_TYPE_YOUTUBE_CATEGORY),
+                                                                         error));
        g_object_unref (message);
 
        return categories;
diff --git a/gdata/services/youtube/gdata-youtube-service.h b/gdata/services/youtube/gdata-youtube-service.h
index 999fa4c..23e6ef5 100644
--- a/gdata/services/youtube/gdata-youtube-service.h
+++ b/gdata/services/youtube/gdata-youtube-service.h
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2008-2009 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2008-2009, 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
@@ -33,20 +33,51 @@ G_BEGIN_DECLS
 
 /**
  * GDataYouTubeStandardFeedType:
- * @GDATA_YOUTUBE_TOP_RATED_FEED: This feed contains the most highly rated YouTube videos.
- * @GDATA_YOUTUBE_TOP_FAVORITES_FEED: This feed contains videos most frequently flagged as favorite videos.
- * @GDATA_YOUTUBE_MOST_VIEWED_FEED: This feed contains the most frequently watched YouTube videos.
- * @GDATA_YOUTUBE_MOST_POPULAR_FEED: This feed contains the most popular YouTube videos, selected using an 
algorithm that combines many
- * different signals to determine overall popularity.
- * @GDATA_YOUTUBE_MOST_RECENT_FEED: This feed contains the videos most recently submitted to YouTube.
- * @GDATA_YOUTUBE_MOST_DISCUSSED_FEED: This feed contains the YouTube videos that have received the most 
comments.
- * @GDATA_YOUTUBE_MOST_LINKED_FEED: This feed contains the YouTube videos that receive the most links from 
other websites.
- * @GDATA_YOUTUBE_MOST_RESPONDED_FEED: This feed contains YouTube videos that receive the most video 
responses.
- * @GDATA_YOUTUBE_RECENTLY_FEATURED_FEED: This feed contains videos recently featured on the YouTube home 
page or featured videos tab.
- * @GDATA_YOUTUBE_WATCH_ON_MOBILE_FEED: This feed contains videos suitable for playback on mobile devices.
+ * @GDATA_YOUTUBE_TOP_RATED_FEED: This feed contains the most highly rated
+ *   YouTube videos. Deprecated: UNRELEASED: Google no longer supports this feed
+ *   type, and it will return results equivalent to
+ *   %GDATA_YOUTUBE_MOST_POPULAR_FEED.
+ * @GDATA_YOUTUBE_TOP_FAVORITES_FEED: This feed contains videos most frequently
+ *   flagged as favorite videos. Deprecated: UNRELEASED: Google no longer
+ *   supports this feed type, and it will return results equivalent to
+ *   %GDATA_YOUTUBE_MOST_POPULAR_FEED.
+ * @GDATA_YOUTUBE_MOST_VIEWED_FEED: This feed contains the most frequently
+ *   watched YouTube videos. Deprecated: UNRELEASED: Google no longer supports
+ *   this feed type, and it will return results equivalent to
+ *   %GDATA_YOUTUBE_MOST_POPULAR_FEED.
+ * @GDATA_YOUTUBE_MOST_POPULAR_FEED: This feed contains the most popular YouTube
+ *   videos, selected using an algorithm that combines many different signals to
+ *   determine overall popularity. As of version UNRELEASED, this is the only
+ *   supported feed type.
+ * @GDATA_YOUTUBE_MOST_RECENT_FEED: This feed contains the videos most recently
+ *   submitted to YouTube. Deprecated: UNRELEASED: Google no longer supports
+ *   this feed type, and it will return results equivalent to
+ *   %GDATA_YOUTUBE_MOST_POPULAR_FEED.
+ * @GDATA_YOUTUBE_MOST_DISCUSSED_FEED: This feed contains the YouTube videos
+ *   that have received the most comments. Deprecated: UNRELEASED: Google no
+ *   longer supports this feed type, and it will return results equivalent to
+ *   %GDATA_YOUTUBE_MOST_POPULAR_FEED.
+ * @GDATA_YOUTUBE_MOST_LINKED_FEED: This feed contains the YouTube videos that
+ *   receive the most links from other websites. Deprecated: UNRELEASED: Google
+ *   no longer supports this feed type, and it will return results equivalent to
+ *   %GDATA_YOUTUBE_MOST_POPULAR_FEED.
+ * @GDATA_YOUTUBE_MOST_RESPONDED_FEED: This feed contains YouTube videos that
+ *   receive the most video responses. Deprecated: UNRELEASED: Google no longer
+ *   supports this feed type, and it will return results equivalent to
+ *   %GDATA_YOUTUBE_MOST_POPULAR_FEED.
+ * @GDATA_YOUTUBE_RECENTLY_FEATURED_FEED: This feed contains videos recently
+ *   featured on the YouTube home page or featured videos tab. Deprecated:
+ *   UNRELEASED: Google no longer supports this feed type, and it will return
+ *   results equivalent to %GDATA_YOUTUBE_MOST_POPULAR_FEED.
+ * @GDATA_YOUTUBE_WATCH_ON_MOBILE_FEED: This feed contains videos suitable for
+ *   playback on mobile devices. Deprecated: UNRELEASED: Google no longer
+ *   supports this feed type, and it will return results equivalent to
+ *   %GDATA_YOUTUBE_MOST_POPULAR_FEED.
  *
- * Standard feed types for standard feed queries with gdata_youtube_service_query_standard_feed(). For more 
information, see
- * the <ulink type="http" 
url="http://code.google.com/apis/youtube/2.0/developers_guide_protocol.html#Standard_feeds";>online 
documentation</ulink>.
+ * Standard feed types for standard feed queries with
+ * gdata_youtube_service_query_standard_feed(). For more information, see the
+ * <ulink type="http" 
url="https://developers.google.com/youtube/2.0/developers_guide_protocol_video_feeds#Standard_feeds";>online
+ * documentation</ulink>.
  **/
 typedef enum {
        GDATA_YOUTUBE_TOP_RATED_FEED,
diff --git a/gdata/services/youtube/gdata-youtube-state.c b/gdata/services/youtube/gdata-youtube-state.c
index e2da9bb..4b0b354 100644
--- a/gdata/services/youtube/gdata-youtube-state.c
+++ b/gdata/services/youtube/gdata-youtube-state.c
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2009–2010 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2009–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
@@ -30,17 +30,12 @@
  **/
 
 #include <glib.h>
-#include <libxml/parser.h>
 
 #include "gdata-youtube-state.h"
 #include "gdata-parsable.h"
-#include "gdata-parser.h"
 
 static void gdata_youtube_state_finalize (GObject *object);
 static void gdata_youtube_state_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 gboolean parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError 
**error);
-static void get_namespaces (GDataParsable *parsable, GHashTable *namespaces);
 
 struct _GDataYouTubeStatePrivate {
        gchar *name;
@@ -62,19 +57,12 @@ static void
 gdata_youtube_state_class_init (GDataYouTubeStateClass *klass)
 {
        GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
-       GDataParsableClass *parsable_class = GDATA_PARSABLE_CLASS (klass);
 
        g_type_class_add_private (klass, sizeof (GDataYouTubeStatePrivate));
 
        gobject_class->get_property = gdata_youtube_state_get_property;
        gobject_class->finalize = gdata_youtube_state_finalize;
 
-       parsable_class->pre_parse_xml = pre_parse_xml;
-       parsable_class->parse_xml = parse_xml;
-       parsable_class->get_namespaces = get_namespaces;
-       parsable_class->element_name = "state";
-       parsable_class->element_namespace = "yt";
-
        /**
         * GDataYouTubeState:name:
         *
@@ -186,42 +174,6 @@ gdata_youtube_state_get_property (GObject *object, guint property_id, GValue *va
        }
 }
 
-static gboolean
-pre_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *root_node, gpointer user_data, GError **error)
-{
-       GDataYouTubeStatePrivate *priv = GDATA_YOUTUBE_STATE (parsable)->priv;
-       xmlChar *name;
-
-       name = xmlGetProp (root_node, (xmlChar*) "name");
-       if (name == NULL || *name == '\0') {
-               g_free (name);
-               return gdata_parser_error_required_property_missing (root_node, "name", error);
-       }
-
-       priv->name = (gchar*) name;
-       priv->reason_code = (gchar*) xmlGetProp (root_node, (xmlChar*) "reasonCode");
-       priv->help_uri = (gchar*) xmlGetProp (root_node, (xmlChar*) "helpUrl");
-       priv->message = (gchar*) xmlNodeListGetString (doc, root_node->children, TRUE);
-
-       return TRUE;
-}
-
-static gboolean
-parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error)
-{
-       /* Textual content's handled in pre_parse_xml */
-       if (node->type != XML_ELEMENT_NODE)
-               return TRUE;
-
-       return GDATA_PARSABLE_CLASS (gdata_youtube_state_parent_class)->parse_xml (parsable, doc, node, 
user_data, error);
-}
-
-static void
-get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
-{
-       g_hash_table_insert (namespaces, (gchar*) "yt", (gchar*) "http://gdata.youtube.com/schemas/2007";);
-}
-
 /**
  * gdata_youtube_state_get_name:
  * @self: a #GDataYouTubeState
diff --git a/gdata/services/youtube/gdata-youtube-video.c b/gdata/services/youtube/gdata-youtube-video.c
index e93751d..e59c5c7 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
@@ -25,11 +25,14 @@
  *
  * #GDataYouTubeVideo is a subclass of #GDataEntry to represent a single video on YouTube, either when 
uploading or querying.
  *
- * #GDataYouTubeVideo implements #GDataCommentable, allowing comments on videos to be queried using 
gdata_commentable_query_comments(), and new
- * comments to be added to videos using gdata_commentable_insert_comment(). Note that deletion of comments 
on any #GDataYouTubeVideo is not permitted;
- * gdata_commentable_delete_comment() will always fail if called on a #GDataYouTubeVideo.
+ * #GDataYouTubeVideo implements #GDataCommentable, allowing comments on videos
+ * to be queried and added. However, the initial version of the v3 YouTube API
+ * does not currently support comments, so all #GDataCommentable calls will
+ * fail. It is hoped that the YouTube API will regain support for comments in
+ * future.
  *
- * For more details of YouTube's GData API, see the <ulink type="http" 
url="http://code.google.com/apis/youtube/2.0/reference.html";>
+ * For more details of YouTube’s GData API, see the
+ * <ulink type="http" url="https://developers.google.com/youtube/v3/docs/";>
  * online documentation</ulink>.
  *
  * <example>
@@ -43,20 +46,13 @@
  *
  *     video = gdata_youtube_service_query_single_video (service, NULL, "R-9gzmQHoe0", NULL, NULL);
  *
- *     video_id = gdata_youtube_video_get_video_id (video); /<!-- -->* e.g. "R-9gzmQHoe0" *<!-- -->/
+ *     video_id = gdata_entry_get_id (GDATA_ENTRY (video)); /<!-- -->* e.g. "R-9gzmQHoe0" *<!-- -->/
  *     title = gdata_entry_get_title (GDATA_ENTRY (video)); /<!-- -->* e.g. "Korpiklaani Vodka (official 
video 2009)" *<!-- -->/
  *     player_uri = gdata_youtube_video_get_player_uri (video); /<!-- -->* e.g. 
"http://www.youtube.com/watch?v=ZTUVgYoeN_b"; *<!-- -->/
  *     description = gdata_youtube_video_get_description (video); /<!-- -->* e.g. "Vodka is the first single 
from the album..." *<!-- -->/
  *     published = gdata_entry_get_published (GDATA_ENTRY (video)); /<!-- -->* Date and time the video was 
originally published *<!-- -->/
  *     updated = gdata_entry_get_updated (GDATA_ENTRY (video)); /<!-- -->* When the video was most recently 
updated by the author *<!-- -->/
  *
- *     /<!-- -->* Retrieve a specific encoding of the video in GDataMediaContent format *<!-- -->/
- *     content = gdata_youtube_video_look_up_content (video, "video/3gpp");
- *     if (content != NULL)
- *             video_uri = gdata_media_content_get_uri (content); /<!-- -->* the URI for the direct 3GP 
version of the video *<!-- -->/
- *     else
- *             /<!-- -->* Fall back and try a different video encoding? SWF 
("application/x-shockwave-flash") is always present. *<!-- -->/
- *
  *     /<!-- -->* Get a list of GDataMediaThumbnail<!-- -->s for the video *<!-- -->/
  *     for (thumbnails = gdata_youtube_video_get_thumbnails (video); thumbnails != NULL; thumbnails = 
thumbnails->next)
  *             download_and_do_something_with_thumbnail (gdata_media_thumbnail_get_uri (thumbnail));
@@ -69,7 +65,7 @@
 #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"
@@ -78,25 +74,19 @@
 #include "gdata-parser.h"
 #include "media/gdata-media-category.h"
 #include "media/gdata-media-thumbnail.h"
-#include "gdata-youtube-group.h"
 #include "gdata-types.h"
-#include "gdata-youtube-control.h"
 #include "gdata-youtube-enums.h"
-#include "georss/gdata-georss-where.h"
-#include "gd/gdata-gd-feed-link.h"
 #include "gdata-commentable.h"
 #include "gdata-youtube-comment.h"
 #include "gdata-youtube-service.h"
 
-static GObject *gdata_youtube_video_constructor (GType type, guint n_construct_params, GObjectConstructParam 
*construct_params);
 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 void get_json (GDataParsable *parsable, JsonBuilder *builder);
+static const gchar *get_content_type (void);
 static gchar *get_entry_uri (const gchar *id) G_GNUC_WARN_UNUSED_RESULT;
 static void gdata_youtube_video_commentable_init (GDataCommentableInterface *iface);
 static GDataAuthorizationDomain *get_authorization_domain (GDataCommentable *self) G_GNUC_CONST;
@@ -118,18 +108,31 @@ 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;
+       guint duration;
+       gboolean is_private;
 
-       /* georss:where */
-       GDataGeoRSSWhere *georss_where;
+       /* Location. */
+       gdouble latitude;
+       gdouble longitude;
 
        /* Other properties */
-       GDataYouTubeControl *youtube_control;
+       gchar *rejection_reason;
+       gchar *processing_status;
+       gchar *upload_status;
+       gchar *failure_reason;
+       GDataYouTubeState *upload_state;  /* owned */
+
        gint64 recorded;
 
-       /* Comments */
-       GDataGDFeedLink *comments_feed_link;
+       /* State for parse_json(). */
+       gboolean parsing_in_video_list_response;
 };
 
 enum {
@@ -169,19 +172,17 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
 
        g_type_class_add_private (klass, sizeof (GDataYouTubeVideoPrivate));
 
-       gobject_class->constructor = gdata_youtube_video_constructor;
        gobject_class->get_property = gdata_youtube_video_get_property;
        gobject_class->set_property = gdata_youtube_video_set_property;
        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->get_json = get_json;
+       parsable_class->get_content_type = get_content_type;
 
        entry_class->get_entry_uri = get_entry_uri;
-       entry_class->kind_term = "http://gdata.youtube.com/schemas/2007#video";;
+       entry_class->kind_term = "youtube#video";  /* also: youtube#searchResult */
 
        /**
         * GDataYouTubeVideo:view-count:
@@ -189,7 +190,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * The number of times the video has been viewed.
         *
         * 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>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#statistics.viewCount";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_VIEW_COUNT,
                                         g_param_spec_uint ("view-count",
@@ -203,7 +204,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * The number of users who have added the video to their favorites.
         *
         * 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>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#statistics.favoriteCount";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_FAVORITE_COUNT,
                                         g_param_spec_uint ("favorite-count",
@@ -217,7 +218,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * Descriptive text about the location where the video was taken.
         *
         * For more information, see the <ulink type="http"
-        * 
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_yt:location";>online 
documentation</ulink>.
+        * 
url="https://developers.google.com/youtube/v3/docs/videos#recordingDetails.locationDescription";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_LOCATION,
                                         g_param_spec_string ("location",
@@ -231,7 +232,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * The minimum allowed rating for the video.
         *
         * For more information, see the <ulink type="http"
-        * url="http://code.google.com/apis/gdata/docs/2.0/elements.html#gdRating";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#statistics.likeCount";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_MIN_RATING,
                                         g_param_spec_uint ("min-rating",
@@ -245,7 +246,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * The maximum allowed rating for the video.
         *
         * For more information, see the <ulink type="http"
-        * url="http://code.google.com/apis/gdata/docs/2.0/elements.html#gdRating";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#statistics.likeCount";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_MAX_RATING,
                                         g_param_spec_uint ("max-rating",
@@ -259,7 +260,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * The number of times the video has been rated.
         *
         * For more information, see the <ulink type="http"
-        * url="http://code.google.com/apis/gdata/docs/2.0/elements.html#gdRating";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#statistics.likeCount";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_RATING_COUNT,
                                         g_param_spec_uint ("rating-count",
@@ -273,7 +274,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * The average rating of the video, over all the ratings it's received.
         *
         * For more information, see the <ulink type="http"
-        * url="http://code.google.com/apis/gdata/docs/2.0/elements.html#gdRating";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#statistics.likeCount";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_AVERAGE_RATING,
                                         g_param_spec_double ("average-rating",
@@ -287,7 +288,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * A %NULL-terminated array of words associated with the video.
         *
         * For more information, see the <ulink type="http"
-        * 
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_media:keywords";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#snippet.tags[]";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_KEYWORDS,
                                         g_param_spec_boxed ("keywords",
@@ -299,9 +300,6 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * GDataYouTubeVideo:player-uri:
         *
         * A URI for a browser-based media player for the full-length video (i.e. the video's page on 
YouTube).
-        *
-        * For more information, see the <ulink type="http"
-        * 
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_media:player";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_PLAYER_URI,
                                         g_param_spec_string ("player-uri",
@@ -315,7 +313,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * Specifies a genre or developer tag that describes the video.
         *
         * For more information, see the <ulink type="http"
-        * 
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_media:category";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#snippet.categoryId";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_CATEGORY,
                                         g_param_spec_object ("category",
@@ -323,19 +321,24 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
                                                              GDATA_TYPE_MEDIA_CATEGORY,
                                                              G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 
+       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
        /**
         * GDataYouTubeVideo:credit:
         *
         * Identifies the owner of the video.
         *
-        * For more information, see the <ulink type="http"
-        * 
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_media:credit";>online 
documentation</ulink>.
+        * Deprecated: UNRELEASED: This is no longer supported by Google, and
+        *   will always be %NULL. There is no replacement.
         **/
        g_object_class_install_property (gobject_class, PROP_CREDIT,
                                         g_param_spec_object ("credit",
                                                              "Credit", "Identifies the owner of the video.",
                                                              GDATA_TYPE_YOUTUBE_CREDIT,
-                                                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+                                                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS |
+                                                             G_PARAM_DEPRECATED));
+
+       G_GNUC_END_IGNORE_DEPRECATIONS
 
        /**
         * GDataYouTubeVideo:description:
@@ -343,7 +346,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * A summary or description of the video.
         *
         * For more information, see the <ulink type="http"
-        * 
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_media:description";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#snippet.description";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_DESCRIPTION,
                                         g_param_spec_string ("description",
@@ -357,7 +360,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * The duration of the video in seconds.
         *
         * For more information, see the <ulink type="http"
-        * 
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_yt:duration";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#contentDetails.duration";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_DURATION,
                                         g_param_spec_uint ("duration",
@@ -371,7 +374,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * Indicates whether the video is private, meaning that it will not be publicly visible on YouTube's 
website.
         *
         * For more information, see the <ulink type="http"
-        * 
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_yt:private";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#status.privacyStatus";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_IS_PRIVATE,
                                         g_param_spec_boolean ("is-private",
@@ -385,7 +388,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * Specifies the time the video was originally uploaded to YouTube.
         *
         * For more information, see the <ulink type="http"
-        * 
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_yt:uploaded";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#snippet.publishedAt";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_UPLOADED,
                                         g_param_spec_int64 ("uploaded",
@@ -399,27 +402,31 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * Specifies a unique ID which YouTube uses to identify the video. For example: 
<literal>qz8EfkS4KK0</literal>.
         *
         * For more information, see the <ulink type="http"
-        * 
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_yt:videoid";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#id";>online documentation</ulink>.
+        *
+        * Deprecated: UNRELEASED: This is now equal to #GDataEntry:id.
         **/
        g_object_class_install_property (gobject_class, PROP_VIDEO_ID,
                                         g_param_spec_string ("video-id",
                                                              "Video ID", "Specifies a unique ID which 
YouTube uses to identify the video.",
                                                              NULL,
-                                                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+                                                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS |
+                                                             G_PARAM_DEPRECATED));
 
        /**
         * GDataYouTubeVideo:is-draft:
         *
         * Indicates whether the video is in draft, or unpublished, status.
         *
-        * For more information, see the <ulink type="http"
-        * url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_app:draft";>online 
documentation</ulink>.
+        * Deprecated: UNRELEASED: This is now equal to
+        *   #GDataYouTubeVideo:is-private.
         **/
        g_object_class_install_property (gobject_class, PROP_IS_DRAFT,
                                         g_param_spec_boolean ("is-draft",
                                                               "Draft?", "Indicates whether the video is in 
draft, or unpublished, status.",
                                                               FALSE,
-                                                              G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                                                              G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |
+                                                              G_PARAM_DEPRECATED));
 
        /**
         * GDataYouTubeVideo:state:
@@ -428,7 +435,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * It points to a #GDataYouTubeState.
         *
         * For more information, see the <ulink type="http"
-        * url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_yt:state";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#status.uploadStatus";>online 
documentation</ulink>.
         **/
        g_object_class_install_property (gobject_class, PROP_STATE,
                                         g_param_spec_object ("state",
@@ -442,7 +449,7 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * Specifies the time the video was originally recorded.
         *
         * For more information, see the <ulink type="http"
-        * 
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_yt:recorded";>online 
documentation</ulink>.
+        * url="https://developers.google.com/youtube/v3/docs/videos#recordingDetails.recordingDate";>online 
documentation</ulink>.
         *
         * Since: 0.3.0
         **/
@@ -458,9 +465,6 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * The aspect ratio of the video. A %NULL value means the aspect ratio is unknown (it could still be 
a widescreen video). A value of
         * %GDATA_YOUTUBE_ASPECT_RATIO_WIDESCREEN means the video is definitely widescreen.
         *
-        * For more information see the <ulink type="http"
-        * 
url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_yt:aspectratio";>online 
documentation</ulink>.
-        *
         * Since: 0.4.0
         **/
        g_object_class_install_property (gobject_class, PROP_ASPECT_RATIO,
@@ -475,8 +479,9 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * The location as a latitude coordinate associated with this video. Valid latitudes range from <code 
class="literal">-90.0</code>
         * to <code class="literal">90.0</code> inclusive.
         *
-        * For more information, see the <ulink type="http" 
url="http://code.google.com/apis/youtube/2.0/reference.html#GeoRSS_elements_reference";>
-        * GeoRSS specification</ulink>.
+        * For more information, see the
+        * <ulink type="http" 
url="https://developers.google.com/youtube/v3/docs/videos#recordingDetails.location.latitude";>
+        * online documentation</ulink>.
         *
         * Since: 0.8.0
         **/
@@ -492,8 +497,9 @@ gdata_youtube_video_class_init (GDataYouTubeVideoClass *klass)
         * The location as a longitude coordinate associated with this video. Valid longitudes range from 
<code class="literal">-180.0</code>
         * to <code class="literal">180.0</code> inclusive.
         *
-        * For more information, see the <ulink type="http" 
url="http://code.google.com/apis/youtube/2.0/reference.html#GeoRSS_elements_reference";>
-        * GeoRSS specification</ulink>.
+        * For more information, see the
+        * <ulink type="http" 
url="https://developers.google.com/youtube/v3/docs/videos#recordingDetails.location.longitude";>
+        * online documentation</ulink>.
         *
         * Since: 0.8.0
         **/
@@ -515,43 +521,11 @@ 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 *
-gdata_youtube_video_constructor (GType type, guint n_construct_params, GObjectConstructParam 
*construct_params)
-{
-       GObject *object;
-
-       /* Chain up to the parent class */
-       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) {
-               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);
-       }
-
-       return object;
 }
 
 static void
@@ -559,21 +533,9 @@ 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;
-
-       if (priv->youtube_control != NULL)
-               g_object_unref (priv->youtube_control);
-       priv->youtube_control = NULL;
-
-       if (priv->comments_feed_link != NULL)
-               g_object_unref (priv->comments_feed_link);
-       priv->comments_feed_link = NULL;
+       g_clear_object (&priv->category);
+       g_list_free_full (priv->thumbnails, (GDestroyNotify) g_object_unref);
+       g_clear_object (&priv->upload_state);
 
        /* Chain up to the parent class */
        G_OBJECT_CLASS (gdata_youtube_video_parent_class)->dispose (object);
@@ -586,6 +548,15 @@ gdata_youtube_video_finalize (GObject *object)
 
        g_free (priv->location);
        g_hash_table_destroy (priv->access_controls);
+       g_strfreev (priv->keywords);
+       g_free (priv->player_uri);
+       g_strfreev (priv->region_restriction_allowed);
+       g_strfreev (priv->region_restriction_blocked);
+       g_clear_pointer (&priv->content_ratings, (GDestroyNotify) g_hash_table_unref);
+       g_free (priv->rejection_reason);
+       g_free (priv->processing_status);
+       g_free (priv->upload_status);
+       g_free (priv->failure_reason);
 
        /* Chain up to the parent class */
        G_OBJECT_CLASS (gdata_youtube_video_parent_class)->finalize (object);
@@ -619,49 +590,49 @@ gdata_youtube_video_get_property (GObject *object, guint property_id, GValue *va
                        g_value_set_double (value, priv->rating.average);
                        break;
                case PROP_KEYWORDS:
-                       g_value_set_boxed (value, gdata_media_group_get_keywords (priv->media_group));
+                       g_value_set_boxed (value, priv->keywords);
                        break;
                case PROP_PLAYER_URI:
-                       g_value_set_string (value, gdata_media_group_get_player_uri (priv->media_group));
+                       g_value_set_string (value, gdata_youtube_video_get_player_uri (GDATA_YOUTUBE_VIDEO 
(object)));
                        break;
                case PROP_CATEGORY:
-                       g_value_set_object (value, gdata_media_group_get_category (priv->media_group));
+                       g_value_set_object (value, priv->category);
                        break;
                case PROP_CREDIT:
-                       g_value_set_object (value, gdata_media_group_get_credit (priv->media_group));
+                       g_value_set_object (value, NULL);
                        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));
+                       g_value_set_boolean (value, gdata_youtube_video_is_private (GDATA_YOUTUBE_VIDEO 
(object)));
                        break;
                case PROP_STATE:
-                       g_value_set_object (value, gdata_youtube_control_get_state (priv->youtube_control));
+                       g_value_set_object (value, gdata_youtube_video_get_state (GDATA_YOUTUBE_VIDEO 
(object)));
                        break;
                case PROP_RECORDED:
                        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, gdata_youtube_video_get_aspect_ratio (GDATA_YOUTUBE_VIDEO 
(object)));
                        break;
                case PROP_LATITUDE:
-                       g_value_set_double (value, gdata_georss_where_get_latitude (priv->georss_where));
+                       g_value_set_double (value, priv->latitude);
                        break;
                case PROP_LONGITUDE:
-                       g_value_set_double (value, gdata_georss_where_get_longitude (priv->georss_where));
+                       g_value_set_double (value, priv->longitude);
                        break;
                default:
                        /* We don't have any other property... */
@@ -692,7 +663,7 @@ gdata_youtube_video_set_property (GObject *object, guint property_id, const GVal
                        gdata_youtube_video_set_is_private (self, g_value_get_boolean (value));
                        break;
                case PROP_IS_DRAFT:
-                       gdata_youtube_video_set_is_draft (self, g_value_get_boolean (value));
+                       gdata_youtube_video_set_is_private (self, g_value_get_boolean (value));
                        break;
                case PROP_RECORDED:
                        gdata_youtube_video_set_recorded (self, g_value_get_int64 (value));
@@ -701,11 +672,13 @@ gdata_youtube_video_set_property (GObject *object, guint property_id, const GVal
                        gdata_youtube_video_set_aspect_ratio (self, g_value_get_string (value));
                        break;
                case PROP_LATITUDE:
-                       gdata_youtube_video_set_coordinates (self, g_value_get_double (value),
-                                                            gdata_georss_where_get_longitude 
(self->priv->georss_where));
+                       gdata_youtube_video_set_coordinates (self,
+                                                            g_value_get_double (value),
+                                                            self->priv->longitude);
                        break;
                case PROP_LONGITUDE:
-                       gdata_youtube_video_set_coordinates (self, gdata_georss_where_get_latitude 
(self->priv->georss_where),
+                       gdata_youtube_video_set_coordinates (self,
+                                                            self->priv->latitude,
                                                             g_value_get_double (value));
                        break;
                default:
@@ -715,264 +688,784 @@ gdata_youtube_video_set_property (GObject *object, guint property_id, const GVal
        }
 }
 
+/* https://developers.google.com/youtube/v3/docs/videos#contentDetails.duration
+ *
+ * Note that it can also include an ‘hours’ component, as specified in ISO 8601,
+ * but not in the Google documentation. */
 static gboolean
-parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error)
+duration_from_json_member (JsonReader *reader, const gchar *member_name,
+                           GDataParserOptions options, guint *output,
+                           gboolean *success, GError **error)
 {
-       gboolean success;
-       GDataYouTubeVideo *self = GDATA_YOUTUBE_VIDEO (parsable);
+       gchar *duration_str = NULL, *i = NULL, *new_i = NULL;
+       guint64 seconds;
+       gboolean child_success = FALSE;
+
+       if (!gdata_parser_string_from_json_member (reader, member_name, options,
+                                                  &duration_str,
+                                                  &child_success, error)) {
+               return FALSE;
+       }
 
-       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) {
-               return success;
-       } else if (gdata_parser_is_namespace (node, "http://www.w3.org/2007/app";) == TRUE &&
-                  gdata_parser_object_from_element (node, "control", P_REQUIRED | P_NO_DUPES, 
GDATA_TYPE_YOUTUBE_CONTROL,
-                                                    &(self->priv->youtube_control), &success, error) == 
TRUE) {
-               return success;
-       } else if (gdata_parser_is_namespace (node, "http://www.georss.org/georss";) == TRUE &&
-                  gdata_parser_object_from_element (node, "where", P_REQUIRED, GDATA_TYPE_GEORSS_WHERE,
-                                                    &(self->priv->georss_where), &success, error) == TRUE) {
-               return success;
-       } else if (gdata_parser_is_namespace (node, "http://gdata.youtube.com/schemas/2007";) == TRUE) {
-               if (gdata_parser_string_from_element (node, "location", P_NONE, &(self->priv->location), 
&success, error) == TRUE) {
-                       return success;
-               } else if (xmlStrcmp (node->name, (xmlChar*) "statistics") == 0) {
-                       /* yt:statistics */
-                       xmlChar *view_count, *favorite_count;
-
-                       /* View count */
-                       view_count = xmlGetProp (node, (xmlChar*) "viewCount");
-                       if (view_count == NULL)
-                               return gdata_parser_error_required_property_missing (node, "viewCount", 
error);
-                       self->priv->view_count = g_ascii_strtoull ((gchar*) view_count, NULL, 10);
-                       xmlFree (view_count);
-
-                       /* Favourite count */
-                       favorite_count = xmlGetProp (node, (xmlChar*) "favoriteCount");
-                       self->priv->favorite_count = (favorite_count != NULL) ? g_ascii_strtoull ((gchar*) 
favorite_count, NULL, 10) : 0;
-                       xmlFree (favorite_count);
-               } else if (xmlStrcmp (node->name, (xmlChar*) "noembed") == 0) {
-                       /* yt:noembed */
-                       /* Ignore this now; it's been superceded by yt:accessControl.
-                        * See http://apiblog.youtube.com/2010/02/extended-access-controls-available-via.html 
*/
-               } else if (xmlStrcmp (node->name, (xmlChar*) "accessControl") == 0) {
-                       /* yt:accessControl */
-                       xmlChar *action, *permission;
-                       GDataYouTubePermission permission_enum;
-
-                       action = xmlGetProp (node, (xmlChar*) "action");
-                       if (action == NULL)
-                               return gdata_parser_error_required_property_missing (node, "action", error);
-                       permission = xmlGetProp (node, (xmlChar*) "permission");
-                       if (permission == NULL) {
-                               xmlFree (action);
-                               return gdata_parser_error_required_property_missing (node, "permission", 
error);
-                       }
+       *success = child_success;
+       *output = 0;
 
-                       /* Work out what the permission is */
-                       if (xmlStrcmp (permission, (xmlChar*) "allowed") == 0) {
-                               permission_enum = GDATA_YOUTUBE_PERMISSION_ALLOWED;
-                       } else if (xmlStrcmp (permission, (xmlChar*) "denied") == 0) {
-                               permission_enum = GDATA_YOUTUBE_PERMISSION_DENIED;
-                       } else if (xmlStrcmp (permission, (xmlChar*) "moderated") == 0) {
-                               permission_enum = GDATA_YOUTUBE_PERMISSION_MODERATED;
-                       } else {
-                               xmlFree (action);
-                               xmlFree (permission);
-                               return gdata_parser_error_unknown_property_value (node, "permission", 
(gchar*) permission, error);
-                       }
+       if (!child_success) {
+               return TRUE;
+       }
 
-                       /* Store the access control */
-                       g_hash_table_insert (self->priv->access_controls, (gchar*) action, GINT_TO_POINTER 
(permission_enum));
-               } else if (xmlStrcmp (node->name, (xmlChar*) "recorded") == 0) {
-                       /* yt:recorded */
-                       xmlChar *recorded;
-                       gint64 recorded_int64;
-
-                       recorded = xmlNodeListGetString (doc, node->children, TRUE);
-                       if (gdata_parser_int64_from_date ((gchar*) recorded, &recorded_int64) == FALSE) {
-                               /* Error */
-                               gdata_parser_error_not_iso8601_format (node, (gchar*) recorded, error);
-                               xmlFree (recorded);
-                               return FALSE;
-                       }
-                       xmlFree (recorded);
-                       gdata_youtube_video_set_recorded (self, recorded_int64);
-               } else {
-                       return GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->parse_xml (parsable, 
doc, node, user_data, error);
+       /* Parse the string. Format: ‘PT(hH)?(mM)?(sS)?’, where ‘h’, ‘m’ and ‘s’
+        * are integer numbers of hours, minutes and seconds. Each element may
+        * not be present. */
+       i = duration_str;
+       if (strncmp (duration_str, "PT", 2) != 0) {
+               goto error;
+       }
+
+       i += 2;  /* PT */
+
+       seconds = 0;
+
+       while (*i != '\0') {
+               guint64 element;
+               gchar designator;
+
+               element = g_ascii_strtoull (i, &new_i, 10);
+               if (new_i == i) {
+                       goto error;
                }
-       } else if (gdata_parser_is_namespace (node, "http://schemas.google.com/g/2005";) == TRUE) {
-               if (xmlStrcmp (node->name, (xmlChar*) "rating") == 0) {
-                       /* gd:rating */
-                       xmlChar *min, *max, *num_raters, *average;
-                       guint num_raters_uint;
-                       gdouble average_double;
-
-                       min = xmlGetProp (node, (xmlChar*) "min");
-                       if (min == NULL)
-                               return gdata_parser_error_required_property_missing (node, "min", error);
-
-                       max = xmlGetProp (node, (xmlChar*) "max");
-                       if (max == NULL) {
-                               gdata_parser_error_required_property_missing (node, "max", error);
-                               xmlFree (min);
-                               return FALSE;
-                       }
 
-                       num_raters = xmlGetProp (node, (xmlChar*) "numRaters");
-                       if (num_raters == NULL)
-                               num_raters_uint = 0;
-                       else
-                               num_raters_uint = g_ascii_strtoull ((gchar*) num_raters, NULL, 10);
-                       xmlFree (num_raters);
-
-                       average = xmlGetProp (node, (xmlChar*) "average");
-                       if (average == NULL)
-                               average_double = 0;
-                       else
-                               average_double = g_ascii_strtod ((gchar*) average, NULL);
-                       xmlFree (average);
-
-                       self->priv->rating.min = g_ascii_strtoull ((gchar*) min, NULL, 10);
-                       self->priv->rating.max = g_ascii_strtoull ((gchar*) max, NULL, 10);
-                       self->priv->rating.count = num_raters_uint;
-                       self->priv->rating.average = average_double;
-               } else if (xmlStrcmp (node->name, (xmlChar*) "comments") == 0) {
-                       /* gd:comments */
-                       xmlNode *child_node;
-
-                       /* This is actually the child of the <comments> element */
-                       child_node = node->children;
-                       if (child_node == NULL) {
-                               return gdata_parser_error_required_element_missing ("gd:feedLink", 
"gd:comments", error);
-                       }
+               i = new_i;
 
-                       if (gdata_parser_object_from_element (child_node, "feedLink", P_REQUIRED | 
P_NO_DUPES, GDATA_TYPE_GD_FEED_LINK,
-                                                             &(self->priv->comments_feed_link), &success, 
error) == TRUE) {
-                               return success;
-                       }
+               designator = i[0];
+               if (designator == 'H') {
+                       seconds += 60 * 60 * element;
+               } else if (designator == 'M') {
+                       seconds += 60 * element;
+               } else if (designator == 'S') {
+                       seconds += element;
                } else {
-                       return GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->parse_xml (parsable, 
doc, node, user_data, error);
+                       goto error;
                }
-       } else {
-               return GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->parse_xml (parsable, doc, 
node, user_data, error);
+
+               i += 1;
        }
 
+       *output = seconds;
+       *success = child_success;
+
+       g_free (duration_str);
+
+       return TRUE;
+
+error:
+       gdata_parser_error_not_iso8601_format_json (reader, duration_str,
+                                                   error);
+       g_free (duration_str);
+
        return TRUE;
 }
 
+/* https://developers.google.com/youtube/v3/docs/videos#snippet.thumbnails */
 static gboolean
-post_parse_xml (GDataParsable *parsable, gpointer user_data, GError **error)
+thumbnails_from_json_member (JsonReader *reader, const gchar *member_name,
+                             GDataParserOptions options, GList **output,
+                             gboolean *success, GError **error)
 {
-       GDataYouTubeVideoPrivate *priv = GDATA_YOUTUBE_VIDEO (parsable)->priv;
+       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;
+       }
 
-       /* Chain up to the parent class */
-       GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->post_parse_xml (parsable, user_data, error);
+       /* 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;
+       }
 
-       /* This must always exist, so is_draft can be set on it */
-       if (priv->youtube_control == NULL)
-               priv->youtube_control = g_object_new (GDATA_TYPE_YOUTUBE_CONTROL, NULL);
+       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;
 }
 
-static void
-access_control_cb (const gchar *action, gpointer value, GString *xml_string)
+/* 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)
 {
-       GDataYouTubePermission permission = GPOINTER_TO_INT (value);
-       const gchar *permission_string;
+       guint i, len;
+       const GError *child_error = NULL;
 
-       switch (permission) {
-               case GDATA_YOUTUBE_PERMISSION_ALLOWED:
-                       permission_string = "allowed";
-                       break;
-               case GDATA_YOUTUBE_PERMISSION_DENIED:
-                       permission_string = "denied";
-                       break;
-               case GDATA_YOUTUBE_PERMISSION_MODERATED:
-                       permission_string = "moderated";
-                       break;
-               default:
-                       g_assert_not_reached ();
+       /* 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;
        }
 
-       gdata_parser_string_append_escaped (xml_string, "<yt:accessControl action='", action, "'");
-       g_string_append_printf (xml_string, " permission='%s'/>", permission_string);
+       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;
 }
 
-static void
-get_xml (GDataParsable *parsable, GString *xml_string)
+/* https://developers.google.com/youtube/v3/docs/videos#contentDetails.contentRating */
+static gboolean
+content_rating_from_json_member (JsonReader *reader,
+                                 const gchar *member_name,
+                                 GDataParserOptions options,
+                                 GHashTable **output,
+                                 gboolean *success, GError **error)
 {
-       GDataYouTubeVideoPrivate *priv = GDATA_YOUTUBE_VIDEO (parsable)->priv;
+       guint i, len;
+       const GError *child_error = NULL;
 
-       /* Chain up to the parent class */
-       GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->get_xml (parsable, xml_string);
+       /* Check if there's such element */
+       if (g_strcmp0 (json_reader_get_member_name (reader),
+                      member_name) != 0) {
+               return FALSE;
+       }
 
-       /* media:group */
-       _gdata_parsable_get_xml (GDATA_PARSABLE (priv->media_group), xml_string, 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);
 
-       if (priv->location != NULL)
-               gdata_parser_string_append_escaped (xml_string, "<yt:location>", priv->location, 
"</yt:location>");
+       len = json_reader_count_members (reader);
+       child_error = json_reader_get_error (reader);
 
-       if (priv->recorded != -1) {
-               gchar *recorded = gdata_parser_date_from_int64 (priv->recorded);
-               g_string_append_printf (xml_string, "<yt:recorded>%s</yt:recorded>", recorded);
-               g_free (recorded);
+       if (child_error != NULL) {
+               *success = gdata_parser_error_from_json_error (reader,
+                                                              child_error,
+                                                              error);
+               return TRUE;
        }
 
-       /* yt:accessControl */
-       g_hash_table_foreach (priv->access_controls, (GHFunc) access_control_cb, xml_string);
+       *output = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                        g_free, g_free);
 
-       /* app:control */
-       _gdata_parsable_get_xml (GDATA_PARSABLE (priv->youtube_control), xml_string, FALSE);
+       for (i = 0; i < len; i++) {
+               const gchar *scheme, *rating;
 
-       /* georss:where */
-       if (priv->georss_where != NULL && gdata_georss_where_get_latitude (priv->georss_where) != G_MAXDOUBLE 
&&
-           gdata_georss_where_get_longitude (priv->georss_where) != G_MAXDOUBLE) {
-               _gdata_parsable_get_xml (GDATA_PARSABLE (priv->georss_where), xml_string, FALSE);
+               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 void
-get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
+static guint64
+parse_uint64_from_json_string_member (JsonReader *reader,
+                                      const gchar *member_name,
+                                      GError **error)
 {
-       GDataYouTubeVideoPrivate *priv = GDATA_YOUTUBE_VIDEO (parsable)->priv;
+       const gchar *str_val, *end_ptr;
+       guint64 out;
+       const GError *child_error = NULL;
 
-       /* Chain up to the parent class */
-       GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->get_namespaces (parsable, namespaces);
+       /* Grab the string. */
+       json_reader_read_member (reader, member_name);
 
-       g_hash_table_insert (namespaces, (gchar*) "yt", (gchar*) "http://gdata.youtube.com/schemas/2007";);
+       str_val = json_reader_get_string_value (reader);
+       child_error = json_reader_get_error (reader);
 
-       /* 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);
+       if (child_error != NULL) {
+               gdata_parser_error_from_json_error (reader, child_error, error);
+               out = 0;
+               goto done;
+       }
+
+       /* Try and parse it as an integer. */
+       out = g_ascii_strtoull (str_val, (gchar **) &end_ptr, 10);
+
+       if (*end_ptr != '\0') {
+               gdata_parser_error_required_json_content_missing (reader,
+                                                                 error);
+               out = 0;
+               goto done;
+       }
+
+done:
+       json_reader_end_member (reader);
+
+       return out;
 }
 
-static gchar *
-get_entry_uri (const gchar *id)
+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;
+
+       /* HACK: When called with gdata_service_query_single_entry(), the video
+        * list endpoint returns a 0–1 item list of results as a normal feed.
+        * (See: https://developers.google.com/youtube/v3/docs/videos/list)
+        * This differs from the v2 API, which returned just the entry.
+        *
+        * So, we need a hack to extract the single entry from the feed without
+        * being able to invoke the parsing machinery in GDataFeed, because
+        * gdata_service_query_single_entry() can’t do that. Do that by checking
+        * the kind, and then ignoring all subsequent members until we reach the
+        * items member. Recursively parse in there, then break out again.
+        * This all assumes that we see the kind member before items. */
+       if (g_strcmp0 (json_reader_get_member_name (reader), "kind") == 0 &&
+           g_strcmp0 (json_reader_get_string_value (reader),
+                      "youtube#videoListResponse") == 0) {
+               priv->parsing_in_video_list_response = TRUE;
+               return TRUE;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader), "items") == 0 &&
+                  priv->parsing_in_video_list_response) {
+               guint i;
+
+               /* Instead of a 404 when searching for an invalid ID, the server
+                * returns an empty results list. */
+               if (json_reader_count_elements (reader) != 1) {
+                       g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_NOT_FOUND,
+                                    /* Translators: the parameter is an error message returned by the 
server. */
+                                    _("The requested resource was not found: %s"),
+                                    "items");
+                       return TRUE;
+               }
+
+               /* Parse the first (and only) array element. */
+               json_reader_read_element (reader, 0);
+               priv->parsing_in_video_list_response = FALSE;
+
+               /* Parse all its properties. */
+               for (i = 0; i < (guint) json_reader_count_members (reader); i++) {
+                       g_return_val_if_fail (json_reader_read_element (reader, i), NULL);
+
+                       if (GDATA_PARSABLE_GET_CLASS (self)->parse_json (GDATA_PARSABLE (self), reader, 
user_data, error) == FALSE) {
+                               json_reader_end_element (reader);
+                               g_object_unref (parsable);
+                               break;
+                       }
+
+                       json_reader_end_element (reader);
+               }
+
+               priv->parsing_in_video_list_response = TRUE;
+               json_reader_end_element (reader);
+
+               return TRUE;  /* handled */
+       } else if (priv->parsing_in_video_list_response) {
+               /* Ignore the member. */
+               return TRUE;
+       }
+
+       /* Actual video property parsing. */
+       if (g_strcmp0 (json_reader_get_member_name (reader), "id") == 0) {
+               const gchar *id = NULL;
+
+               /* If this is a youtube#searchResult, the id will be an object:
+                * https://developers.google.com/youtube/v3/docs/search#resource
+                * If it is a youtube#video, the id will be a string:
+                * https://developers.google.com/youtube/v3/docs/videos#resource
+                */
+
+               if (json_reader_is_value (reader)) {
+                       id = json_reader_get_string_value (reader);
+               } else if (json_reader_is_object (reader)) {
+                       json_reader_read_member (reader, "videoId");
+                       id = json_reader_get_string_value (reader);
+                       json_reader_end_member (reader);
+               }
+
+               /* Empty ID? */
+               if (id == NULL || *id == '\0') {
+                       return gdata_parser_error_required_json_content_missing (reader, error);
+               }
+
+               _gdata_entry_set_id (GDATA_ENTRY (parsable), id);
+
+               return TRUE;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader),
+                             "snippet") == 0) {
+               guint i;
+
+               /* Check this is an object. */
+               if (!json_reader_is_object (reader)) {
+                       return gdata_parser_error_required_json_content_missing (reader, error);
+               }
+
+               for (i = 0; i < (guint) json_reader_count_members (reader); i++) {
+                       gint64 published_at;
+                       gchar *title = NULL, *description = NULL;
+                       gchar *category_id = NULL;
+
+                       json_reader_read_element (reader, i);
+
+                       if (gdata_parser_int64_time_from_json_member (reader, "publishedAt", P_DEFAULT, 
&published_at, &success, error)) {
+                               if (success) {
+                                       _gdata_entry_set_published (GDATA_ENTRY (parsable),
+                                                                   published_at);
+                               }
+                       } else if (gdata_parser_string_from_json_member (reader, "title", P_DEFAULT, &title, 
&success, error)) {
+                               if (success) {
+                                       gdata_entry_set_title (GDATA_ENTRY (parsable),
+                                                              title);
+                               }
+
+                               g_free (title);
+                       } else if (gdata_parser_string_from_json_member (reader, "description", P_DEFAULT, 
&description, &success, error)) {
+                               if (success) {
+                                       gdata_entry_set_summary (GDATA_ENTRY (parsable),
+                                                                description);
+                               }
+
+                               g_free (description);
+                       } else if (gdata_parser_strv_from_json_member (reader, "tags", P_DEFAULT, 
&priv->keywords, &success, error) ||
+                                  thumbnails_from_json_member (reader, "thumbnails", P_DEFAULT, 
&priv->thumbnails, &success, error)) {
+                               /* Fall through. */
+                       } else if (gdata_parser_string_from_json_member (reader, "categoryId", P_DEFAULT, 
&category_id, &success, error)) {
+                               if (success) {
+                                       priv->category = gdata_media_category_new (category_id,
+                                                                                  NULL,
+                                                                                  NULL);
+                               }
+
+                               g_free (category_id);
+                       }
+
+                       json_reader_end_element (reader);
+
+                       if (!success) {
+                               return FALSE;
+                       }
+               }
+
+               return TRUE;
+       } 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);
+
+                       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)) {
+                               /* Fall through. */
+                       }
+
+                       json_reader_end_element (reader);
+
+                       if (!success) {
+                               return FALSE;
+                       }
+               }
+
+               return TRUE;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader),
+                             "status") == 0) {
+               const gchar *privacy_status;
+
+               /* Check this is an object. */
+               if (!json_reader_is_object (reader)) {
+                       return gdata_parser_error_required_json_content_missing (reader, error);
+               }
+
+               json_reader_read_member (reader, "privacyStatus");
+               privacy_status = json_reader_get_string_value (reader);
+               json_reader_end_member (reader);
+
+               if (g_strcmp0 (privacy_status, "private") == 0) {
+                       priv->is_private = TRUE;
+               } else if (g_strcmp0 (privacy_status, "public") == 0) {
+                       priv->is_private = FALSE;
+                       g_hash_table_insert (priv->access_controls,
+                                            (gpointer) "list",
+                                            GINT_TO_POINTER (GDATA_YOUTUBE_PERMISSION_ALLOWED));
+               } else if (g_strcmp0 (privacy_status, "unlisted") == 0) {
+                       /* See: ‘list’ on
+                        * 
https://developers.google.com/youtube/2.0/reference?csw=1#youtube_data_api_tag_yt:accessControl */
+                       priv->is_private = FALSE;
+                       g_hash_table_insert (priv->access_controls,
+                                            (gpointer) "list",
+                                            GINT_TO_POINTER (GDATA_YOUTUBE_PERMISSION_DENIED));
+               }
+
+               json_reader_read_member (reader, "embeddable");
+               g_hash_table_insert (priv->access_controls,
+                                    (gpointer) GDATA_YOUTUBE_ACTION_EMBED,
+                                    GINT_TO_POINTER (json_reader_get_boolean_value (reader) ?
+                                                     GDATA_YOUTUBE_PERMISSION_ALLOWED :
+                                                     GDATA_YOUTUBE_PERMISSION_DENIED));
+               json_reader_end_member (reader);
+
+               json_reader_read_member (reader, "uploadStatus");
+               priv->upload_status = g_strdup (json_reader_get_string_value (reader));
+               json_reader_end_member (reader);
+
+               json_reader_read_member (reader, "failureReason");
+               priv->rejection_reason = g_strdup (json_reader_get_string_value (reader));
+               json_reader_end_member (reader);
+
+               json_reader_read_member (reader, "rejectionReason");
+               priv->rejection_reason = g_strdup (json_reader_get_string_value (reader));
+               json_reader_end_member (reader);
+
+               return TRUE;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader),
+                             "statistics") == 0) {
+               gint64 likes, dislikes;
+               GError *child_error = NULL;
+
+               /* Check this is an object. */
+               if (!json_reader_is_object (reader)) {
+                       return gdata_parser_error_required_json_content_missing (reader, error);
+               }
+
+               /* Views and favourites. For some unknown reason, the feed
+                * returns them as a string, even though they’re documented as
+                * being unsigned longs.
+                *
+                * Reference: https://developers.google.com/youtube/v3/docs/videos#statistics */
+               priv->view_count = parse_uint64_from_json_string_member (reader, "viewCount", &child_error);
+               if (child_error != NULL) {
+                       g_propagate_error (error, child_error);
+                       return FALSE;
+               }
+
+               priv->favorite_count = parse_uint64_from_json_string_member (reader, "favoriteCount", 
&child_error);
+               if (child_error != NULL) {
+                       g_propagate_error (error, child_error);
+                       return FALSE;
+               }
+
+               /* The new ratings API (total likes, total dislikes) doesn’t
+                * really match with the old API (collection of integer ratings
+                * between 1 and 5). Try and return something appropriate. */
+               likes = parse_uint64_from_json_string_member (reader, "likeCount", &child_error);
+               if (child_error != NULL) {
+                       g_propagate_error (error, child_error);
+                       return FALSE;
+               }
+
+               dislikes = parse_uint64_from_json_string_member (reader, "dislikeCount", &child_error);
+               if (child_error != NULL) {
+                       g_propagate_error (error, child_error);
+                       return FALSE;
+               }
+
+               priv->rating.min = 0;
+               priv->rating.max = 1;
+               priv->rating.count = likes + dislikes;
+               if (likes + dislikes == 0) {
+                       priv->rating.average = 0.0;  /* basically undefined */
+               } else {
+                       priv->rating.average = (gdouble) likes / (gdouble) (likes + dislikes);
+               }
+
+               return TRUE;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader),
+                             "processingDetails") == 0) {
+               /* Check this is an object. */
+               if (!json_reader_is_object (reader)) {
+                       return gdata_parser_error_required_json_content_missing (reader, error);
+               }
+
+               json_reader_read_member (reader, "processingStatus");
+               priv->processing_status = g_strdup (json_reader_get_string_value (reader));
+               json_reader_end_member (reader);
+
+               return TRUE;
+       } else if (g_strcmp0 (json_reader_get_member_name (reader),
+                             "recordingDetails") == 0) {
+               const gchar *recording_date;
+               const GError *child_error = NULL;
+
+               /* Check this is an object. */
+               if (!json_reader_is_object (reader)) {
+                       return gdata_parser_error_required_json_content_missing (reader, error);
+               }
+
+               json_reader_read_member (reader, "recordingDate");
+               recording_date = json_reader_get_string_value (reader);
+               json_reader_end_member (reader);
+
+               if (!gdata_parser_int64_from_date (recording_date,
+                                                  &priv->recorded)) {
+                       /* Error */
+                       gdata_parser_error_not_iso8601_format_json (reader,
+                                                                   recording_date,
+                                                                   error);
+                       return FALSE;
+               }
+
+               json_reader_read_member (reader, "locationDescription");
+               priv->location = g_strdup (json_reader_get_string_value (reader));
+               json_reader_end_member (reader);
+
+               if (json_reader_read_member (reader, "location")) {
+                       json_reader_read_member (reader, "latitude");
+                       priv->latitude = json_reader_get_double_value (reader);
+                       json_reader_end_member (reader);
+
+                       json_reader_read_member (reader, "longitude");
+                       priv->longitude = json_reader_get_double_value (reader);
+                       json_reader_end_member (reader);
+               }
+
+               json_reader_end_member (reader);
+
+               child_error = json_reader_get_error (reader);
+               if (child_error != NULL) {
+                       return gdata_parser_error_from_json_error (reader,
+                                                                  child_error,
+                                                                  error);
+               }
+
+               return TRUE;
+       } else {
+               return GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->parse_json (parsable, reader, 
user_data, error);
+       }
+
+       return TRUE;
+}
+
+static void
+get_json (GDataParsable *parsable, JsonBuilder *builder)
 {
-       /* 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;
+       GDataEntry *entry = GDATA_ENTRY (parsable);
+       GDataYouTubeVideoPrivate *priv = GDATA_YOUTUBE_VIDEO (parsable)->priv;
        guint i;
+       gpointer permission_ptr;
 
-       parts = g_strsplit (id, ":", -1);
+       /* Chain up to the parent class */
+       GDATA_PARSABLE_CLASS (gdata_youtube_video_parent_class)->get_json (parsable, builder);
+
+       /* Add the video-specific JSON.
+        * Reference:
+        * https://developers.google.com/youtube/v3/docs/videos/insert#request_body */
+       /* snippet object. */
+       json_builder_set_member_name (builder, "snippet");
+       json_builder_begin_object (builder);
+
+       if (gdata_entry_get_title (entry) != NULL) {
+               json_builder_set_member_name (builder, "title");
+               json_builder_add_string_value (builder,
+                                              gdata_entry_get_title (entry));
+       }
 
-       for (i = 0; parts[i] != NULL && parts[i + 1] != NULL; i += 2) {
-               if (strcmp (parts[i], "video") == 0) {
-                       video_id = parts[i + 1];
-                       break;
+       if (gdata_entry_get_summary (entry) != NULL) {
+               json_builder_set_member_name (builder, "description");
+               json_builder_add_string_value (builder,
+                                              gdata_entry_get_summary (entry));
+       }
+
+       if (priv->keywords != NULL) {
+               json_builder_set_member_name (builder, "tags");
+               json_builder_begin_array (builder);
+
+               for (i = 0; priv->keywords[i] != NULL; i++) {
+                       json_builder_add_string_value (builder,
+                                                      priv->keywords[i]);
                }
+
+               json_builder_end_array (builder);
+       }
+
+       if (priv->category != NULL) {
+               json_builder_set_member_name (builder, "categoryId");
+               json_builder_add_string_value (builder,
+                                              gdata_media_category_get_category (priv->category));
        }
 
-       g_assert (video_id != NULL);
+       json_builder_end_object (builder);
+
+       /* status object. */
+       json_builder_set_member_name (builder, "status");
+       json_builder_begin_object (builder);
+
+       json_builder_set_member_name (builder, "privacyStatus");
 
-       /* Build the URI using the video ID */
-       uri = g_strconcat ("https://gdata.youtube.com/feeds/api/videos/";, video_id, NULL);
-       g_strfreev (parts);
+       if (!priv->is_private &&
+           g_hash_table_lookup_extended (priv->access_controls,
+                                         "list", NULL,
+                                         &permission_ptr)) {
+               GDataYouTubePermission perm;
+
+               perm = GPOINTER_TO_INT (permission_ptr);
+
+               /* See the ‘list’ documentation on:
+                * 
https://developers.google.com/youtube/2.0/reference?csw=1#youtube_data_api_tag_yt:accessControl */
+               json_builder_add_string_value (builder,
+                                              (perm == GDATA_YOUTUBE_PERMISSION_ALLOWED) ? "public" : 
"unlisted");
+       } else {
+               json_builder_add_string_value (builder,
+                                              priv->is_private ? "private" : "public");
+       }
+
+       if (g_hash_table_lookup_extended (priv->access_controls,
+                                         GDATA_YOUTUBE_ACTION_EMBED, NULL,
+                                         &permission_ptr)) {
+               GDataYouTubePermission perm;
+
+               perm = GPOINTER_TO_INT (permission_ptr);
+
+               json_builder_set_member_name (builder, "embeddable");
+               json_builder_add_boolean_value (builder,
+                                               perm == GDATA_YOUTUBE_PERMISSION_ALLOWED);
+       }
+
+       /* FIXME: add support for:
+        * publicStatsViewable
+        * publishAt
+        * license
+        */
+
+       json_builder_end_object (builder);
+
+       /* recordingDetails object. */
+       json_builder_set_member_name (builder, "recordingDetails");
+       json_builder_begin_object (builder);
+
+       if (priv->location != NULL) {
+               json_builder_set_member_name (builder, "locationDescription");
+               json_builder_add_string_value (builder, priv->location);
+       }
+
+       if (priv->latitude != G_MAXDOUBLE && priv->longitude != G_MAXDOUBLE) {
+               json_builder_set_member_name (builder, "location");
+               json_builder_begin_object (builder);
+
+               json_builder_set_member_name (builder, "latitude");
+               json_builder_add_double_value (builder, priv->latitude);
+
+               json_builder_set_member_name (builder, "longitude");
+               json_builder_add_double_value (builder, priv->longitude);
+
+               json_builder_end_object (builder);
+       }
 
-       return uri;
+       if (priv->recorded != -1) {
+               gchar *recorded = gdata_parser_date_from_int64 (priv->recorded);
+               json_builder_set_member_name (builder, "recordingDate");
+               json_builder_add_string_value (builder, recorded);
+               g_free (recorded);
+       }
+
+       json_builder_end_object (builder);
+}
+
+static const gchar *
+get_content_type (void)
+{
+       return "application/json";
+}
+
+static gchar *
+get_entry_uri (const gchar *id)
+{
+       const gchar *old_prefix = "tag:youtube.com,2008:video:";
+
+       /* For compatibility with previous video ID formats, strip off the v2
+        * ID prefix. */
+       if (g_str_has_prefix (id, old_prefix)) {
+               id += strlen (old_prefix);
+       }
+
+       /* Build the query URI for a single video. This is a bit of a pain,
+        * because it actually returns a list containing a single video, but
+        * there seems no other way to do it. See parsing_in_video_list_response
+        * in parse_json() for the fallout.
+        *
+        * Reference: https://developers.google.com/youtube/v3/docs/videos/list#part */
+       return _gdata_service_build_uri ("https://www.googleapis.com/youtube/v3/videos";
+                                        "?part=contentDetails,id,"
+                                              "recordingDetails,snippet,"
+                                              "status,statistics,"
+                                              "processingDetails"
+                                        "&id=%s", id);
 }
 
 static GDataAuthorizationDomain *
@@ -984,35 +1477,24 @@ get_authorization_domain (GDataCommentable *self)
 static gchar *
 get_query_comments_uri (GDataCommentable *self)
 {
-       GDataGDFeedLink *feed_link;
-
-       feed_link = GDATA_YOUTUBE_VIDEO (self)->priv->comments_feed_link;
-
-       if (feed_link == NULL) {
-               return NULL;
-       }
-
-       return _gdata_service_fix_uri_scheme (gdata_gd_feed_link_get_uri (feed_link));
+       /* FIXME: Currently unsupported:
+        * https://developers.google.com/youtube/v3/migration-guide#to_be_migrated */
+       return NULL;
 }
 
 static gchar *
 get_insert_comment_uri (GDataCommentable *self, GDataComment *comment_)
 {
-       GDataGDFeedLink *feed_link;
-
-       feed_link = GDATA_YOUTUBE_VIDEO (self)->priv->comments_feed_link;
-
-       if (feed_link == NULL) {
-               return NULL;
-       }
-
-       return _gdata_service_fix_uri_scheme (gdata_gd_feed_link_get_uri (feed_link));
+       /* FIXME: Currently unsupported:
+        * https://developers.google.com/youtube/v3/migration-guide#to_be_migrated */
+       return NULL;
 }
 
 static gboolean
 is_comment_deletable (GDataCommentable *self, GDataComment *comment_)
 {
-       /* Deletion of comments is unsupported. */
+       /* FIXME: Currently unsupported:
+        * https://developers.google.com/youtube/v3/migration-guide#to_be_migrated */
        return FALSE;
 }
 
@@ -1150,6 +1632,10 @@ gdata_youtube_video_set_access_control (GDataYouTubeVideo *self, const gchar *ac
  * @average: (out caller-allocates) (allow-none): return location for the average rating value, or %NULL
  *
  * Gets various properties of the ratings on the video.
+ *
+ * Note that this property may not be retrieved when querying for multiple
+ * videos at once, but is guaranteed to be retrieved when querying with
+ * gdata_service_query_single_entry_async().
  **/
 void
 gdata_youtube_video_get_rating (GDataYouTubeVideo *self, guint *min, guint *max, guint *count, gdouble 
*average)
@@ -1177,7 +1663,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;
 }
 
 /**
@@ -1188,7 +1674,7 @@ gdata_youtube_video_get_keywords (GDataYouTubeVideo *self)
  * Sets the #GDataYouTubeVideo:keywords property to the new keyword list, @keywords.
  *
  * @keywords must not be %NULL. For more information, see the <ulink type="http"
- * url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_media:keywords";>online 
documentation</ulink>.
+ * url="https://developers.google.com/youtube/v3/docs/videos#snippet.tags[]";>online documentation</ulink>.
  **/
 void
 gdata_youtube_video_set_keywords (GDataYouTubeVideo *self, const gchar * const *keywords)
@@ -1196,7 +1682,8 @@ gdata_youtube_video_set_keywords (GDataYouTubeVideo *self, const gchar * const *
        g_return_if_fail (GDATA_IS_YOUTUBE_VIDEO (self));
        g_return_if_fail (keywords != NULL);
 
-       gdata_media_group_set_keywords (self->priv->media_group, keywords);
+       g_strfreev (self->priv->keywords);
+       self->priv->keywords = g_strdupv ((gchar **) keywords);
        g_object_notify (G_OBJECT (self), "keywords");
 }
 
@@ -1211,8 +1698,33 @@ gdata_youtube_video_set_keywords (GDataYouTubeVideo *self, const gchar * const *
 const gchar *
 gdata_youtube_video_get_player_uri (GDataYouTubeVideo *self)
 {
+       GDataYouTubeVideoPrivate *priv;
+
        g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
-       return gdata_media_group_get_player_uri (self->priv->media_group);
+
+       priv = self->priv;
+
+       /* Generate and cache the player URI. */
+       if (priv->player_uri == NULL) {
+               priv->player_uri = _gdata_service_build_uri ("https://www.youtube.com/watch?v=%s";,
+                                                            gdata_entry_get_id (GDATA_ENTRY (self)));
+       }
+
+       return priv->player_uri;
+}
+
+static gboolean
+strv_contains (const gchar * const *strv, const gchar *key)
+{
+       guint i;
+
+       for (i = 0; strv[i] != NULL; i++) {
+               if (g_strcmp0 (strv[i], key) == 0) {
+                       return TRUE;
+               }
+       }
+
+       return FALSE;
 }
 
 /**
@@ -1230,10 +1742,65 @@ 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 (!strv_contains ((const gchar * const *) priv->region_restriction_allowed, country) &&
+               (strv_contains ((const gchar * const *) priv->region_restriction_blocked, country) ||
+                priv->region_restriction_allowed == NULL ||
+                priv->region_restriction_allowed[0] == NULL));
+}
+
+/* References:
+ * v2: https://developers.google.com/youtube/2.0/reference#youtube_data_api_tag_media:rating
+ * v3: https://developers.google.com/youtube/v3/docs/videos#contentDetails.contentRating.mpaaRating
+ */
+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;
+       }
+}
+
+/* References:
+ * v2: https://developers.google.com/youtube/2.0/reference#youtube_data_api_tag_media:rating
+ * v3: https://developers.google.com/youtube/v3/docs/videos#contentDetails.contentRating.tvpgRating
+ */
+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;
+       }
 }
 
 /**
@@ -1244,7 +1811,7 @@ gdata_youtube_video_is_restricted_in_country (GDataYouTubeVideo *self, const gch
  * Returns the rating of the given type for the video, if one exists. For example, this could be a film 
rating awarded by the MPAA; or a simple
  * rating specifying whether the video contains adult content.
  *
- * The valid values for @rating_type are: %GDATA_YOUTUBE_RATING_TYPE_SIMPLE, %GDATA_YOUTUBE_RATING_TYPE_MPAA 
and %GDATA_YOUTUBE_RATING_TYPE_V_CHIP.
+ * The valid values for @rating_type are: %GDATA_YOUTUBE_RATING_TYPE_MPAA and 
%GDATA_YOUTUBE_RATING_TYPE_V_CHIP.
  * Further values may be added in future; if an unknown rating type is passed to the function, %NULL will be 
returned.
  *
  * The possible return values depend on what's passed to @rating_type. Valid values for each rating type are 
listed in the documentation for the
@@ -1257,10 +1824,31 @@ gdata_youtube_video_is_restricted_in_country (GDataYouTubeVideo *self, const gch
 const gchar *
 gdata_youtube_video_get_media_rating (GDataYouTubeVideo *self, const gchar *rating_type)
 {
+       const gchar *rating;
+
        g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
        g_return_val_if_fail (rating_type != NULL && *rating_type != '\0', NULL);
 
-       return gdata_media_group_get_media_rating (self->priv->media_group, rating_type);
+       /* All ratings are unknown. */
+       if (self->priv->content_ratings == NULL) {
+               return NULL;
+       }
+
+       /* Compatibility with the old API. */
+       if (g_strcmp0 (rating_type, "simple") == 0) {
+               /* Not supported any more. */
+               return NULL;
+       } else if (g_strcmp0 (rating_type, "mpaa") == 0) {
+               rating = g_hash_table_lookup (self->priv->content_ratings,
+                                             "mpaaRating");
+               return convert_mpaa_rating (rating);
+       } else if (g_strcmp0 (rating_type, "v-chip") == 0) {
+               rating = g_hash_table_lookup (self->priv->content_ratings,
+                                             "tvpgRating");
+               return convert_tvpg_rating (rating);
+       }
+
+       return g_hash_table_lookup (self->priv->content_ratings, rating_type);
 }
 
 /**
@@ -1275,7 +1863,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;
 }
 
 /**
@@ -1286,7 +1874,7 @@ gdata_youtube_video_get_category (GDataYouTubeVideo *self)
  * Sets the #GDataYouTubeVideo:category property to the new category, @category, and increments its 
reference count.
  *
  * @category must not be %NULL. For more information, see the <ulink type="http"
- * url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_media:category";>online 
documentation</ulink>.
+ * url="https://developers.google.com/youtube/v3/docs/videos#snippet.categoryId";>online 
documentation</ulink>.
  **/
 void
 gdata_youtube_video_set_category (GDataYouTubeVideo *self, GDataMediaCategory *category)
@@ -1294,10 +1882,14 @@ 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");
 }
 
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
 /**
  * gdata_youtube_video_get_credit:
  * @self: a #GDataYouTubeVideo
@@ -1305,14 +1897,18 @@ gdata_youtube_video_set_category (GDataYouTubeVideo *self, GDataMediaCategory *c
  * Gets the #GDataYouTubeVideo:credit property.
  *
  * Return value: (transfer none): a #GDataMediaCredit giving information on whom to credit for the video, or 
%NULL
+ * Deprecated: UNRELEASED: This is no longer supported by Google, and will
+ *   always return %NULL. There is no replacement.
  **/
 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 NULL;
 }
 
+G_GNUC_END_IGNORE_DEPRECATIONS
+
 /**
  * gdata_youtube_video_get_description:
  * @self: a #GDataYouTubeVideo
@@ -1325,7 +1921,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,11 +1937,12 @@ 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");
 }
 
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
 /**
  * gdata_youtube_video_look_up_content:
  * @self: a #GDataYouTubeVideo
@@ -1355,6 +1952,9 @@ gdata_youtube_video_set_description (GDataYouTubeVideo *self, const gchar *descr
  * a list of URIs to various formats of the video itself, such as its SWF URI or RTSP stream.
  *
  * Return value: (transfer none): a #GDataYouTubeContent matching @type, or %NULL
+ * Deprecated: UNRELEASED: This is no longer supported by Google, and will
+ *   always return %NULL. To view a video, open the URI returned by
+ *   gdata_youtube_video_get_player_uri() in a web browser.
  **/
 GDataYouTubeContent *
 gdata_youtube_video_look_up_content (GDataYouTubeVideo *self, const gchar *type)
@@ -1362,9 +1962,12 @@ gdata_youtube_video_look_up_content (GDataYouTubeVideo *self, const gchar *type)
        g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
        g_return_val_if_fail (type != NULL, NULL);
 
-       return GDATA_YOUTUBE_CONTENT (gdata_media_group_look_up_content (self->priv->media_group, type));
+       /* Not supported in the v3 API. */
+       return NULL;
 }
 
+G_GNUC_END_IGNORE_DEPRECATIONS
+
 /**
  * gdata_youtube_video_get_thumbnails:
  * @self: a #GDataYouTubeVideo
@@ -1377,7 +1980,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 +1995,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 +2010,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 +2024,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 +2040,7 @@ gint64
 gdata_youtube_video_get_uploaded (GDataYouTubeVideo *self)
 {
        g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), -1);
-       return gdata_youtube_group_get_uploaded (GDATA_YOUTUBE_GROUP (self->priv->media_group));
+       return gdata_entry_get_published (GDATA_ENTRY (self));
 }
 
 /**
@@ -1447,12 +2050,13 @@ gdata_youtube_video_get_uploaded (GDataYouTubeVideo *self)
  * Gets the #GDataYouTubeVideo:video-id property.
  *
  * Return value: the video's unique and permanent ID
+ * Deprecated: UNRELEASED: This is now equal to #GDataEntry:id.
  **/
 const gchar *
 gdata_youtube_video_get_video_id (GDataYouTubeVideo *self)
 {
        g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
-       return gdata_youtube_group_get_video_id (GDATA_YOUTUBE_GROUP (self->priv->media_group));
+       return gdata_entry_get_id (GDATA_ENTRY (self));
 }
 
 /**
@@ -1462,12 +2066,14 @@ gdata_youtube_video_get_video_id (GDataYouTubeVideo *self)
  * Gets the #GDataYouTubeVideo:is-draft property.
  *
  * Return value: %TRUE if the video is a draft, %FALSE otherwise
+ * Deprecated: UNRELEASED: This is now equal to
+ *   gdata_youtube_video_is_private().
  **/
 gboolean
 gdata_youtube_video_is_draft (GDataYouTubeVideo *self)
 {
        g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), FALSE);
-       return gdata_youtube_control_is_draft (self->priv->youtube_control);
+       return gdata_youtube_video_is_private (self);
 }
 
 /**
@@ -1476,15 +2082,97 @@ gdata_youtube_video_is_draft (GDataYouTubeVideo *self)
  * @is_draft: whether the video is a draft
  *
  * Sets the #GDataYouTubeVideo:is-draft property to decide whether the video is a draft.
+ *
+ * Deprecated: UNRELEASED: This is now equivalent to
+ *   gdata_youtube_video_set_is_private().
  **/
 void
 gdata_youtube_video_set_is_draft (GDataYouTubeVideo *self, gboolean is_draft)
 {
        g_return_if_fail (GDATA_IS_YOUTUBE_VIDEO (self));
-       gdata_youtube_control_set_is_draft (self->priv->youtube_control, is_draft);
+       gdata_youtube_video_set_is_private (self, is_draft);
        g_object_notify (G_OBJECT (self), "is-draft");
 }
 
+/* Convert from v3 to v2 API video upload state. References:
+ * v2: https://developers.google.com/youtube/2.0/reference?csw=1#youtube_data_api_tag_yt:state
+ * v3: https://developers.google.com/youtube/v3/docs/videos#processingDetails.processingStatus
+ *     https://developers.google.com/youtube/v3/docs/videos#status.uploadStatus
+ */
+static const gchar *
+convert_state_name (const gchar *v3_processing_status,
+                    const gchar *v3_upload_status)
+{
+       if (g_strcmp0 (v3_upload_status, "deleted") == 0 ||
+           g_strcmp0 (v3_upload_status, "failed") == 0 ||
+           g_strcmp0 (v3_upload_status, "rejected") == 0) {
+               return v3_upload_status;
+       } else if (g_strcmp0 (v3_processing_status, "processing") == 0) {
+               return v3_processing_status;
+       }
+
+       return NULL;
+}
+
+/* References:
+ * v2: https://developers.google.com/youtube/2.0/reference?csw=1#youtube_data_api_tag_yt:state
+ * v3: https://developers.google.com/youtube/v3/docs/videos#status.failureReason
+ *     https://developers.google.com/youtube/v3/docs/videos#status.rejectionReason
+ */
+static const gchar *
+convert_state_reason_code (const gchar *v2_name,
+                           const gchar *v3_failure_reason,
+                           const gchar *v3_rejection_reason)
+{
+       if (v2_name == NULL ||
+           g_strcmp0 (v2_name, "processing") == 0 ||
+           g_strcmp0 (v2_name, "deleted") == 0) {
+               /* Explicitly unset if unknown, processing or deleted. */
+               return NULL;
+       } else if (g_strcmp0 (v2_name, "restricted") == 0) {
+               /* Unsupported conversion; convert_state_name() can never return
+                * ‘restricted’ anyway. */
+               return NULL;
+       } else if (g_strcmp0 (v2_name, "rejected") == 0) {
+               if (g_strcmp0 (v3_rejection_reason, "claim") == 0 ||
+                   g_strcmp0 (v3_rejection_reason, "copyright") == 0 ||
+                   g_strcmp0 (v3_rejection_reason, "trademark") == 0) {
+                       return "copyright";
+               } else if (g_strcmp0 (v3_rejection_reason, "duplicate") == 0) {
+                       return "duplicate";
+               } else if (g_strcmp0 (v3_rejection_reason,
+                                     "inappropriate") == 0) {
+                       return "inappropriate";
+               } else if (g_strcmp0 (v3_rejection_reason, "length") == 0) {
+                       return "tooLong";
+               } else if (g_strcmp0 (v3_rejection_reason, "termsOfUse") == 0) {
+                       return "termsOfUse";
+               } else if (g_strcmp0 (v3_rejection_reason,
+                                     "uploaderAccountClosed") == 0 ||
+                          g_strcmp0 (v3_rejection_reason,
+                                     "uploaderAccountSuspended") == 0) {
+                       return "duplicate";
+               } else {
+                       /* Generic fallback. */
+                       return "termsOfUse";
+               }
+       } else if (g_strcmp0 (v2_name, "failed") == 0) {
+               if (g_strcmp0 (v3_failure_reason, "codec") == 0) {
+                       return "unsupportedCodec";
+               } else if (g_strcmp0 (v3_failure_reason, "conversion") == 0) {
+                       return "invalidFormat";
+               } else if (g_strcmp0 (v3_failure_reason, "emptyFile") == 0) {
+                       return "empty";
+               } else if (g_strcmp0 (v3_failure_reason, "tooSmall") == 0) {
+                       return "tooSmall";
+               } else {
+                       return "cantProcess";
+               }
+       }
+
+       return NULL;
+}
+
 /**
  * gdata_youtube_video_get_state:
  * @self: a #GDataYouTubeVideo
@@ -1492,15 +2180,38 @@ gdata_youtube_video_set_is_draft (GDataYouTubeVideo *self, gboolean is_draft)
  * Gets the #GDataYouTubeVideo:state property.
  *
  * For more information, see the <ulink type="http"
- * url="http://code.google.com/apis/youtube/2.0/reference.html#youtube_data_api_tag_yt:state";>online 
documentation</ulink>.
+ * url="https://developers.google.com/youtube/v3/docs/videos#status.uploadStatus";>online 
documentation</ulink>.
  *
  * Return value: (transfer none): a #GDataYouTubeState showing the state of the video, or %NULL
  **/
 GDataYouTubeState *
 gdata_youtube_video_get_state (GDataYouTubeVideo *self)
 {
+       GDataYouTubeVideoPrivate *priv;
+
        g_return_val_if_fail (GDATA_IS_YOUTUBE_VIDEO (self), NULL);
-       return gdata_youtube_control_get_state (self->priv->youtube_control);
+
+       priv = self->priv;
+
+       /* Lazily create the state object. */
+       if (priv->upload_state == NULL) {
+               const gchar *name, *reason_code;
+
+               name = convert_state_name (priv->processing_status,
+                                          priv->upload_status);
+               reason_code = convert_state_reason_code (name,
+                                                        priv->failure_reason,
+                                                        priv->rejection_reason);
+
+               priv->upload_state = g_object_new (GDATA_TYPE_YOUTUBE_STATE,
+                                                  "name", name,
+                                                  "reason-code", reason_code,
+                                                  "help-uri", NULL,
+                                                  "message", NULL,
+                                                  NULL);
+       }
+
+       return priv->upload_state;
 }
 
 /**
@@ -1622,7 +2333,10 @@ 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));
+
+       /* Permanently NULL for the moment, but let’s not deprecate the property
+        * because it looks like it might come in useful in future. */
+       return NULL;
 }
 
 /**
@@ -1639,8 +2353,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_object_notify (G_OBJECT (self), "aspect-ratio");
+
+       /* Ignore it. See note in gdata_youtube_video_get_aspect_ratio(),
+        * above. */
 }
 
 /**
@@ -1659,10 +2374,12 @@ gdata_youtube_video_get_coordinates (GDataYouTubeVideo *self, gdouble *latitude,
 {
        g_return_if_fail (GDATA_IS_YOUTUBE_VIDEO (self));
 
-       if (latitude != NULL)
-               *latitude = gdata_georss_where_get_latitude (self->priv->georss_where);
-       if (longitude != NULL)
-               *longitude = gdata_georss_where_get_longitude (self->priv->georss_where);
+       if (latitude != NULL) {
+               *latitude = self->priv->latitude;
+       }
+       if (longitude != NULL) {
+               *longitude = self->priv->longitude;
+       }
 }
 
 /**
@@ -1680,8 +2397,8 @@ gdata_youtube_video_set_coordinates (GDataYouTubeVideo *self, gdouble latitude,
 {
        g_return_if_fail (GDATA_IS_YOUTUBE_VIDEO (self));
 
-       gdata_georss_where_set_latitude (self->priv->georss_where, latitude);
-       gdata_georss_where_set_longitude (self->priv->georss_where, longitude);
+       self->priv->latitude = latitude;
+       self->priv->longitude = longitude;
 
        g_object_freeze_notify (G_OBJECT (self));
        g_object_notify (G_OBJECT (self), "latitude");
diff --git a/gdata/services/youtube/gdata-youtube-video.h b/gdata/services/youtube/gdata-youtube-video.h
index 8fc5ce5..2312cf4 100644
--- a/gdata/services/youtube/gdata-youtube-video.h
+++ b/gdata/services/youtube/gdata-youtube-video.h
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2008-2009 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2008-2009, 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
@@ -97,6 +97,7 @@ G_BEGIN_DECLS
  **/
 #define GDATA_YOUTUBE_ACTION_SYNDICATE "syndicate"
 
+#ifndef LIBGDATA_DISABLE_DEPRECATED
 /**
  * GDATA_YOUTUBE_RATING_TYPE_SIMPLE:
  *
@@ -104,8 +105,12 @@ G_BEGIN_DECLS
  * <code class="literal">adult</code> and <code class="literal">nonadult</code>.
  *
  * Since: 0.10.0
+ * Deprecated: UNRELEASED: No longer supported by Google. Calling
+ *   gdata_youtube_video_get_media_rating() with this rating type will always
+ *   return %NULL.
  */
 #define GDATA_YOUTUBE_RATING_TYPE_SIMPLE "simple"
+#endif /* !LIBGDATA_DISABLE_DEPRECATED */
 
 /**
  * GDATA_YOUTUBE_RATING_TYPE_MPAA:
@@ -201,18 +206,13 @@ gboolean gdata_youtube_video_is_restricted_in_country (GDataYouTubeVideo *self,
 const gchar *gdata_youtube_video_get_media_rating (GDataYouTubeVideo *self, const gchar *rating_type) 
G_GNUC_PURE;
 GDataMediaCategory *gdata_youtube_video_get_category (GDataYouTubeVideo *self) G_GNUC_PURE;
 void gdata_youtube_video_set_category (GDataYouTubeVideo *self, GDataMediaCategory *category);
-GDataYouTubeCredit *gdata_youtube_video_get_credit (GDataYouTubeVideo *self) G_GNUC_PURE;
 const gchar *gdata_youtube_video_get_description (GDataYouTubeVideo *self) G_GNUC_PURE;
 void gdata_youtube_video_set_description (GDataYouTubeVideo *self, const gchar *description);
-GDataYouTubeContent *gdata_youtube_video_look_up_content (GDataYouTubeVideo *self, const gchar *type) 
G_GNUC_PURE;
 GList *gdata_youtube_video_get_thumbnails (GDataYouTubeVideo *self) G_GNUC_PURE;
 guint gdata_youtube_video_get_duration (GDataYouTubeVideo *self) G_GNUC_PURE;
 gboolean gdata_youtube_video_is_private (GDataYouTubeVideo *self) G_GNUC_PURE;
 void gdata_youtube_video_set_is_private (GDataYouTubeVideo *self, gboolean is_private);
 gint64 gdata_youtube_video_get_uploaded (GDataYouTubeVideo *self);
-const gchar *gdata_youtube_video_get_video_id (GDataYouTubeVideo *self) G_GNUC_PURE;
-gboolean gdata_youtube_video_is_draft (GDataYouTubeVideo *self) G_GNUC_PURE;
-void gdata_youtube_video_set_is_draft (GDataYouTubeVideo *self, gboolean is_draft);
 GDataYouTubeState *gdata_youtube_video_get_state (GDataYouTubeVideo *self) G_GNUC_PURE;
 gint64 gdata_youtube_video_get_recorded (GDataYouTubeVideo *self);
 void gdata_youtube_video_set_recorded (GDataYouTubeVideo *self, gint64 recorded);
@@ -223,6 +223,16 @@ void gdata_youtube_video_set_coordinates (GDataYouTubeVideo *self, gdouble latit
 
 gchar *gdata_youtube_video_get_video_id_from_uri (const gchar *video_uri) G_GNUC_WARN_UNUSED_RESULT 
G_GNUC_MALLOC;
 
+#ifndef LIBGDATA_DISABLE_DEPRECATED
+const gchar *gdata_youtube_video_get_video_id (GDataYouTubeVideo *self) G_GNUC_PURE 
G_GNUC_DEPRECATED_FOR(gdata_entry_get_id);
+GDataYouTubeContent *gdata_youtube_video_look_up_content (GDataYouTubeVideo *self, const gchar *type) 
G_GNUC_PURE G_GNUC_DEPRECATED_FOR(gdata_youtube_video_get_player_uri);
+
+gboolean gdata_youtube_video_is_draft (GDataYouTubeVideo *self) G_GNUC_PURE 
G_GNUC_DEPRECATED_FOR(gdata_youtube_video_is_private);
+void gdata_youtube_video_set_is_draft (GDataYouTubeVideo *self, gboolean is_draft) 
G_GNUC_DEPRECATED_FOR(gdata_youtube_video_set_is_private);
+
+GDataYouTubeCredit *gdata_youtube_video_get_credit (GDataYouTubeVideo *self) G_GNUC_PURE G_GNUC_DEPRECATED;
+#endif /* !LIBGDATA_DISABLE_DEPRECATED */
+
 G_END_DECLS
 
 #endif /* !GDATA_YOUTUBE_VIDEO_H */
diff --git a/gdata/tests/youtube.c b/gdata/tests/youtube.c
index 1195bc3..9ced7b4 100644
--- a/gdata/tests/youtube.c
+++ b/gdata/tests/youtube.c
@@ -343,6 +343,8 @@ test_query_standard_feed_with_query (gconstpointer service)
        GDataFeed *feed;
        GError *error = NULL;
 
+       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
        gdata_test_mock_server_start_trace (mock_server, "query-standard-feed-with-query");
 
        query = gdata_youtube_query_new (NULL);
@@ -359,6 +361,8 @@ test_query_standard_feed_with_query (gconstpointer service)
        g_object_unref (feed);
 
        uhm_server_end_trace (mock_server);
+
+       G_GNUC_END_IGNORE_DEPRECATIONS
 }
 
 /* HTTP message responses and the expected associated GData error domain/code. */
@@ -898,7 +902,9 @@ test_parsing_app_control (void)
        g_clear_error (&error);
 
        /* Test the app:control values */
+       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
        g_assert (gdata_youtube_video_is_draft (video) == TRUE);
+       G_GNUC_END_IGNORE_DEPRECATIONS
 
        state = gdata_youtube_video_get_state (video);
        g_assert_cmpstr (gdata_youtube_state_get_name (state), ==, "blacklisted");
@@ -1599,6 +1605,8 @@ test_query_uri (void)
        gchar *query_uri;
        GDataYouTubeQuery *query = gdata_youtube_query_new ("q");
 
+       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
        gdata_youtube_query_set_format (query, GDATA_YOUTUBE_FORMAT_RTSP_H263_AMR);
        g_assert_cmpuint (gdata_youtube_query_get_format (query), ==, 1);
 
@@ -1671,6 +1679,8 @@ test_query_uri (void)
        g_free (query_uri);
 
        g_object_unref (query);
+
+       G_GNUC_END_IGNORE_DEPRECATIONS
 }
 
 static void
@@ -1681,6 +1691,8 @@ test_query_etag (void)
        /* Test that setting any property will unset the ETag */
        g_test_bug ("613529");
 
+       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+
 #define CHECK_ETAG(C) \
        gdata_query_set_etag (GDATA_QUERY (query), "foobar");           \
        (C);                                                            \
@@ -1700,6 +1712,8 @@ test_query_etag (void)
 #undef CHECK_ETAG
 
        g_object_unref (query);
+
+       G_GNUC_END_IGNORE_DEPRECATIONS
 }
 
 static void
@@ -1718,8 +1732,12 @@ test_query_single (gconstpointer service)
        g_assert_no_error (error);
        g_assert (video != NULL);
        g_assert (GDATA_IS_YOUTUBE_VIDEO (video));
+
+       G_GNUC_BEGIN_IGNORE_DEPRECATIONS
        g_assert_cmpstr (gdata_youtube_video_get_video_id (video), ==, "_LeQuMpwbW4");
        g_assert_cmpstr (gdata_entry_get_id (GDATA_ENTRY (video)), ==, 
"tag:youtube.com,2008:video:_LeQuMpwbW4");
+       G_GNUC_END_IGNORE_DEPRECATIONS
+
        g_clear_error (&error);
 
        g_object_unref (video);
@@ -1739,9 +1757,11 @@ G_STMT_START {
        video = GDATA_YOUTUBE_VIDEO (gdata_service_query_single_entry_finish (GDATA_SERVICE (obj), 
async_result, &error));
 
        if (error == NULL) {
+               G_GNUC_BEGIN_IGNORE_DEPRECATIONS
                g_assert (GDATA_IS_YOUTUBE_VIDEO (video));
                g_assert_cmpstr (gdata_youtube_video_get_video_id (video), ==, "_LeQuMpwbW4");
                g_assert_cmpstr (gdata_entry_get_id (GDATA_ENTRY (video)), ==, 
"tag:youtube.com,2008:video:_LeQuMpwbW4");
+               G_GNUC_END_IGNORE_DEPRECATIONS
 
                g_object_unref (video);
        } else {



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]