[libgdata] [picasaweb] Added a PicasaWeb service



commit 0f9d29fdbb87e6bb2a4a0e5d239e6e8a8f8fe3f3
Author: Philip Withnall <philip tecnocode co uk>
Date:   Sun Jun 21 20:24:34 2009 +0100

    [picasaweb] Added a PicasaWeb service
    
    Originally written by Richard Schwarting <aquarichy gmail com>, the PicasaWeb
    service has decent test suite coverage, decent documentation, and support for
    common PicasaWeb operations. Closes: bgo#580375

 configure.in                                       |    1 +
 docs/reference/Makefile.am                         |    3 +-
 docs/reference/gdata-docs.xml                      |    8 +
 docs/reference/gdata-sections.txt                  |  141 +++
 gdata/Makefile.am                                  |    1 +
 gdata/gdata-access-rule.c                          |    6 +-
 gdata/gdata-entry.c                                |   74 ++-
 gdata/gdata-entry.h                                |    2 +
 gdata/gdata-feed.c                                 |    3 +-
 gdata/gdata-parser.c                               |   17 +
 gdata/gdata-parser.h                               |    1 +
 gdata/gdata-service.c                              |    1 +
 gdata/gdata.h                                      |    7 +
 gdata/gdata.symbols                                |   83 ++
 gdata/media/gdata-media-content.c                  |   12 +-
 gdata/media/gdata-media-credit.c                   |    6 +-
 gdata/media/gdata-media-group.c                    |   21 +-
 gdata/media/gdata-media-group.h                    |    1 +
 gdata/services/Makefile.am                         |    2 +-
 gdata/services/picasaweb/Makefile.am               |   69 +
 gdata/services/picasaweb/gdata-picasaweb-album.c   | 1097 ++++++++++++++++
 gdata/services/picasaweb/gdata-picasaweb-album.h   |  109 ++
 gdata/services/picasaweb/gdata-picasaweb-file.c    | 1303 ++++++++++++++++++++
 gdata/services/picasaweb/gdata-picasaweb-file.h    |  110 ++
 gdata/services/picasaweb/gdata-picasaweb-query.c   |  547 ++++++++
 gdata/services/picasaweb/gdata-picasaweb-query.h   |   84 ++
 gdata/services/picasaweb/gdata-picasaweb-service.c |  397 ++++++
 gdata/services/picasaweb/gdata-picasaweb-service.h |   87 ++
 gdata/tests/Makefile.am                            |    3 +
 gdata/tests/picasaweb.c                            |  554 +++++++++
 po/POTFILES.in                                     |    1 +
 31 files changed, 4736 insertions(+), 15 deletions(-)
---
diff --git a/configure.in b/configure.in
index fbbf593..49ab4f6 100644
--- a/configure.in
+++ b/configure.in
@@ -92,6 +92,7 @@ gdata/services/Makefile
 gdata/services/calendar/Makefile
 gdata/services/contacts/Makefile
 gdata/services/youtube/Makefile
+gdata/services/picasaweb/Makefile
 gdata/tests/Makefile
 po/Makefile.in
 docs/Makefile
diff --git a/docs/reference/Makefile.am b/docs/reference/Makefile.am
index 1602c35..2f2ac2f 100644
--- a/docs/reference/Makefile.am
+++ b/docs/reference/Makefile.am
@@ -62,7 +62,8 @@ IGNORE_HFILES = \
 	gdata-youtube-group.c	\
 	gdata-youtube-group.h	\
 	gdata-youtube-control.c	\
-	gdata-youtube-control.h
+	gdata-youtube-control.h	\
+	gdata-picasaweb-enums.h
 
 # Images to copy into HTML directory.
 # e.g. HTML_IMAGES=$(top_srcdir)/gtk/stock-icons/stock_about_24.png
diff --git a/docs/reference/gdata-docs.xml b/docs/reference/gdata-docs.xml
index b6e3d57..2e9daa4 100644
--- a/docs/reference/gdata-docs.xml
+++ b/docs/reference/gdata-docs.xml
@@ -103,6 +103,14 @@
 			<xi:include href="xml/gdata-contacts-query.xml"/>
 			<xi:include href="xml/gdata-contacts-contact.xml"/>
 		</chapter>
+
+		<chapter>
+			<title>Google PicasaWeb API</title>
+			<xi:include href="xml/gdata-picasaweb-service.xml"/>
+			<xi:include href="xml/gdata-picasaweb-query.xml"/>
+			<xi:include href="xml/gdata-picasaweb-album.xml"/>
+			<xi:include href="xml/gdata-picasaweb-file.xml"/>
+		</chapter>
 	</part>
 
 	<part>
diff --git a/docs/reference/gdata-sections.txt b/docs/reference/gdata-sections.txt
index efc928f..8b22ce2 100644
--- a/docs/reference/gdata-sections.txt
+++ b/docs/reference/gdata-sections.txt
@@ -135,6 +135,8 @@ gdata_entry_new
 gdata_entry_new_from_xml
 gdata_entry_get_title
 gdata_entry_set_title
+gdata_entry_get_summary
+gdata_entry_set_summary
 gdata_entry_get_id
 gdata_entry_get_etag
 gdata_entry_get_content
@@ -1115,3 +1117,142 @@ GDATA_TYPE_YOUTUBE_STATE
 <SUBSECTION Private>
 GDataYouTubeStatePrivate
 </SECTION>
+
+<SECTION>
+<FILE>gdata-picasaweb-service</FILE>
+<TITLE>GDataPicasaWebService</TITLE>
+GDataPicasaWebService
+GDataPicasaWebServiceClass
+gdata_picasaweb_service_new
+gdata_picasaweb_service_query_all_albums
+gdata_picasaweb_service_query_all_albums_async
+gdata_picasaweb_service_query_files
+gdata_picasaweb_service_upload_file
+<SUBSECTION Standard>
+gdata_picasaweb_service_get_type
+GDATA_IS_PICASAWEB_SERVICE
+GDATA_IS_PICASAWEB_SERVICE_CLASS
+GDATA_PICASAWEB_SERVICE
+GDATA_PICASAWEB_SERVICE_CLASS
+GDATA_PICASAWEB_SERVICE_GET_CLASS
+GDATA_TYPE_PICASAWEB_SERVICE
+</SECTION>
+
+<SECTION>
+<FILE>gdata-picasaweb-query</FILE>
+<TITLE>GDataPicasaWebQuery</TITLE>
+GDataPicasaWebQuery
+GDataPicasaWebQueryClass
+gdata_picasaweb_query_new
+gdata_picasaweb_query_get_visibility
+gdata_picasaweb_query_set_visibility
+gdata_picasaweb_query_get_thumbnail_size
+gdata_picasaweb_query_set_thumbnail_size
+gdata_picasaweb_query_get_image_size
+gdata_picasaweb_query_set_image_size
+gdata_picasaweb_query_get_tag
+gdata_picasaweb_query_set_tag
+gdata_picasaweb_query_get_bounding_box
+gdata_picasaweb_query_set_bounding_box
+gdata_picasaweb_query_get_location
+gdata_picasaweb_query_set_location
+<SUBSECTION Standard>
+gdata_picasaweb_query_get_type
+GDATA_IS_PICASAWEB_QUERY
+GDATA_IS_PICASAWEB_QUERY_CLASS
+GDATA_PICASAWEB_QUERY
+GDATA_PICASAWEB_QUERY_CLASS
+GDATA_PICASAWEB_QUERY_GET_CLASS
+GDATA_TYPE_PICASAWEB_QUERY
+<SUBSECTION Private>
+GDataPicasaWebQueryPrivate
+</SECTION>
+
+<SECTION>
+<FILE>gdata-picasaweb-album</FILE>
+<TITLE>GDataPicasaWebAlbum</TITLE>
+GDataPicasaWebAlbum
+GDataPicasaWebAlbumClass
+GDataPicasaWebVisibility
+gdata_picasaweb_album_new
+gdata_picasaweb_album_new_from_xml
+gdata_picasaweb_album_get_user
+gdata_picasaweb_album_get_nickname
+gdata_picasaweb_album_get_edited
+gdata_picasaweb_album_get_name
+gdata_picasaweb_album_get_location
+gdata_picasaweb_album_set_location
+gdata_picasaweb_album_get_visibility
+gdata_picasaweb_album_set_visibility
+gdata_picasaweb_album_get_timestamp
+gdata_picasaweb_album_set_timestamp
+gdata_picasaweb_album_get_num_photos
+gdata_picasaweb_album_get_num_photos_remaining
+gdata_picasaweb_album_get_bytes_used
+gdata_picasaweb_album_is_commenting_enabled
+gdata_picasaweb_album_set_is_commenting_enabled
+gdata_picasaweb_album_get_comment_count
+gdata_picasaweb_album_get_tags
+gdata_picasaweb_album_set_tags
+gdata_picasaweb_album_get_description
+gdata_picasaweb_album_set_description
+gdata_picasaweb_album_get_contents
+gdata_picasaweb_album_get_thumbnails
+<SUBSECTION Standard>
+gdata_picasaweb_album_get_type
+GDATA_IS_PICASAWEB_ALBUM
+GDATA_IS_PICASAWEB_ALBUM_CLASS
+GDATA_PICASAWEB_ALBUM
+GDATA_PICASAWEB_ALBUM_CLASS
+GDATA_PICASAWEB_ALBUM_GET_CLASS
+GDATA_TYPE_PICASAWEB_ALBUM
+<SUBSECTION Private>
+GDataPicasaWebAlbumPrivate
+</SECTION>
+
+<SECTION>
+<FILE>gdata-picasaweb-file</FILE>
+<TITLE>GDataPicasaWebFile</TITLE>
+GDataPicasaWebFile
+GDataPicasaWebFileClass
+gdata_picasaweb_file_new
+gdata_picasaweb_file_new_from_xml
+gdata_picasaweb_file_get_edited
+gdata_picasaweb_file_get_version
+gdata_picasaweb_file_get_position
+gdata_picasaweb_file_set_position
+gdata_picasaweb_file_get_album_id
+gdata_picasaweb_file_set_album_id
+gdata_picasaweb_file_get_width
+gdata_picasaweb_file_get_height
+gdata_picasaweb_file_get_size
+gdata_picasaweb_file_get_client
+gdata_picasaweb_file_set_client
+gdata_picasaweb_file_get_checksum
+gdata_picasaweb_file_set_checksum
+gdata_picasaweb_file_get_timestamp
+gdata_picasaweb_file_set_timestamp
+gdata_picasaweb_file_is_commenting_enabled
+gdata_picasaweb_file_set_is_commenting_enabled
+gdata_picasaweb_file_get_comment_count
+gdata_picasaweb_file_get_rotation
+gdata_picasaweb_file_set_rotation
+gdata_picasaweb_file_get_video_status
+gdata_picasaweb_file_get_tags
+gdata_picasaweb_file_set_tags
+gdata_picasaweb_file_get_credit
+gdata_picasaweb_file_get_caption
+gdata_picasaweb_file_set_caption
+gdata_picasaweb_file_get_contents
+gdata_picasaweb_file_get_thumbnails
+<SUBSECTION Standard>
+gdata_picasaweb_file_get_type
+GDATA_IS_PICASAWEB_FILE
+GDATA_IS_PICASAWEB_FILE_CLASS
+GDATA_PICASAWEB_FILE
+GDATA_PICASAWEB_FILE_CLASS
+GDATA_PICASAWEB_FILE_GET_CLASS
+GDATA_TYPE_PICASAWEB_FILE
+<SUBSECTION Private>
+GDataPicasaWebFilePrivate
+</SECTION>
diff --git a/gdata/Makefile.am b/gdata/Makefile.am
index ed03465..e3a085f 100644
--- a/gdata/Makefile.am
+++ b/gdata/Makefile.am
@@ -93,6 +93,7 @@ libgdata_la_LIBADD = \
 	media/libgdatamedia.la			\
 	services/youtube/libgdatayoutube.la	\
 	services/calendar/libgdatacalendar.la	\
+	services/picasaweb/libgdatapicasaweb.la	\
 	services/contacts/libgdatacontacts.la
 
 libgdata_la_LDFLAGS = \
diff --git a/gdata/gdata-access-rule.c b/gdata/gdata-access-rule.c
index 45852b6..1778967 100644
--- a/gdata/gdata-access-rule.c
+++ b/gdata/gdata-access-rule.c
@@ -88,7 +88,7 @@ gdata_access_rule_class_init (GDataAccessRuleClass *klass)
 				g_param_spec_string ("role",
 					"Role", "The role of the person concerned by this ACL.",
 					NULL,
-					G_PARAM_READWRITE ));
+					G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 
 	/**
 	 * GDataAccessRule:scope-type:
@@ -101,7 +101,7 @@ gdata_access_rule_class_init (GDataAccessRuleClass *klass)
 				g_param_spec_string ("scope-type",
 					"Scope type", "Specifies to whom this access rule applies.",
 					NULL,
-					G_PARAM_READWRITE ));
+					G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 
 	/**
 	 * GDataAccessRule:scope-value:
@@ -115,7 +115,7 @@ gdata_access_rule_class_init (GDataAccessRuleClass *klass)
 				g_param_spec_string ("scope-value",
 					"Scope value", "The scope value for this access rule.",
 					NULL,
-					G_PARAM_READWRITE ));
+					G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 }
 
 /**
diff --git a/gdata/gdata-entry.c b/gdata/gdata-entry.c
index e1a15bb..9fed168 100644
--- a/gdata/gdata-entry.c
+++ b/gdata/gdata-entry.c
@@ -55,6 +55,7 @@ static void get_namespaces (GDataParsable *parsable, GHashTable *namespaces);
 
 struct _GDataEntryPrivate {
 	gchar *title;
+	gchar *summary;
 	gchar *id;
 	gchar *etag;
 	GTimeVal updated;
@@ -67,6 +68,7 @@ struct _GDataEntryPrivate {
 
 enum {
 	PROP_TITLE = 1,
+	PROP_SUMMARY,
 	PROP_ETAG,
 	PROP_ID,
 	PROP_UPDATED,
@@ -86,8 +88,8 @@ gdata_entry_class_init (GDataEntryClass *klass)
 
 	g_type_class_add_private (klass, sizeof (GDataEntryPrivate));
 
-	gobject_class->set_property = gdata_entry_set_property;
 	gobject_class->get_property = gdata_entry_get_property;
+	gobject_class->set_property = gdata_entry_set_property;
 	gobject_class->dispose = gdata_entry_dispose;
 	gobject_class->finalize = gdata_entry_finalize;
 
@@ -103,6 +105,23 @@ gdata_entry_class_init (GDataEntryClass *klass)
 					"Title", "The title for this entry.",
 					NULL,
 					G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataEntry:summary:
+	 *
+	 * A short summary, abstract, or excerpt of the entry.
+	 *
+	 * For more information, see the <ulink type="http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.summary";>
+	 * Atom specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_SUMMARY,
+				g_param_spec_string ("summary",
+					"Summary", "A short summary, abstract, or excerpt of the entry.",
+					NULL,
+					G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
 	g_object_class_install_property (gobject_class, PROP_ID,
 				g_param_spec_string ("id",
 					"ID", "The ID for this entry.",
@@ -175,6 +194,7 @@ gdata_entry_finalize (GObject *object)
 	GDataEntryPrivate *priv = GDATA_ENTRY_GET_PRIVATE (object);
 
 	g_free (priv->title);
+	g_free (priv->summary);
 	xmlFree (priv->id);
 	xmlFree (priv->etag);
 	g_free (priv->content);
@@ -192,6 +212,9 @@ gdata_entry_get_property (GObject *object, guint property_id, GValue *value, GPa
 		case PROP_TITLE:
 			g_value_set_string (value, priv->title);
 			break;
+		case PROP_SUMMARY:
+			g_value_set_string (value, priv->summary);
+			break;
 		case PROP_ID:
 			g_value_set_string (value, priv->id);
 			break;
@@ -234,6 +257,9 @@ gdata_entry_set_property (GObject *object, guint property_id, const GValue *valu
 		case PROP_TITLE:
 			gdata_entry_set_title (self, g_value_get_string (value));
 			break;
+		case PROP_SUMMARY:
+			gdata_entry_set_summary (self, g_value_get_string (value));
+			break;
 		case PROP_CONTENT:
 			gdata_entry_set_content (self, g_value_get_string (value));
 			break;
@@ -334,6 +360,11 @@ parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_da
 			return FALSE;
 
 		self->priv->authors = g_list_prepend (self->priv->authors, author);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "summary") == 0) {
+		/* atom:summary */
+		xmlChar *summary = xmlNodeListGetString (doc, node->children, TRUE);
+		gdata_entry_set_summary (self, (gchar*) summary);
+		xmlFree (summary);
 	} else if (GDATA_PARSABLE_CLASS (gdata_entry_parent_class)->parse_xml (parsable, doc, node, user_data, error) == FALSE) {
 		/* Error! */
 		return FALSE;
@@ -409,6 +440,12 @@ get_xml (GDataParsable *parsable, GString *xml_string)
 		g_free (published);
 	}
 
+	if (priv->summary != NULL) {
+		gchar *summary = g_markup_escape_text (priv->summary, -1);
+		g_string_append_printf (xml_string, "<summary type='text'>%s</summary>", summary);
+		g_free (summary);
+	}
+
 	if (priv->content != NULL) {
 		gchar *content = g_markup_escape_text (priv->content, -1);
 		g_string_append_printf (xml_string, "<content type='text'>%s</content>", content);
@@ -497,6 +534,41 @@ gdata_entry_set_title (GDataEntry *self, const gchar *title)
 }
 
 /**
+ * gdata_entry_get_summary:
+ * @self: a #GDataEntry
+ *
+ * Returns the summary of the entry.
+ *
+ * Return value: the entry's summary, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_entry_get_summary (GDataEntry *self)
+{
+	g_return_val_if_fail (GDATA_IS_ENTRY (self), NULL);
+	return self->priv->summary;
+}
+
+/**
+ * gdata_entry_set_summary:
+ * @self: a #GDataEntry
+ * @summary: the new entry summary, or %NULL
+ *
+ * Sets the summary of the entry.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_entry_set_summary (GDataEntry *self, const gchar *summary)
+{
+	g_return_if_fail (GDATA_IS_ENTRY (self));
+	g_free (self->priv->summary);
+	self->priv->summary = g_strdup (summary);
+	g_object_notify (G_OBJECT (self), "summary");
+}
+
+/**
  * gdata_entry_get_id:
  * @self: a #GDataEntry
  *
diff --git a/gdata/gdata-entry.h b/gdata/gdata-entry.h
index d8d190d..46329e3 100644
--- a/gdata/gdata-entry.h
+++ b/gdata/gdata-entry.h
@@ -66,6 +66,8 @@ GDataEntry *gdata_entry_new_from_xml (const gchar *xml, gint length, GError **er
 
 const gchar *gdata_entry_get_title (GDataEntry *self);
 void gdata_entry_set_title (GDataEntry *self, const gchar *title);
+const gchar *gdata_entry_get_summary (GDataEntry *self);
+void gdata_entry_set_summary (GDataEntry *self, const gchar *summary);
 const gchar *gdata_entry_get_id (GDataEntry *self);
 const gchar *gdata_entry_get_etag (GDataEntry *self);
 void gdata_entry_get_updated (GDataEntry *self, GTimeVal *updated);
diff --git a/gdata/gdata-feed.c b/gdata/gdata-feed.c
index d096453..2b6a9c4 100644
--- a/gdata/gdata-feed.c
+++ b/gdata/gdata-feed.c
@@ -432,8 +432,9 @@ parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_da
 			return gdata_parser_error_duplicate_element (node, error);
 
 		self->priv->subtitle = (gchar*) xmlNodeListGetString (doc, node->children, TRUE);
-	} else if (xmlStrcmp (node->name, (xmlChar*) "id") == 0) {
+	} else if (xmlStrcmp (node->name, (xmlChar*) "id") == 0 && xmlStrcmp (node->ns->href, (xmlChar*) "http://www.w3.org/2005/Atom";) == 0) {
 		/* atom:id */
+		/* The namespace check is necessary because there's an "id" element in the gphoto namespace (PicasaWeb service) */
 		if (self->priv->id != NULL)
 			return gdata_parser_error_duplicate_element (node, error);
 
diff --git a/gdata/gdata-parser.c b/gdata/gdata-parser.c
index 2adc56f..dd241e5 100644
--- a/gdata/gdata-parser.c
+++ b/gdata/gdata-parser.c
@@ -106,6 +106,23 @@ gdata_parser_error_unknown_property_value (xmlNode *element, const gchar *proper
 }
 
 gboolean
+gdata_parser_error_unknown_content (xmlNode *element, const gchar *actual_content, GError **error)
+{
+	gchar *element_string = print_element (element);
+
+	g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
+		     /* Translators: the first parameter is the name of an XML element (including the angle brackets ("<" and ">")),
+		      * and the second parameter is the unknown content of that element.
+		      *
+		      * For example:
+		      *  The content of a <gphoto:access> element ("protected") was unknown. */
+		     _("The content of a %s element (\"%s\") was unknown."), element_string, actual_content);
+	g_free (element_string);
+
+	return FALSE;
+}
+
+gboolean
 gdata_parser_error_required_property_missing (xmlNode *element, const gchar *property_name, GError **error)
 {
 	gchar *property_string, *element_string;
diff --git a/gdata/gdata-parser.h b/gdata/gdata-parser.h
index 61b4708..b794c5c 100644
--- a/gdata/gdata-parser.h
+++ b/gdata/gdata-parser.h
@@ -27,6 +27,7 @@ G_BEGIN_DECLS
 gboolean gdata_parser_error_required_content_missing (xmlNode *element, GError **error);
 gboolean gdata_parser_error_not_iso8601_format (xmlNode *element, const gchar *actual_value, GError **error);
 gboolean gdata_parser_error_unknown_property_value (xmlNode *element, const gchar *property_name, const gchar *actual_value, GError **error);
+gboolean gdata_parser_error_unknown_content (xmlNode *element, const gchar *actual_content, GError **error);
 gboolean gdata_parser_error_required_property_missing (xmlNode *element, const gchar *property_name, GError **error);
 gboolean gdata_parser_error_required_element_missing (const gchar *element_name, const gchar *parent_element_name, GError **error);
 gboolean gdata_parser_error_duplicate_element (xmlNode *element, GError **error);
diff --git a/gdata/gdata-service.c b/gdata/gdata-service.c
index 48114fc..d94d0ca 100644
--- a/gdata/gdata-service.c
+++ b/gdata/gdata-service.c
@@ -896,6 +896,7 @@ gdata_service_query_async (GDataService *self, const gchar *feed_uri, GDataQuery
 	g_return_if_fail (GDATA_IS_SERVICE (self));
 	g_return_if_fail (feed_uri != NULL);
 	g_return_if_fail (entry_type != G_TYPE_INVALID);
+	g_return_if_fail (callback != NULL);
 
 	data = g_slice_new (QueryAsyncData);
 	data->feed_uri = g_strdup (feed_uri);
diff --git a/gdata/gdata.h b/gdata/gdata.h
index 5275fe6..1b58c7e 100644
--- a/gdata/gdata.h
+++ b/gdata/gdata.h
@@ -75,6 +75,13 @@
 #include <gdata/services/calendar/gdata-calendar-event.h>
 #include <gdata/services/calendar/gdata-calendar-query.h>
 
+/* Google PicasaWeb */
+#include <gdata/services/picasaweb/gdata-picasaweb-service.h>
+#include <gdata/services/picasaweb/gdata-picasaweb-query.h>
+#include <gdata/services/picasaweb/gdata-picasaweb-album.h>
+#include <gdata/services/picasaweb/gdata-picasaweb-file.h>
+#include <gdata/services/picasaweb/gdata-picasaweb-enums.h>
+
 /* Google Contacts */
 #include <gdata/services/contacts/gdata-contacts-service.h>
 #include <gdata/services/contacts/gdata-contacts-contact.h>
diff --git a/gdata/gdata.symbols b/gdata/gdata.symbols
index a6f4adf..17446fc 100644
--- a/gdata/gdata.symbols
+++ b/gdata/gdata.symbols
@@ -6,6 +6,8 @@ gdata_entry_new
 gdata_entry_new_from_xml
 gdata_entry_get_title
 gdata_entry_set_title
+gdata_entry_get_summary
+gdata_entry_set_summary
 gdata_entry_get_id
 gdata_entry_get_etag
 gdata_entry_get_updated
@@ -242,6 +244,7 @@ gdata_calendar_service_insert_event
 gdata_service_error_get_type
 gdata_authentication_error_get_type
 gdata_media_expression_get_type
+gdata_media_medium_get_type
 gdata_parser_error_get_type
 gdata_parser_error_quark
 gdata_contacts_service_get_type
@@ -481,3 +484,83 @@ gdata_youtube_state_get_name
 gdata_youtube_state_get_reason_code
 gdata_youtube_state_get_help_uri
 gdata_youtube_state_get_message
+gdata_picasaweb_album_get_type
+gdata_picasaweb_album_new
+gdata_picasaweb_album_new_from_xml
+gdata_picasaweb_album_get_user
+gdata_picasaweb_album_get_nickname
+gdata_picasaweb_album_get_edited
+gdata_picasaweb_album_get_name
+gdata_picasaweb_album_get_location
+gdata_picasaweb_album_set_location
+gdata_picasaweb_album_get_visibility
+gdata_picasaweb_album_set_visibility
+gdata_picasaweb_album_get_timestamp
+gdata_picasaweb_album_set_timestamp
+gdata_picasaweb_album_get_num_photos
+gdata_picasaweb_album_get_num_photos_remaining
+gdata_picasaweb_album_get_bytes_used
+gdata_picasaweb_album_is_commenting_enabled
+gdata_picasaweb_album_set_is_commenting_enabled
+gdata_picasaweb_album_get_comment_count
+gdata_picasaweb_album_get_tags
+gdata_picasaweb_album_set_tags
+gdata_picasaweb_album_get_description
+gdata_picasaweb_album_set_description
+gdata_picasaweb_album_get_contents
+gdata_picasaweb_album_get_thumbnails
+gdata_picasaweb_file_get_type
+gdata_picasaweb_file_new
+gdata_picasaweb_file_new_from_xml
+gdata_picasaweb_file_get_edited
+gdata_picasaweb_file_get_version
+gdata_picasaweb_file_get_position
+gdata_picasaweb_file_set_position
+gdata_picasaweb_file_get_album_id
+gdata_picasaweb_file_set_album_id
+gdata_picasaweb_file_get_width
+gdata_picasaweb_file_get_height
+gdata_picasaweb_file_get_size
+gdata_picasaweb_file_get_client
+gdata_picasaweb_file_set_client
+gdata_picasaweb_file_get_checksum
+gdata_picasaweb_file_set_checksum
+gdata_picasaweb_file_get_timestamp
+gdata_picasaweb_file_set_timestamp
+gdata_picasaweb_file_is_commenting_enabled
+gdata_picasaweb_file_set_is_commenting_enabled
+gdata_picasaweb_file_get_comment_count
+gdata_picasaweb_file_get_rotation
+gdata_picasaweb_file_set_rotation
+gdata_picasaweb_file_get_video_status
+gdata_picasaweb_file_get_tags
+gdata_picasaweb_file_set_tags
+gdata_picasaweb_file_get_credit
+gdata_picasaweb_file_get_caption
+gdata_picasaweb_file_set_caption
+gdata_picasaweb_file_get_contents
+gdata_picasaweb_file_get_thumbnails
+gdata_picasaweb_query_get_type
+gdata_picasaweb_query_new
+gdata_picasaweb_query_new_with_limits
+gdata_picasaweb_service_get_type
+gdata_picasaweb_service_new
+gdata_picasaweb_service_query_all_albums
+gdata_picasaweb_service_query_all_albums_async
+gdata_picasaweb_service_query_files
+gdata_picasaweb_service_upload_file
+gdata_picasaweb_query_get_type
+gdata_picasaweb_query_new
+gdata_picasaweb_query_get_visibility
+gdata_picasaweb_query_set_visibility
+gdata_picasaweb_query_get_thumbnail_size
+gdata_picasaweb_query_set_thumbnail_size
+gdata_picasaweb_query_get_image_size
+gdata_picasaweb_query_set_image_size
+gdata_picasaweb_query_get_tag
+gdata_picasaweb_query_set_tag
+gdata_picasaweb_query_get_bounding_box
+gdata_picasaweb_query_set_bounding_box
+gdata_picasaweb_query_get_location
+gdata_picasaweb_query_set_location
+gdata_picasaweb_visibility_get_type
diff --git a/gdata/media/gdata-media-content.c b/gdata/media/gdata-media-content.c
index 18d0e01..de10bdf 100644
--- a/gdata/media/gdata-media-content.c
+++ b/gdata/media/gdata-media-content.c
@@ -182,7 +182,7 @@ gdata_media_content_class_init (GDataMediaContentClass *klass)
 	 *
 	 * Since: 0.4.0
 	 **/
-	g_object_class_install_property (gobject_class, PROP_EXPRESSION,
+	g_object_class_install_property (gobject_class, PROP_DURATION,
 				g_param_spec_int64 ("duration",
 					"Duration", "The number of seconds for which the media object plays.",
 					0, G_MAXINT64, 0,
@@ -304,10 +304,10 @@ pre_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *root_node, gpointe
 
 	/* Parse expression */
 	expression = xmlGetProp (root_node, (xmlChar*) "expression");
-	if (xmlStrcmp (expression, (xmlChar*) "sample") == 0)
-		expression_enum = GDATA_MEDIA_EXPRESSION_SAMPLE;
-	else if (xmlStrcmp (expression, (xmlChar*) "full") == 0)
+	if (expression == NULL || xmlStrcmp (expression, (xmlChar*) "full") == 0)
 		expression_enum = GDATA_MEDIA_EXPRESSION_FULL;
+	else if (xmlStrcmp (expression, (xmlChar*) "sample") == 0)
+		expression_enum = GDATA_MEDIA_EXPRESSION_SAMPLE;
 	else if (xmlStrcmp (expression, (xmlChar*) "nonstop") == 0)
 		expression_enum = GDATA_MEDIA_EXPRESSION_NONSTOP;
 	else {
@@ -319,7 +319,9 @@ pre_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *root_node, gpointe
 
 	/* Parse medium */
 	medium = xmlGetProp (root_node, (xmlChar*) "medium");
-	if (xmlStrcmp (medium, (xmlChar*) "image") == 0)
+	if (medium == NULL)
+		medium_enum = GDATA_MEDIA_UNKNOWN;
+	else if (xmlStrcmp (medium, (xmlChar*) "image") == 0)
 		medium_enum = GDATA_MEDIA_IMAGE;
 	else if (xmlStrcmp (medium, (xmlChar*) "audio") == 0)
 		medium_enum = GDATA_MEDIA_AUDIO;
diff --git a/gdata/media/gdata-media-credit.c b/gdata/media/gdata-media-credit.c
index 768d587..5af0cbd 100644
--- a/gdata/media/gdata-media-credit.c
+++ b/gdata/media/gdata-media-credit.c
@@ -188,8 +188,10 @@ pre_parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *root_node, gpointe
 	priv->role = g_strdup ((gchar*) role);
 
 	/* Convert the role to lower case */
-	for (i = 0; priv->role[i] != '\0'; i++)
-		priv->role[i] = g_ascii_tolower (priv->role[i]);
+	if (priv->role != NULL) {
+		for (i = 0; priv->role[i] != '\0'; i++)
+			priv->role[i] = g_ascii_tolower (priv->role[i]);
+	}
 
 	xmlFree (credit);
 	xmlFree (scheme);
diff --git a/gdata/media/gdata-media-group.c b/gdata/media/gdata-media-group.c
index 0b75459..f2d66c2 100644
--- a/gdata/media/gdata-media-group.c
+++ b/gdata/media/gdata-media-group.c
@@ -263,7 +263,11 @@ get_xml (GDataParsable *parsable, GString *xml_string)
 	GDataMediaGroupPrivate *priv = GDATA_MEDIA_GROUP (parsable)->priv;
 
 	/* Media category */
-	g_string_append (xml_string, _gdata_parsable_get_xml (GDATA_PARSABLE (priv->category), "media:category", FALSE));
+	if (priv->category != NULL) {
+		gchar *xml = _gdata_parsable_get_xml (GDATA_PARSABLE (priv->category), "media:category", FALSE);
+		g_string_append (xml_string, xml);
+		g_free (xml);
+	}
 
 	if (priv->title != NULL) {
 		gchar *title = g_markup_escape_text (priv->title, -1);
@@ -462,6 +466,21 @@ gdata_media_group_look_up_content (GDataMediaGroup *self, const gchar *type)
 	return GDATA_MEDIA_CONTENT (element->data);
 }
 
+/**
+ * gdata_media_group_get_contents:
+ * @self: a #GDataMediaGroup
+ *
+ * Returns a list of #GDataMediaContent<!-- -->s, giving the content enclosed by the group.
+ *
+ * Return value: a #GList of #GDataMediaContent<!-- -->s,  or %NULL
+ **/
+GList *
+gdata_media_group_get_contents (GDataMediaGroup *self)
+{
+	g_return_val_if_fail (GDATA_IS_MEDIA_GROUP (self), NULL);
+	return self->priv->contents;
+}
+
 void
 _gdata_media_group_add_content (GDataMediaGroup *self, GDataMediaContent *content)
 {
diff --git a/gdata/media/gdata-media-group.h b/gdata/media/gdata-media-group.h
index 17adb0f..58912cc 100644
--- a/gdata/media/gdata-media-group.h
+++ b/gdata/media/gdata-media-group.h
@@ -72,6 +72,7 @@ void gdata_media_group_set_keywords (GDataMediaGroup *self, const gchar *keyword
 GDataMediaCategory *gdata_media_group_get_category (GDataMediaGroup *self);
 void gdata_media_group_set_category (GDataMediaGroup *self, GDataMediaCategory *category);
 GDataMediaContent *gdata_media_group_look_up_content (GDataMediaGroup *self, const gchar *type);
+GList *gdata_media_group_get_contents (GDataMediaGroup *self);
 void _gdata_media_group_add_content (GDataMediaGroup *self, GDataMediaContent *content);
 GDataMediaCredit *gdata_media_group_get_credit (GDataMediaGroup *self);
 void _gdata_media_group_set_credit (GDataMediaGroup *self, GDataMediaCredit *credit);
diff --git a/gdata/services/Makefile.am b/gdata/services/Makefile.am
index 6d224f1..87d94f1 100644
--- a/gdata/services/Makefile.am
+++ b/gdata/services/Makefile.am
@@ -1,3 +1,3 @@
-SUBDIRS = youtube calendar contacts
+SUBDIRS = youtube calendar contacts picasaweb
 
 -include $(top_srcdir)/git.mk
diff --git a/gdata/services/picasaweb/Makefile.am b/gdata/services/picasaweb/Makefile.am
new file mode 100644
index 0000000..9b7b32e
--- /dev/null
+++ b/gdata/services/picasaweb/Makefile.am
@@ -0,0 +1,69 @@
+# Enums
+GDATA_PICASAWEB_ENUM_FILES = \
+	gdata-picasaweb-enums.c	\
+	gdata-picasaweb-enums.h
+
+gdata-picasaweb-enums.h: $(gdatapicasawebinclude_HEADERS) Makefile
+	(cd $(srcdir) && $(GLIB_MKENUMS) \
+			--fhead "#ifndef GDATA_PICASAWEB_ENUMS_H\n#define GDATA_PICASAWEB_ENUMS_H\n\n#include <glib-object.h>\n\nG_BEGIN_DECLS\n" \
+			--fprod "/* enumerations from \"@filename \" */\n" \
+			--vhead "GType @enum_name _get_type (void) G_GNUC_CONST;\n#define GDATA_TYPE_ ENUMSHORT@ (@enum_name _get_type())\n" \
+			--ftail "G_END_DECLS\n\n#endif /* !GDATA_PICASAWEB_ENUMS_H */" $(gdatapicasawebinclude_HEADERS)) > gdata-picasaweb-enums.h.tmp \
+	&& sed "s/g_data_picasa_web/gdata_picasaweb/" gdata-picasaweb-enums.h.tmp > gdata-picasaweb-enums.h.tmp2 \
+	&& sed "s/GDATA_TYPE_DATA_PICASA_WEB/GDATA_TYPE_PICASAWEB/" gdata-picasaweb-enums.h.tmp2 > gdata-picasaweb-enums.h \
+	&& rm -f gdata-picasaweb-enums.h.tmp \
+	&& rm -f gdata-picasaweb-enums.h.tmp2
+
+gdata-picasaweb-enums.c: $(gdatapicasawebinclude_HEADERS) Makefile gdata-picasaweb-enums.h
+	(cd $(srcdir) && $(GLIB_MKENUMS) \
+			--fhead "#include \"gdata-picasaweb-album.h\"\n#include \"gdata-picasaweb-enums.h\"" \
+			--fprod "\n/* enumerations from \"@filename \" */" \
+			--vhead "GType\n enum_name@_get_type (void)\n{\n  static GType etype = 0;\n  if (etype == 0) {\n    static const G Type@Value values[] = {" \
+			--vprod "      { @VALUENAME@, \"@VALUENAME \", \"@valuenick \" }," \
+			--vtail "      { 0, NULL, NULL }\n    };\n    etype = g_ type@_register_static (\"@EnumName \", values);\n  }\n  return etype;\n}\n" \
+		$(gdatapicasawebinclude_HEADERS)) > gdata-picasaweb-enums.c.tmp \
+	&& sed "s/g_data_picasa_web/gdata_picasaweb/" gdata-picasaweb-enums.c.tmp > gdata-picasaweb-enums.c \
+	&& rm -f gdata-picasaweb-enums.c.tmp
+
+# Library
+gdatapicasawebincludedir = $(pkgincludedir)/gdata/services/picasaweb
+gdatapicasawebinclude_HEADERS = \
+	gdata-picasaweb-service.h	\
+	gdata-picasaweb-query.h		\
+	gdata-picasaweb-file.h		\
+	gdata-picasaweb-album.h		\
+	gdata-picasaweb-enums.h
+
+noinst_LTLIBRARIES = libgdatapicasaweb.la
+
+libgdatapicasaweb_la_SOURCES = \
+	gdata-picasaweb-enums.c		\
+	gdata-picasaweb-album.c		\
+	gdata-picasaweb-query.c		\
+	gdata-picasaweb-file.c		\
+	gdata-picasaweb-service.c
+
+libgdatapicasaweb_la_CPPFLAGS = \
+	-I$(top_srcdir)					\
+	-I$(top_srcdir)/gdata				\
+	-I$(top_srcdir)/gdata/services/picasaweb	\
+	$(DISABLE_DEPRECATED)				\
+	$(AM_CPPFLAGS)
+
+libgdatapicasaweb_la_CFLAGS = \
+	$(GDATA_CFLAGS)	\
+	$(WARN_CFLAGS)	\
+	$(AM_CFLAGS)	\
+	-D_GNU_SOURCE
+
+libgdatapicasaweb_la_LIBADD = \
+	$(GDATA_LIBS)
+
+libgdatapicasaweb_la_LDFLAGS = \
+	-no-undefined	\
+	$(AM_LDFLAGS)
+
+# General cleanup
+CLEANFILES = $(GDATA_PICASAWEB_ENUM_FILES)
+
+-include $(top_srcdir)/git.mk
diff --git a/gdata/services/picasaweb/gdata-picasaweb-album.c b/gdata/services/picasaweb/gdata-picasaweb-album.c
new file mode 100644
index 0000000..2e20033
--- /dev/null
+++ b/gdata/services/picasaweb/gdata-picasaweb-album.c
@@ -0,0 +1,1097 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Richard Schwarting 2009 <aquarichy gmail com>
+ * Copyright (C) Philip Withnall 2009 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION:gdata-picasaweb-album
+ * @short_description: GData PicasaWeb album object
+ * @stability: Unstable
+ * @include: gdata/services/picasaweb/gdata-picasaweb-album.h
+ *
+ * #GDataPicasaWebAlbum is a subclass of #GDataEntry to represent an album from Google PicasaWeb.
+ *
+ * For more details of Google PicasaWeb's GData API, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html";>
+ * online documentation</ulink>.
+ **/
+
+/* TODO: support the album cover/icon ? */
+
+#include <config.h>
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <libxml/parser.h>
+#include <string.h>
+
+#include "gdata-picasaweb-album.h"
+#include "gdata-private.h"
+#include "gdata-service.h"
+#include "gdata-parsable.h"
+#include "gdata-parser.h"
+#include "gdata-types.h"
+#include "media/gdata-media-group.h"
+#include "gdata-picasaweb-enums.h"
+
+static void gdata_picasaweb_album_dispose (GObject *object);
+static void gdata_picasaweb_album_finalize (GObject *object);
+static void gdata_picasaweb_album_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec);
+static void gdata_picasaweb_album_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec);
+static void get_xml (GDataParsable *parsable, GString *xml_string);
+static gboolean parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error);
+static void get_namespaces (GDataParsable *parsable, GHashTable *namespaces);
+
+struct _GDataPicasaWebAlbumPrivate {
+	gchar *user;
+	gchar *nickname;
+	GTimeVal edited;
+	gchar *name; /* album title, usable in URIs */
+	gchar *location;
+	GDataPicasaWebVisibility visibility;
+	GTimeVal timestamp;
+	guint num_photos;
+	guint num_photos_remaining;
+	glong bytes_used;
+	gboolean is_commenting_enabled;
+	guint comment_count;
+
+	/* media:group */
+	GDataMediaGroup *media_group;
+};
+
+enum {
+	PROP_USER = 1,
+	PROP_NICKNAME,
+	PROP_EDITED,
+	PROP_NAME,
+	PROP_LOCATION,
+	PROP_VISIBILITY,
+	PROP_TIMESTAMP,
+	PROP_NUM_PHOTOS,
+	PROP_NUM_PHOTOS_REMAINING,
+	PROP_BYTES_USED,
+	PROP_IS_COMMENTING_ENABLED,
+	PROP_COMMENT_COUNT,
+	PROP_DESCRIPTION,
+	PROP_TAGS
+};
+
+G_DEFINE_TYPE (GDataPicasaWebAlbum, gdata_picasaweb_album, GDATA_TYPE_ENTRY)
+#define GDATA_PICASAWEB_ALBUM_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GDATA_TYPE_PICASAWEB_ALBUM, GDataPicasaWebAlbumPrivate))
+
+static void
+gdata_picasaweb_album_class_init (GDataPicasaWebAlbumClass *klass)
+{
+	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+	GDataParsableClass *parsable_class = GDATA_PARSABLE_CLASS (klass);
+
+	g_type_class_add_private (klass, sizeof (GDataPicasaWebAlbumPrivate));
+
+	gobject_class->get_property = gdata_picasaweb_album_get_property;
+	gobject_class->set_property = gdata_picasaweb_album_set_property;
+	gobject_class->dispose = gdata_picasaweb_album_dispose;
+	gobject_class->finalize = gdata_picasaweb_album_finalize;
+
+	parsable_class->parse_xml = parse_xml;
+	parsable_class->get_xml = get_xml;
+	parsable_class->get_namespaces = get_namespaces;
+
+	/**
+	 * GDataPicasaWeb:user
+	 *
+	 * The username of the album owner.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_user";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_USER,
+					 g_param_spec_string ("user",
+							      "User", "The username of the album owner.",
+							      NULL,
+							      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWeb:nickname
+	 *
+	 * The user's nickname. This is a user-specified value that should be used when referring to the user by name.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_nickname";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_NICKNAME,
+					 g_param_spec_string ("nickname",
+							      "Nickname", "The user's nickname.",
+							      NULL,
+							      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWeb:edited
+	 *
+	 * The time this album was last edited. If the album has not been edited yet, the content indicates the time it was created.
+	 *
+	 * For more information, see the <ulink type="http" url="http://www.atomenabled.org/developers/protocol/#appEdited";>
+	 * Atom Publishing Protocol specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_EDITED,
+					 g_param_spec_boxed ("edited",
+							     "Edited", "The time this album was last edited.",
+							     GDATA_TYPE_G_TIME_VAL,
+							     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWeb:name
+	 *
+	 * The name of the album, which is the URI-usable name derived from the album title (#GDataEntry:title).
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_name";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_NAME,
+					 g_param_spec_string ("name",
+							      "Name", "The name of the album.",
+							      NULL,
+							      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWeb:location
+	 *
+	 * The user-specified location associated with the album. A place name.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_location";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_LOCATION,
+					 g_param_spec_string ("location",
+							      "Location", "The user-specified location associated with the album.",
+							      NULL,
+							      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWeb:visibility
+	 *
+	 * The visibility (or access rights) of the album.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_access";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	/* TODO: atom:rights duplicates this? */
+	g_object_class_install_property (gobject_class, PROP_VISIBILITY,
+					 g_param_spec_enum ("visibility",
+							    "Visibility", "The visibility (or access rights) of the album.",
+							    GDATA_TYPE_PICASAWEB_VISIBILITY, GDATA_PICASAWEB_PUBLIC,
+							    G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWeb:timestamp
+	 *
+	 * The timestamp of when the album occurred, settable by the user.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_timestamp";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_TIMESTAMP,
+					 g_param_spec_boxed ("timestamp",
+							     "Timestamp", "The timestamp of when the album occurred, settable by the user.",
+							     GDATA_TYPE_G_TIME_VAL,
+							     G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/* TODO: Change to photo-count? */
+	/**
+	 * GDataPicasaWebAlbum:num-photos
+	 *
+	 * The number of photos and videos in the album.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_numphotos";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_NUM_PHOTOS,
+					 g_param_spec_uint ("num-photos",
+							    "Number of photos", "The number of photos and videos in the album.",
+							    0, G_MAXUINT, 0,
+							    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/* TODO: Change to remaining-photos-count? */
+	/**
+	 * GDataPicasaWebAlbum:num-photos-remaining
+	 *
+	 * The number of photos and videos that can still be uploaded to this album.
+	 * This doesn't account for quota, just a hardcoded maximum number per album set by Google.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_numphotosremaining";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_NUM_PHOTOS_REMAINING,
+					 g_param_spec_uint ("num-photos-remaining",
+							    "Number of photo spaces remaining", "The number of photos and videos that can still be"
+							    " uploaded to this album.",
+							    0, G_MAXUINT, 0,
+							    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebAlbum:bytes-used:
+	 *
+	 * The number of bytes consumed by this album and its contents. Note that this is only set if the authenticated user is the owner of the
+	 * album; it's otherwise %-1.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_bytesUsed";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_BYTES_USED,
+					 g_param_spec_long ("bytes-used",
+							    "Number of bytes used", "The number of bytes consumed by this album and its contents.",
+							    -1, G_MAXLONG, -1,
+							    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebAlbum:commenting-enabled:
+	 *
+	 * Whether commenting is enabled for this album.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_IS_COMMENTING_ENABLED,
+					 g_param_spec_boolean ("is-commenting-enabled",
+							       "Commenting enabled?", "Whether commenting is enabled for this album.",
+							       FALSE,
+							       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebAlbum:comment-count:
+	 *
+	 * The number of comments on the album.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_commentCount";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_COMMENT_COUNT,
+					 g_param_spec_uint ("comment-count",
+							    "Comment count", "The number of comments on the album.",
+							    0, G_MAXUINT, 0,
+							    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebAlbum:description:
+	 *
+	 * Description of the album.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#media_description";>
+	 * Media RSS specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_DESCRIPTION,
+					 g_param_spec_string ("description",
+							      "Description", "Description of the album.",
+							      NULL,
+							      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebAlbum:tags:
+	 *
+	 * A comma-separated list of tags associated with the album; all the tags associated with the individual photos in the album.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#media_keywords";>
+	 * Media RSS specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_TAGS,
+					 g_param_spec_string ("tags",
+							      "Tags", "A comma-separated list of tags associated with the album",
+							      NULL,
+							      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+}
+
+static void
+notify_title_cb (GDataPicasaWebAlbum *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_picasaweb_album_init (GDataPicasaWebAlbum *self)
+{
+	self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_PICASAWEB_ALBUM, GDataPicasaWebAlbumPrivate);
+	self->priv->media_group = g_object_new (GDATA_TYPE_MEDIA_GROUP, NULL);
+
+	/* Connect to the notify::title signal from GDataEntry so our media:group title can be kept in sync */
+	g_signal_connect (GDATA_ENTRY (self), "notify::title", G_CALLBACK (notify_title_cb), NULL);
+}
+
+static void
+gdata_picasaweb_album_dispose (GObject *object)
+{
+	GDataPicasaWebAlbumPrivate *priv = GDATA_PICASAWEB_ALBUM_GET_PRIVATE (object);
+
+	if (priv->media_group != NULL)
+		g_object_unref (priv->media_group);
+	priv->media_group = NULL;
+
+	/* Chain up to the parent class */
+	G_OBJECT_CLASS (gdata_picasaweb_album_parent_class)->dispose (object);
+}
+
+static void
+gdata_picasaweb_album_finalize (GObject *object)
+{
+	GDataPicasaWebAlbumPrivate *priv = GDATA_PICASAWEB_ALBUM_GET_PRIVATE (object);
+
+	xmlFree (priv->user);
+	xmlFree (priv->nickname);
+	xmlFree (priv->name);
+	g_free (priv->location);
+
+	/* Chain up to the parent class */
+	G_OBJECT_CLASS (gdata_picasaweb_album_parent_class)->finalize (object);
+}
+
+static void
+gdata_picasaweb_album_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
+{
+	GDataPicasaWebAlbumPrivate *priv = GDATA_PICASAWEB_ALBUM_GET_PRIVATE (object);
+
+	switch (property_id) {
+		case PROP_USER:
+			g_value_set_string (value, priv->user);
+			break;
+		case PROP_NICKNAME:
+			g_value_set_string (value, priv->nickname);
+			break;
+		case PROP_EDITED:
+			g_value_set_boxed (value, &(priv->edited));
+			break;
+		case PROP_NAME:
+			g_value_set_string (value, priv->name);
+			break;
+		case PROP_LOCATION:
+			g_value_set_string (value, priv->location);
+			break;
+		case PROP_VISIBILITY:
+			g_value_set_enum (value, priv->visibility);
+			break;
+		case PROP_TIMESTAMP:
+			g_value_set_boxed (value, &(priv->timestamp));
+			break;
+		case PROP_NUM_PHOTOS:
+			g_value_set_uint (value, priv->num_photos);
+			break;
+		case PROP_NUM_PHOTOS_REMAINING:
+			g_value_set_uint (value, priv->num_photos_remaining);
+			break;
+		case PROP_BYTES_USED:
+			g_value_set_long (value, priv->bytes_used);
+			break;
+		case PROP_IS_COMMENTING_ENABLED:
+			g_value_set_boolean (value, priv->is_commenting_enabled);
+			break;
+		case PROP_COMMENT_COUNT:
+			g_value_set_uint (value, priv->comment_count);
+			break;
+		case PROP_DESCRIPTION:
+			g_value_set_string (value, gdata_media_group_get_description (priv->media_group));
+			break;
+		case PROP_TAGS:
+			g_value_set_string (value, gdata_media_group_get_keywords (priv->media_group));
+			break;
+		default:
+			/* We don't have any other property... */
+			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+			break;
+	}
+}
+
+static void
+gdata_picasaweb_album_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
+{
+	GDataPicasaWebAlbum *self = GDATA_PICASAWEB_ALBUM (object);
+
+	switch (property_id) {
+		case PROP_LOCATION:
+			gdata_picasaweb_album_set_location (self, g_value_get_string (value));
+			break;
+		case PROP_VISIBILITY:
+			gdata_picasaweb_album_set_visibility (self, g_value_get_enum (value));
+			break;
+		case PROP_TIMESTAMP:
+			gdata_picasaweb_album_set_timestamp (self, g_value_get_boxed (value));
+			break;
+		case PROP_IS_COMMENTING_ENABLED:
+			gdata_picasaweb_album_set_is_commenting_enabled (self, g_value_get_boolean (value));
+			break;
+		case PROP_DESCRIPTION:
+			gdata_picasaweb_album_set_description (self, g_value_get_string (value));
+			break;
+		case PROP_TAGS:
+			gdata_picasaweb_album_set_tags (self, g_value_get_string (value));
+			break;
+		default:
+			/* We don't have any other property... */
+			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+			break;
+	}
+}
+
+static gboolean
+parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error)
+{
+	GDataPicasaWebAlbum *self = GDATA_PICASAWEB_ALBUM (parsable);
+
+	if (xmlStrcmp (node->name, (xmlChar*) "group") == 0) {
+		/* media:group */
+		GDataMediaGroup *group = GDATA_MEDIA_GROUP (_gdata_parsable_new_from_xml_node (GDATA_TYPE_MEDIA_GROUP, "group", doc,
+											       node, NULL, error));
+		if (group == NULL)
+			return FALSE;
+
+		if (self->priv->media_group != NULL)
+			/* We should really error here, but we can't, as priv->media_group has to be pre-populated
+			 * in order for things like gdata_picasaweb_album_get_tags() to work. */
+			g_object_unref (self->priv->media_group);
+
+		self->priv->media_group = group;
+	} else if (xmlStrcmp (node->name, (xmlChar*) "user") == 0) {
+		/* gphoto:user */
+		xmlChar *user = xmlNodeListGetString (doc, node->children, TRUE);
+		if (user == NULL || *user == '\0')
+			return gdata_parser_error_required_content_missing (node, error);
+		xmlFree (self->priv->user);
+		self->priv->user = (gchar*) user;
+	} else if (xmlStrcmp (node->name, (xmlChar*) "nickname") == 0) {
+		/* gphoto:nickname */
+		xmlChar *nickname = xmlNodeListGetString (doc, node->children, TRUE);
+		if (nickname == NULL || *nickname == '\0')
+			return gdata_parser_error_required_content_missing (node, error);
+		xmlFree (self->priv->nickname);
+		self->priv->nickname = (gchar*) nickname;
+	} else if (xmlStrcmp (node->name, (xmlChar*) "edited") == 0) {
+		/* app:edited */
+		xmlChar *edited = xmlNodeListGetString (doc, node->children, TRUE);
+		if (g_time_val_from_iso8601 ((gchar*) edited, &(self->priv->edited)) == FALSE) {
+			/* Error */
+			gdata_parser_error_not_iso8601_format (node, (gchar*) edited, error);
+			xmlFree (edited);
+			return FALSE;
+		}
+		xmlFree (edited);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "summary") == 0) {
+		/* gphoto:summary */
+		/* @summary and @description are the same, so they're combined to @description */
+		xmlChar *summary = xmlNodeListGetString (doc, node->children, TRUE);
+		gdata_picasaweb_album_set_description (self, (gchar*) summary);
+		xmlFree (summary);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "name") == 0) {
+		/* gphoto:name */
+		xmlChar *name = xmlNodeListGetString (doc, node->children, TRUE);
+		if (name == NULL || *name == '\0') {
+			xmlFree (name);
+			return gdata_parser_error_required_content_missing (node, error);
+		}
+		xmlFree (self->priv->name);
+		self->priv->name = (gchar*) name;
+	} else if (xmlStrcmp (node->name, (xmlChar*) "location") == 0) {
+		/* gphoto:location */
+		xmlChar *location = xmlNodeListGetString (doc, node->children, TRUE);
+		gdata_picasaweb_album_set_location (self, (gchar*) location);
+		xmlFree (location);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "access") == 0) {
+		/* gphoto:access */
+		xmlChar *access = xmlNodeListGetString (doc, node->children, TRUE);
+		if (xmlStrcmp (access, (xmlChar*) "public") == 0) {
+			gdata_picasaweb_album_set_visibility (self, GDATA_PICASAWEB_PUBLIC);
+		} else if (xmlStrcmp (access, (xmlChar*) "private") == 0) {
+			gdata_picasaweb_album_set_visibility (self, GDATA_PICASAWEB_PRIVATE);
+		} else {
+			gdata_parser_error_unknown_content (node, (gchar*) access, error);
+			xmlFree (access);
+			return FALSE;
+		}
+		xmlFree (access);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "timestamp") == 0) {
+		/* gphoto:timestamp */
+		xmlChar *timestamp_str;
+		gulong milliseconds;
+		GTimeVal timestamp;
+
+		timestamp_str = xmlNodeListGetString (doc, node->children, TRUE);
+		milliseconds = strtoull ((char*) timestamp_str, NULL, 10);
+		xmlFree (timestamp_str);
+
+		timestamp.tv_sec = (glong) (milliseconds / 1000);
+		timestamp.tv_usec = (glong) ((milliseconds % 1000) * 1000);
+		gdata_picasaweb_album_set_timestamp (self, &timestamp);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "numphotos") == 0) {
+		/* gphoto:numphotos */
+		xmlChar *num_photos = xmlNodeListGetString (doc, node->children, TRUE);
+		if (num_photos == NULL || *num_photos == '\0')
+			return gdata_parser_error_required_content_missing (node, error);
+		self->priv->num_photos = strtoul ((char*) num_photos, NULL, 10);
+		xmlFree (num_photos);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "numphotosremaining") == 0) {
+		/* gphoto:numphotosremaining */
+		xmlChar *num_photos_remaining = xmlNodeListGetString (doc, node->children, TRUE);
+		if (num_photos_remaining == NULL || *num_photos_remaining == '\0')
+			return gdata_parser_error_required_content_missing (node, error);
+		self->priv->num_photos_remaining = strtoul ((char*) num_photos_remaining, NULL, 10);
+		xmlFree (num_photos_remaining);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "bytesUsed") == 0) {
+		/* gphoto:bytesUsed */
+		xmlChar *bytes_used = xmlNodeListGetString (doc, node->children, TRUE);
+		if (bytes_used == NULL || *bytes_used == '\0')
+			return gdata_parser_error_required_content_missing (node, error);
+		self->priv->bytes_used = strtol ((char*) bytes_used, NULL, 10);
+		xmlFree (bytes_used);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "commentingEnabled") == 0) {
+		/* gphoto:commentingEnabled */
+		xmlChar *commenting_enabled = xmlNodeListGetString (doc, node->children, TRUE);
+		if (commenting_enabled == NULL || *commenting_enabled == '\0')
+			return gdata_parser_error_required_content_missing (node, error);
+		gdata_picasaweb_album_set_is_commenting_enabled (self, (xmlStrcmp (commenting_enabled, (xmlChar*) "true") == 0) ? TRUE : FALSE);
+		xmlFree (commenting_enabled);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "commentCount") == 0) {
+		xmlChar *comment_count = xmlNodeListGetString (doc, node->children, TRUE);
+		if (comment_count == NULL || *comment_count == '\0')
+			return gdata_parser_error_required_content_missing (node, error);
+		self->priv->comment_count = strtoul ((char*) comment_count, NULL, 10);
+		xmlFree (comment_count);
+	} else if (GDATA_PARSABLE_CLASS (gdata_picasaweb_album_parent_class)->parse_xml (parsable, doc, node, user_data, error) == FALSE) {
+		/* Error! */
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static void
+get_xml (GDataParsable *parsable, GString *xml_string)
+{
+	gchar *xml;
+	GDataPicasaWebAlbumPrivate *priv = GDATA_PICASAWEB_ALBUM (parsable)->priv;
+
+	/* Chain up to the parent class */
+	GDATA_PARSABLE_CLASS (gdata_picasaweb_album_parent_class)->get_xml (parsable, xml_string);
+
+	/* Add all the album-specific XML */
+	/* TODO: gphoto:name?, gphoto:id */
+	if (priv->location != NULL) {
+		gchar *location = g_markup_escape_text (priv->location, -1);
+		g_string_append_printf (xml_string, "<gphoto:location>%s</gphoto:location>", location);
+		g_free (location);
+	}
+
+	if (priv->visibility == GDATA_PICASAWEB_PUBLIC)
+		g_string_append (xml_string, "<gphoto:access>public</gphoto:access>");
+	else if (priv->visibility == GDATA_PICASAWEB_PRIVATE)
+		g_string_append (xml_string, "<gphoto:access>private</gphoto:access>");
+	else
+		g_assert_not_reached ();
+
+	if (priv->timestamp.tv_sec != 0 || priv->timestamp.tv_usec != 0) {
+		/* in milliseconds */
+		g_string_append_printf (xml_string, "<gphoto:timestamp>%lu</gphoto:timestamp>",
+					priv->timestamp.tv_sec * 1000 + priv->timestamp.tv_usec);
+	}
+
+	if (priv->is_commenting_enabled == FALSE)
+		g_string_append (xml_string, "<gphoto:commentingEnabled>false</gphoto:commentingEnabled>");
+	else
+		g_string_append (xml_string, "<gphoto:commentingEnabled>true</gphoto:commentingEnabled>");
+
+	/* media:group */
+	xml = _gdata_parsable_get_xml (GDATA_PARSABLE (priv->media_group), "media:group", FALSE);
+	g_string_append (xml_string, xml);
+	g_free (xml);
+
+	/* TODO: add GML support */
+	/* TODO:
+	 * - Finish supporting all tags
+	 * - Check all tags here are valid for insertions and updates
+	 * - Check things are escaped (or not) as appropriate
+	 * - Write a function to encapsulate g_markup_escape_text and
+	 *   g_string_append_printf to reduce the number of allocations
+	 */
+}
+
+static void
+get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
+{
+	GDataPicasaWebAlbumPrivate *priv = GDATA_PICASAWEB_ALBUM (parsable)->priv;
+
+	/* Chain up to the parent class */
+	GDATA_PARSABLE_CLASS (gdata_picasaweb_album_parent_class)->get_namespaces (parsable, namespaces);
+
+	g_hash_table_insert (namespaces, (gchar*) "gphoto", (gchar*) "http://schemas.google.com/photos/2007";);
+	g_hash_table_insert (namespaces, (gchar*) "app", (gchar*) "http://www.w3.org/2007/app";);
+
+	/* Add the media:group namespaces */
+	GDATA_PARSABLE_GET_CLASS (priv->media_group)->get_namespaces (GDATA_PARSABLE (priv->media_group), namespaces);
+}
+
+/**
+ * gdata_picasaweb_album_new:
+ * @id: the album's ID, or %NULL
+ *
+ * Creates a new #GDataPicasaWebAlbum with the given ID and default properties.
+ *
+ * Return value: a new #GDataPicasaWebAlbum; unref with g_object_unref()
+ *
+ * Since: 0.4.0
+ **/
+GDataPicasaWebAlbum *
+gdata_picasaweb_album_new (const gchar *id)
+{
+	return g_object_new (GDATA_TYPE_PICASAWEB_ALBUM, "id", id, NULL);
+}
+
+/**
+ * gdata_picasaweb_album_new_from_xml:
+ * @xml: an XML string
+ * @length: the length in characters of @xml, or %-1
+ * @error: a #GError, or %NULL
+ *
+ * Creates a new #GDataPicasaWebAlbum from an XML string. If @length is %-1, the length of
+ * the string will be calculated.
+ *
+ * Errors from #GDataParserError can be returned if problems are found in the XML.
+ *
+ * Return value: a new #GDataPicasaWebAlbum, or %NULL; unref with g_object_unref()
+ *
+ * Since: 0.4.0
+ **/
+GDataPicasaWebAlbum *
+gdata_picasaweb_album_new_from_xml (const gchar *xml, gint length, GError **error)
+{
+	return GDATA_PICASAWEB_ALBUM (_gdata_entry_new_from_xml (GDATA_TYPE_PICASAWEB_ALBUM, xml, length, error));
+}
+
+/**
+ * gdata_picasaweb_album_get_user:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Gets the #GDataPicasaWebAlbum:user property.
+ *
+ * Return value: the album owner's username
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_album_get_user (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), NULL);
+	return self->priv->user;
+}
+
+/**
+ * gdata_picasaweb_album_get_nickname:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Gets the #GDataPicasaWebAlbum:nickname property.
+ *
+ * Return value: the album owner's nickname
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_album_get_nickname (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), NULL);
+	return self->priv->nickname;
+}
+
+/**
+ * gdata_picasaweb_album_get_edited:
+ * @self: a #GDataPicasaWebAlbum
+ * @edited: a #GTimeVal
+ *
+ * Gets the #GDataPicasaWebAlbum:edited property and puts it in @edited. If the property is unset,
+ * both fields in the #GTimeVal will be set to %0.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_album_get_edited (GDataPicasaWebAlbum *self, GTimeVal *edited)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_ALBUM (self));
+	g_return_if_fail (edited != NULL);
+	*edited = self->priv->edited;
+}
+
+/**
+ * gdata_picasaweb_album_get_name:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Gets the #GDataPicasaWebAlbum:name property.
+ *
+ * Return value: the album's name, as usable in URIs, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_album_get_name (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), NULL);
+	return self->priv->name;
+}
+
+/**
+ * gdata_picasaweb_album_get_location:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Gets the #GDataPicasaWebAlbum:location property.
+ *
+ * Return value: the album's location, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_album_get_location (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), NULL);
+	return self->priv->location;
+}
+
+/**
+ * gdata_picasaweb_album_set_location:
+ * @self: a #GDataPicasaWebAlbum
+ * @location: the new album location
+ *
+ * Sets the #GDataPicasaWebAlbum:location property to @location.
+ *
+ * Set @location to %NULL to unset the property.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_album_set_location (GDataPicasaWebAlbum *self, const gchar *location)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_ALBUM (self));
+
+	g_free (self->priv->location);
+	self->priv->location = g_strdup (location);
+	g_object_notify (G_OBJECT (self), "location");
+}
+
+/**
+ * gdata_picasaweb_album_get_visibility:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Gets the #GDataPicasaWebAlbum:visibility property.
+ *
+ * Return value: the album's visibility level
+ *
+ * Since: 0.4.0
+ **/
+GDataPicasaWebVisibility
+gdata_picasaweb_album_get_visibility (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), GDATA_PICASAWEB_PUBLIC);
+	return self->priv->visibility;
+}
+
+/**
+ * gdata_picasaweb_album_set_visibility:
+ * @self: a #GDataPicasaWebAlbum
+ * @visibility: the new album visibility level
+ *
+ * Sets the #GDataPicasaWebAlbum:visibility property to @visibility.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_album_set_visibility (GDataPicasaWebAlbum *self, GDataPicasaWebVisibility visibility)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_ALBUM (self));
+
+	self->priv->visibility = visibility;
+	g_object_notify (G_OBJECT (self), "visibility");
+}
+
+/**
+ * gdata_picasaweb_album_get_timestamp:
+ * @self: a #GDataPicasaWebAlbum
+ * @timestamp: a #GTimeVal
+ *
+ * Gets the #GDataPicasaWebAlbum:timestamp property and puts it in @timestamp. If the property is unset,
+ * both fields in the #GTimeVal will be set to %0.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_album_get_timestamp (GDataPicasaWebAlbum *self, GTimeVal *timestamp)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_ALBUM (self));
+	g_return_if_fail (timestamp != NULL);
+	*timestamp = self->priv->timestamp;
+}
+
+/**
+ * gdata_picasaweb_album_set_timestamp:
+ * @self: a #GDataPicasaWebAlbum
+ * @timestamp: a #GTimeVal, or %NULL
+ *
+ * Sets the #GDataPicasaWebAlbum:timestamp property from values supplied by @timestamp.
+ *
+ * Set @timestamp to %NULL to unset the property.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_album_set_timestamp (GDataPicasaWebAlbum *self, GTimeVal *timestamp)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_ALBUM (self));
+	if (timestamp == NULL)
+		self->priv->timestamp.tv_sec = self->priv->timestamp.tv_usec = 0;
+	else
+		self->priv->timestamp = *timestamp;
+	g_object_notify (G_OBJECT (self), "timestamp");
+}
+
+/**
+ * gdata_picasaweb_album_get_num_photos:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Gets the #GDataPicasaWebAlbum:num-photos property.
+ *
+ * Return value: the number of photos currently in the album
+ *
+ * Since: 0.4.0
+ **/
+guint
+gdata_picasaweb_album_get_num_photos (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), 0);
+	return self->priv->num_photos;
+}
+
+/**
+ * gdata_picasaweb_album_get_num_photos_remaining:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Gets the #GDataPicasaWebAlbum:num-photos-remaining property.
+ *
+ * Return value: the number of photos that can still be uploaded to the album
+ *
+ * Since: 0.4.0
+ **/
+guint
+gdata_picasaweb_album_get_num_photos_remaining (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), 0);
+	return self->priv->num_photos_remaining;
+}
+
+/**
+ * gdata_picasaweb_album_get_bytes_used:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Gets the #GDataPicasaWebAlbum:bytes-used property. It will return %-1 if the current authenticated
+ * user is not the owner of the album.
+ *
+ * Return value: the number of bytes used by the album and its contents, or %-1
+ *
+ * Since: 0.4.0
+ **/
+glong
+gdata_picasaweb_album_get_bytes_used (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), -1);
+	return self->priv->bytes_used;
+}
+
+/**
+ * gdata_picasaweb_album_is_commenting_enabled:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Gets the #GDataPicasaWebAlbum:is-commenting-enabled property.
+ *
+ * Return value: %TRUE if commenting is enabled for the album, %FALSE otherwise
+ *
+ * Since: 0.4.0
+ **/
+gboolean
+gdata_picasaweb_album_is_commenting_enabled (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), FALSE);
+	return self->priv->is_commenting_enabled;
+}
+
+/**
+ * gdata_picasaweb_album_set_is_commenting_enabled:
+ * @self: a #GDataPicasaWebAlbum
+ * @is_commenting_enabled: %TRUE if commenting should be enabled for the album, %FALSE otherwise
+ *
+ * Sets the #GDataPicasaWebAlbum:is-commenting-enabled property to @is_commenting_enabled.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_album_set_is_commenting_enabled (GDataPicasaWebAlbum *self, gboolean is_commenting_enabled)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_ALBUM (self));
+	self->priv->is_commenting_enabled = is_commenting_enabled;
+	g_object_notify (G_OBJECT (self), "is-commenting-enabled");
+}
+
+/**
+ * gdata_picasaweb_album_get_comment_count:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Gets the #GDataPicasaWebAlbum:comment-count property.
+ *
+ * Return value: the number of comments on the album
+ *
+ * Since: 0.4.0
+ **/
+guint
+gdata_picasaweb_album_get_comment_count (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), 0);
+	return self->priv->comment_count;
+}
+
+/**
+ * gdata_picasaweb_album_get_tags:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Gets the #GDataPicasaWebAlbum:tags property.
+ *
+ * Return value: a comma-separated list of tags associated with all the photos in the album, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_album_get_tags (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), NULL);
+	return gdata_media_group_get_keywords (self->priv->media_group);
+}
+
+/**
+ * gdata_picasaweb_album_set_tags:
+ * @self: a #GDataPicasaWebAlbum
+ * @tags: the new comma-separated list of tags, or %NULL
+ *
+ * Sets the #GDataPicasaWebAlbum:tags property to @tags.
+ *
+ * Set @tags to %NULL to unset the album's tag list.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_album_set_tags (GDataPicasaWebAlbum *self, const gchar *tags)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_ALBUM (self));
+
+	gdata_media_group_set_keywords (self->priv->media_group, tags);
+	g_object_notify (G_OBJECT (self), "tags");
+}
+
+/**
+ * gdata_picasaweb_album_get_description:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Gets the #GDataPicasaWebAlbum:description property.
+ *
+ * Return value: the album's long text description, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_album_get_description (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), NULL);
+	return gdata_media_group_get_description (self->priv->media_group);
+}
+
+/**
+ * gdata_picasaweb_album_set_description:
+ * @self: a #GDataPicasaWebAlbum
+ * @description: the album's new description, or %NULL
+ *
+ * Sets the #GDataPicasaWebAlbum:description property to the new description, @description.
+ *
+ * Set @description to %NULL to unset the album's description.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_album_set_description (GDataPicasaWebAlbum *self, const gchar *description)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_ALBUM (self));
+
+	/* media:group/media:description is the same as atom:summary */
+	gdata_media_group_set_description (self->priv->media_group, description);
+	/*gdata_entry_set_summary (GDATA_ENTRY (self), description); TODO function doesn't exist yet */
+	g_object_notify (G_OBJECT (self), "description");
+}
+
+/**
+ * gdata_picasaweb_album_get_contents:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Returns a list of media content, such as the cover image for the album.
+ *
+ * Return value: a #GList of #GDataMediaContent items
+ *
+ * Since: 0.4.0
+ **/
+GList *
+gdata_picasaweb_album_get_contents (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), NULL);
+	return gdata_media_group_get_contents (self->priv->media_group);
+}
+
+/**
+ * gdata_picasaweb_album_get_thumbnails:
+ * @self: a #GDataPicasaWebAlbum
+ *
+ * Returns a list of thumbnails, often at different sizes, for this album.
+ *
+ * Return value: a #GList of #GDataMediaThumbnail<!-- -->s, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+GList *
+gdata_picasaweb_album_get_thumbnails (GDataPicasaWebAlbum *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_ALBUM (self), NULL);
+	return gdata_media_group_get_thumbnails (self->priv->media_group);
+}
diff --git a/gdata/services/picasaweb/gdata-picasaweb-album.h b/gdata/services/picasaweb/gdata-picasaweb-album.h
new file mode 100644
index 0000000..efc57ef
--- /dev/null
+++ b/gdata/services/picasaweb/gdata-picasaweb-album.h
@@ -0,0 +1,109 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Richard Schwarting 2009 <aquarichy gmail com>
+ * Copyright (C) Philip Withnall 2009 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GDATA_PICASWEB_ALBUM_H
+#define GDATA_PICASWEB_ALBUM_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gdata/gdata-entry.h>
+
+G_BEGIN_DECLS
+
+/**
+ * GDataPicasaWebVisibility:
+ * @GDATA_PICASAWEB_PUBLIC: the album is visible to everyone, regardless of whether they're authenticated
+ * @GDATA_PICASAWEB_PRIVATE: the album is visible only to authenticated users in a whitelist
+ *
+ * Visibility statuses available for albums on PicasaWeb. For more information, see the <ulink type="http"
+ * url="http://code.google.com/apis/picasaweb/reference.html#Visibility";>online documentation</ulink>.
+ *
+ * Since: 0.4.0
+ **/
+typedef enum {
+	GDATA_PICASAWEB_PUBLIC = 1,
+	GDATA_PICASAWEB_PRIVATE
+} GDataPicasaWebVisibility;
+
+#define GDATA_TYPE_PICASAWEB_ALBUM		(gdata_picasaweb_album_get_type ())
+#define GDATA_PICASAWEB_ALBUM(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), GDATA_TYPE_PICASAWEB_ALBUM, GDataPicasaWebAlbum))
+#define GDATA_PICASAWEB_ALBUM_CLASS(k)		(G_TYPE_CHECK_CLASS_CAST((k), GDATA_TYPE_PICASAWEB_ALBUM, GDataPicasaWebAlbumClass))
+#define GDATA_IS_PICASAWEB_ALBUM(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), GDATA_TYPE_PICASAWEB_ALBUM))
+#define GDATA_IS_PICASAWEB_ALBUM_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), GDATA_TYPE_PICASAWEB_ALBUM))
+#define GDATA_PICASAWEB_ALBUM_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), GDATA_TYPE_PICASAWEB_ALBUM, GDataPicasaWebAlbumClass))
+
+typedef struct _GDataPicasaWebAlbumPrivate	GDataPicasaWebAlbumPrivate;
+
+/**
+ * GDataPicasaWebAlbum:
+ *
+ * All the fields in the #GDataPicasaWebAlbum structure are private and should never be accessed directly.
+ *
+ * Since: 0.4.0
+ **/
+typedef struct {
+	GDataEntry parent;
+	GDataPicasaWebAlbumPrivate *priv;
+} GDataPicasaWebAlbum;
+
+/**
+ * GDataPicasaWebAlbumClass:
+ *
+ * All the fields in the #GDataPicasaWebAlbumClass structure are private and should never be accessed directly.
+ *
+ * Since: 0.4.0
+ **/
+typedef struct {
+	/*< private >*/
+	GDataEntryClass parent;
+} GDataPicasaWebAlbumClass;
+
+GType gdata_picasaweb_album_get_type (void) G_GNUC_CONST;
+
+GDataPicasaWebAlbum *gdata_picasaweb_album_new (const gchar *id) G_GNUC_WARN_UNUSED_RESULT;
+GDataPicasaWebAlbum *gdata_picasaweb_album_new_from_xml (const gchar *xml, gint length, GError **error) G_GNUC_WARN_UNUSED_RESULT;
+
+const gchar *gdata_picasaweb_album_get_user (GDataPicasaWebAlbum *self);
+const gchar *gdata_picasaweb_album_get_nickname (GDataPicasaWebAlbum *self);
+void gdata_picasaweb_album_get_edited (GDataPicasaWebAlbum *self, GTimeVal *edited);
+const gchar *gdata_picasaweb_album_get_name (GDataPicasaWebAlbum *self);
+const gchar *gdata_picasaweb_album_get_location (GDataPicasaWebAlbum *self);
+void gdata_picasaweb_album_set_location (GDataPicasaWebAlbum *self, const gchar *location);
+GDataPicasaWebVisibility gdata_picasaweb_album_get_visibility (GDataPicasaWebAlbum *self);
+void gdata_picasaweb_album_set_visibility (GDataPicasaWebAlbum *self, GDataPicasaWebVisibility visibility);
+void gdata_picasaweb_album_get_timestamp (GDataPicasaWebAlbum *self, GTimeVal *timestamp);
+void gdata_picasaweb_album_set_timestamp (GDataPicasaWebAlbum *self, GTimeVal *timestamp);
+guint gdata_picasaweb_album_get_num_photos (GDataPicasaWebAlbum *self);
+guint gdata_picasaweb_album_get_num_photos_remaining (GDataPicasaWebAlbum *self);
+glong gdata_picasaweb_album_get_bytes_used (GDataPicasaWebAlbum *self);
+gboolean gdata_picasaweb_album_is_commenting_enabled (GDataPicasaWebAlbum *self);
+void gdata_picasaweb_album_set_is_commenting_enabled (GDataPicasaWebAlbum *self, gboolean is_commenting_enabled);
+guint gdata_picasaweb_album_get_comment_count (GDataPicasaWebAlbum *self);
+const gchar *gdata_picasaweb_album_get_tags (GDataPicasaWebAlbum *self);
+void gdata_picasaweb_album_set_tags (GDataPicasaWebAlbum *self, const gchar *tags);
+const gchar *gdata_picasaweb_album_get_description (GDataPicasaWebAlbum *self);
+void gdata_picasaweb_album_set_description (GDataPicasaWebAlbum *self, const gchar *description);
+GList *gdata_picasaweb_album_get_contents (GDataPicasaWebAlbum *self);
+GList *gdata_picasaweb_album_get_thumbnails (GDataPicasaWebAlbum *self);
+
+G_END_DECLS
+
+#endif /* !GDATA_PICASAWEB_ALBUM_H */
diff --git a/gdata/services/picasaweb/gdata-picasaweb-file.c b/gdata/services/picasaweb/gdata-picasaweb-file.c
new file mode 100644
index 0000000..9a89f74
--- /dev/null
+++ b/gdata/services/picasaweb/gdata-picasaweb-file.c
@@ -0,0 +1,1303 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Richard Schwarting 2009 <aquarichy gmail com>
+ * Copyright (C) Philip Withnall 2009 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION:gdata-picasaweb-file
+ * @short_description: GData PicasaWeb file object
+ * @stability: Unstable
+ * @include: gdata/services/picasaweb/gdata-picasaweb-file.h
+ *
+ * #GDataPicasaWebFile is a subclass of #GDataEntry to represent a file in an album on Google PicasaWeb.
+ *
+ * For more details of Google PicasaWeb's GData API, see the
+ * <ulink type="http" url="http://code.google.com/apis/picasaweb/developers_guide_protocol.html";>online documentation</ulink>.
+ **/
+
+#include <config.h>
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <libxml/parser.h>
+#include <string.h>
+
+#include "gdata-picasaweb-file.h"
+#include "gdata-private.h"
+#include "gdata-service.h"
+#include "gdata-parser.h"
+#include "gdata-types.h"
+#include "media/gdata-media-group.h"
+
+static void gdata_picasaweb_file_dispose (GObject *object);
+static void gdata_picasaweb_file_finalize (GObject *object);
+static void gdata_picasaweb_file_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec);
+static void gdata_picasaweb_file_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec);
+static void get_xml (GDataParsable *parsable, GString *xml_string);
+static gboolean parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error);
+static void get_namespaces (GDataParsable *parsable, GHashTable *namespaces);
+
+struct _GDataPicasaWebFilePrivate {
+	GTimeVal edited;
+	gchar *version;
+	gdouble position;
+	gchar *album_id;
+	guint width;
+	guint height;
+	gsize size;
+	gchar *client;
+	gchar *checksum;
+	GTimeVal timestamp;
+	gboolean is_commenting_enabled;
+	guint comment_count;
+	guint rotation;
+	gchar *video_status;
+
+	/* media:group */
+	GDataMediaGroup *media_group;
+
+	/* georss:where properties */
+	/* TODO these specify a location, like the following example.
+	 * Perhaps investigate whether we can do anything with geoclue/libchamplain
+<georss:where>
+	<gml:Envelope>
+		<gml:lowerCorner>45.2404179 11.7382049</gml:lowerCorner>
+		<gml:upperCorner>45.6278166 12.5196074</gml:upperCorner>
+	</gml:Envelope>
+	<gml:Point>
+		<gml:pos>45.4341173 12.1289062</gml:pos>
+	</gml:Point>
+</georss:where>
+	*/
+
+	/* exif:tags */
+	/* TODO yay, we want these :)
+<exif:tags>
+	<exif:fstop>2.8</exif:fstop>
+	<exif:make>EASTMAN KODAK COMPANY</exif:make>
+	<exif:model>KODAK Z740 ZOOM DIGITAL CAMERA</exif:model>
+	<exif:exposure>0.016666668</exif:exposure>
+	<exif:flash>true</exif:flash>
+	<exif:focallength>6.3</exif:focallength>
+	<exif:iso>80</exif:iso>
+	<exif:time>1228588330000</exif:time>
+	<exif:imageUniqueID>1c179e0ac4f6741c8c1cdda3516e69e5</exif:imageUniqueID>
+</exif:tags>
+	*/
+};
+
+enum {
+	PROP_EDITED = 1,
+	PROP_VERSION,
+	PROP_POSITION,
+	PROP_ALBUM_ID,
+	PROP_WIDTH,
+	PROP_HEIGHT,
+	PROP_SIZE,
+	PROP_CLIENT,
+	PROP_CHECKSUM,
+	PROP_TIMESTAMP,
+	PROP_IS_COMMENTING_ENABLED,
+	PROP_COMMENT_COUNT, /* TODO support comments */
+	PROP_ROTATION,
+	PROP_VIDEO_STATUS,
+	PROP_CREDIT,
+	PROP_CAPTION,
+	PROP_TAGS
+};
+
+G_DEFINE_TYPE (GDataPicasaWebFile, gdata_picasaweb_file, GDATA_TYPE_ENTRY)
+#define GDATA_PICASAWEB_FILE_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GDATA_TYPE_PICASAWEB_FILE, GDataPicasaWebFilePrivate))
+
+static void
+gdata_picasaweb_file_class_init (GDataPicasaWebFileClass *klass)
+{
+	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+	GDataParsableClass *parsable_class = GDATA_PARSABLE_CLASS (klass);
+
+	g_type_class_add_private (klass, sizeof (GDataPicasaWebFilePrivate));
+
+	gobject_class->get_property = gdata_picasaweb_file_get_property;
+	gobject_class->set_property = gdata_picasaweb_file_set_property;
+	gobject_class->dispose = gdata_picasaweb_file_dispose;
+	gobject_class->finalize = gdata_picasaweb_file_finalize;
+
+	parsable_class->get_xml = get_xml;
+	parsable_class->parse_xml = parse_xml;
+	parsable_class->get_namespaces = get_namespaces;
+
+	/**
+	 * GDataPicasaWebFile:version:
+	 *
+	 * The version number of the file. Version numbers are based on modification time, so they don't increment linearly.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_version";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_VERSION,
+					 g_param_spec_string ("version",
+							      "Version", "The version number of the file.",
+							      NULL,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebFile:album-id:
+	 *
+	 * The ID for the file's album.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_albumid";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_ALBUM_ID,
+					 g_param_spec_string ("album-id",
+							      "Album ID", "The ID for the file's album.",
+							      NULL,
+							      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebFile:client:
+	 *
+	 * The name of the software which created or last modified the file.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_client";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_CLIENT,
+					 g_param_spec_string ("client",
+							      "Client", "The name of the software which created or last modified the file.",
+							      NULL,
+							      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+	/**
+	 * GDataPicasaWebFile:checksum:
+	 *
+	 * A checksum of the file, useful for duplicate detection.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_checksum";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_CHECKSUM,
+					 g_param_spec_string ("checksum",
+							      "Checksum", "A checksum of the file, useful for duplicate detection.",
+							      NULL,
+							      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+	/**
+	 * GDataPicasaWebFile:video-status:
+	 *
+	 * The status of the file, if it is a video.
+	 *
+	 * Possible values include "pending", "ready", "final", and "failed".
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_videostatus";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_VIDEO_STATUS,
+					 g_param_spec_string ("video-status",
+							      "Video Status", "The status of the file, if it is a video.",
+							      NULL,
+							      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebFile:position:
+	 *
+	 * The ordinal position of the file within the album. Lower values mean the file will be closer to the start of the album.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_position";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_POSITION,
+					 g_param_spec_double ("position",
+							      "Position", "The ordinal position of the file within the album.",
+							      0.0, G_MAXFLOAT, 0.0,
+							      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+	/**
+	 * GDataPicasaWebFile:width:
+	 *
+	 * The width of the photo or video, in pixels.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_width";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_WIDTH,
+					 g_param_spec_uint ("width",
+							    "Width", "The width of the photo or video, in pixels.",
+							    0, G_MAXUINT, 0,
+							    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+	/**
+	 * GDataPicasaWebFile:height:
+	 *
+	 * The height of the photo or video, in pixels.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_height";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_HEIGHT,
+					 g_param_spec_uint ("height",
+							    "Height", "The height of the photo or video, in pixels.",
+							    0, G_MAXUINT, 0,
+							    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+	/**
+	 * GDataPicasaWebFile:size:
+	 *
+	 * The size of the file, in bytes.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_size";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_SIZE,
+					 g_param_spec_ulong ("size",
+							     "Size", "The size of the file, in bytes.",
+							     0, G_MAXULONG, 0,
+							     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebFile:edited:
+	 *
+	 * The time this file was last edited. If the file has not been edited yet, the content indicates the time it was created.
+	 *
+	 * For more information, see the <ulink type="http" url="http://www.atomenabled.org/developers/protocol/#appEdited";>
+	 * Atom Publishing Protocol specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_EDITED,
+					 g_param_spec_boxed ("edited",
+							     "Edited", "The time this file was last edited.",
+							     GDATA_TYPE_G_TIME_VAL,
+							     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+	/**
+	 * GDataPicasaWebFile:timestamp:
+	 *
+	 * The time the file was purportedly taken.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_timestamp";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	/* TODO: This should be the same as exif:timestamp. */
+	g_object_class_install_property (gobject_class, PROP_TIMESTAMP,
+					 g_param_spec_boxed ("timestamp",
+							     "Timestamp", "The time the file was purportedly taken.",
+							     GDATA_TYPE_G_TIME_VAL,
+							     G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+	/**
+	 * GDataPicasaWebFile:comment-count:
+	 *
+	 * The number of comments on the file.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_commentCount";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_COMMENT_COUNT,
+					 g_param_spec_uint ("comment-count",
+							    "Comment Count", "The number of comments on the file.",
+							    0, G_MAXUINT, 0,
+							    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+	/**
+	 * GDataPicasaWebFile:rotation:
+	 *
+	 * The rotation of the photo, in degrees. This will only be non-zero for files which are pending rotation, and haven't yet been
+	 * permanently modified. For files which have already been rotated, this will be %0.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#gphoto_rotation";>
+	 * gphoto specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_ROTATION,
+					 g_param_spec_uint ("rotation",
+							    "Rotation", "The rotation of the photo, in degrees.",
+							    0, 359, 0,
+							    G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+	/**
+	 * GDataPicasaWebFile:is-commenting-enabled:
+	 *
+	 * Whether commenting is enabled for this file.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_IS_COMMENTING_ENABLED,
+					 g_param_spec_boolean ("is-commenting-enabled",
+							       "Commenting enabled?", "Indicates whether comments are enabled.",
+							       TRUE,
+							       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebFile:credit:
+	 *
+	 * The nickname of the user credited with this file.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#media_credit";>Media RSS
+	 * specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_CREDIT,
+					 g_param_spec_string ("credit",
+							      "Credit", "The nickname of the user credited with this file.",
+							      NULL,
+							      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebFile:caption:
+	 *
+	 * The file's descriptive caption.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_CAPTION,
+					 g_param_spec_string ("caption",
+							      "Caption", "The file's descriptive caption.",
+							      NULL,
+							      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebFile:tags:
+	 *
+	 * A comma-separated list of tags associated with the file.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#media_keywords";>
+	 * Media RSS specification</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_TAGS,
+					 g_param_spec_string ("tags",
+							      "Tags", "A comma-separated list of tags associated with the file.",
+							      NULL,
+							      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+}
+
+static void
+notify_title_cb (GDataPicasaWebFile *self, GParamSpec *pspec, gpointer user_data)
+{
+	/* Keep the atom:title and media:group/media:title in sync */
+	if (self->priv->media_group != NULL)
+		gdata_media_group_set_title (self->priv->media_group, gdata_entry_get_title (GDATA_ENTRY (self)));
+}
+
+static void
+notify_summary_cb (GDataPicasaWebFile *self, GParamSpec *pspec, gpointer user_data)
+{
+	/* Keep the atom:summary and media:group/media:description in sync */
+	if (self->priv->media_group != NULL)
+		gdata_media_group_set_description (self->priv->media_group, gdata_entry_get_summary (GDATA_ENTRY (self)));
+}
+
+static void
+gdata_picasaweb_file_init (GDataPicasaWebFile *self)
+{
+	self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_PICASAWEB_FILE, GDataPicasaWebFilePrivate);
+	self->priv->media_group = g_object_new (GDATA_TYPE_MEDIA_GROUP, NULL);
+	self->priv->is_commenting_enabled = TRUE;
+
+	/* We need to keep atom:title (the canonical title for the file) in sync with media:group/media:title */
+	g_signal_connect (self, "notify::title", G_CALLBACK (notify_title_cb), NULL);
+	/* atom:summary (the canonical summary/caption for the file) in sync with media:group/media:description */
+	g_signal_connect (self, "notify::summary", G_CALLBACK (notify_summary_cb), NULL);
+}
+
+static void
+gdata_picasaweb_file_dispose (GObject *object)
+{
+	GDataPicasaWebFilePrivate *priv = GDATA_PICASAWEB_FILE_GET_PRIVATE (object);
+
+	if (priv->media_group != NULL)
+		g_object_unref (priv->media_group);
+	priv->media_group = NULL;
+
+	/* Chain up to the parent class */
+	G_OBJECT_CLASS (gdata_picasaweb_file_parent_class)->dispose (object);
+}
+
+static void
+gdata_picasaweb_file_finalize (GObject *object)
+{
+	GDataPicasaWebFilePrivate *priv = GDATA_PICASAWEB_FILE_GET_PRIVATE (object);
+
+	g_free (priv->version);
+	g_free (priv->album_id);
+	g_free (priv->client);
+	g_free (priv->checksum);
+	xmlFree (priv->video_status);
+
+	/* Chain up to the parent class */
+	G_OBJECT_CLASS (gdata_picasaweb_file_parent_class)->finalize (object);
+}
+
+static void
+gdata_picasaweb_file_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
+{
+	GDataPicasaWebFilePrivate *priv = GDATA_PICASAWEB_FILE_GET_PRIVATE (object);
+
+	switch (property_id) {
+		case PROP_EDITED:
+			g_value_set_boxed (value, &(priv->edited));
+			break;
+		case PROP_VERSION:
+			g_value_set_string (value, priv->version);
+			break;
+		case PROP_POSITION:
+			g_value_set_double (value, priv->position);
+			break;
+		case PROP_ALBUM_ID:
+			g_value_set_string (value, priv->album_id);
+			break;
+		case PROP_WIDTH:
+			g_value_set_uint (value, priv->width);
+			break;
+		case PROP_HEIGHT:
+			g_value_set_uint (value, priv->height);
+			break;
+		case PROP_SIZE:
+			g_value_set_ulong (value, priv->size);
+			break;
+		case PROP_CLIENT:
+			g_value_set_string (value, priv->client);
+			break;
+		case PROP_CHECKSUM:
+			g_value_set_string (value, priv->checksum);
+			break;
+		case PROP_TIMESTAMP:
+			g_value_set_boxed (value, &(priv->timestamp));
+			break;
+		case PROP_IS_COMMENTING_ENABLED:
+			g_value_set_boolean (value, priv->is_commenting_enabled);
+			break;
+		case PROP_COMMENT_COUNT:
+			g_value_set_uint (value, priv->comment_count);
+			break;
+		case PROP_ROTATION:
+			g_value_set_uint (value, priv->rotation);
+			break;
+		case PROP_VIDEO_STATUS:
+			g_value_set_string (value, priv->video_status);
+			break;
+		case PROP_CREDIT: {
+			GDataMediaCredit *credit = gdata_media_group_get_credit (priv->media_group);
+			g_value_set_string (value, gdata_media_credit_get_credit (credit));
+			break; }
+		case PROP_CAPTION:
+			g_value_set_string (value, gdata_entry_get_summary (GDATA_ENTRY (object)));
+			break;
+		case PROP_TAGS:
+			g_value_set_string (value, gdata_media_group_get_keywords (priv->media_group));
+			break;
+		default:
+			/* We don't have any other property... */
+			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+			break;
+	}
+}
+
+static void
+gdata_picasaweb_file_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
+{
+	GDataPicasaWebFile *self = GDATA_PICASAWEB_FILE (object);
+
+	switch (property_id) {
+		case PROP_VERSION:
+			/* Construct only */
+			g_free (self->priv->version);
+			self->priv->version = g_value_dup_string (value);
+			break;
+		case PROP_POSITION:
+			gdata_picasaweb_file_set_position (self, g_value_get_double (value));
+			break;
+		case PROP_ALBUM_ID:
+			/* TODO: do we allow this to change albums? I think that's how pictures are moved. */
+			gdata_picasaweb_file_set_album_id (self, g_value_get_string (value));
+			break;
+		case PROP_CLIENT:
+			gdata_picasaweb_file_set_client (self, g_value_get_string (value));
+			break;
+		case PROP_CHECKSUM:
+			gdata_picasaweb_file_set_checksum (self, g_value_get_string (value));
+			break;
+		case PROP_TIMESTAMP:
+			gdata_picasaweb_file_set_timestamp (self, g_value_get_boxed (value));
+			break;
+		case PROP_IS_COMMENTING_ENABLED: /* TODO I don't think we can change this on a per file basis */
+			gdata_picasaweb_file_set_is_commenting_enabled (self, g_value_get_boolean (value));
+			break;
+		case PROP_ROTATION:
+			gdata_picasaweb_file_set_rotation (self, g_value_get_uint (value));
+			break;
+		case PROP_CAPTION:
+			gdata_picasaweb_file_set_caption (self, g_value_get_string (value));
+			break;
+		case PROP_TAGS:
+			gdata_picasaweb_file_set_tags (self, g_value_get_string (value));
+			break;
+		default:
+			/* We don't have any other property... */
+			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+			break;
+	}
+}
+
+static gboolean
+parse_xml (GDataParsable *parsable, xmlDoc *doc, xmlNode *node, gpointer user_data, GError **error)
+{
+	GDataPicasaWebFile *self = GDATA_PICASAWEB_FILE (parsable);
+
+	if (xmlStrcmp (node->name, (xmlChar*) "group") == 0) {
+		/* media:group */
+		GDataMediaGroup *group = GDATA_MEDIA_GROUP (_gdata_parsable_new_from_xml_node (GDATA_TYPE_MEDIA_GROUP, "group", doc,
+											       node, NULL, error));
+		if (group == NULL)
+			return FALSE;
+
+		if (self->priv->media_group != NULL)
+			/* We should really error here, but we can't, as priv->media_group has to be pre-populated
+			 * in order for things like gdata_picasaweb_file_set_description() to work. */
+			g_object_unref (self->priv->media_group);
+
+		self->priv->media_group = group;
+	} else if (xmlStrcmp (node->name, (xmlChar*) "edited") == 0) {
+		/* app:edited */
+		xmlChar *edited = xmlNodeListGetString (doc, node->children, TRUE);
+		if (g_time_val_from_iso8601 ((gchar*) edited, &(self->priv->edited)) == FALSE) {
+			/* Error */
+			gdata_parser_error_not_iso8601_format (node, (gchar*) edited, error);
+			xmlFree (edited);
+			return FALSE;
+		}
+		xmlFree (edited);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "imageVersion") == 0) {
+		/* gphoto:imageVersion */
+		xmlChar *version = xmlNodeListGetString (doc, node->children, TRUE);
+		g_free (self->priv->version);
+		self->priv->version = g_strdup ((gchar*) version);
+		xmlFree (version);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "position") == 0) {
+		/* gphoto:position */
+		xmlChar *position_str = xmlNodeListGetString (doc, node->children, TRUE);
+		gdata_picasaweb_file_set_position (self, g_ascii_strtod ((gchar*) position_str, NULL));
+		xmlFree (position_str);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "albumid") == 0) {
+		/* gphoto:album_id */
+		xmlChar *album_id = xmlNodeListGetString (doc, node->children, TRUE);
+		gdata_picasaweb_file_set_album_id (self, (gchar*) album_id);
+		xmlFree (album_id);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "width") == 0) {
+		/* gphoto:width */
+		xmlChar *width = xmlNodeListGetString (doc, node->children, TRUE);
+		self->priv->width = strtoul ((gchar*) width, NULL, 10);
+		xmlFree (width);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "height") == 0) {
+		/* gphoto:height */
+		xmlChar *height = xmlNodeListGetString (doc, node->children, TRUE);
+		self->priv->height = strtoul ((gchar*) height, NULL, 10);
+		xmlFree (height);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "size") == 0) {
+		/* gphoto:size */
+		xmlChar *size = xmlNodeListGetString (doc, node->children, TRUE);
+		self->priv->size = strtoul ((gchar*) size, NULL, 10);
+		xmlFree (size);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "client") == 0) {
+		/* gphoto:client */
+		xmlChar *client = xmlNodeListGetString (doc, node->children, TRUE);
+		gdata_picasaweb_file_set_client (self, (gchar*) client);
+		xmlFree (client);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "checksum") == 0) {
+		/* gphoto:checksum */
+		xmlChar *checksum = xmlNodeListGetString (doc, node->children, TRUE);
+		gdata_picasaweb_file_set_checksum (self, (gchar*) checksum);
+		xmlFree (checksum);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "timestamp") == 0) {
+		/* gphoto:timestamp */
+		xmlChar *timestamp_str;
+		long long int milliseconds;
+		GTimeVal timestamp;
+
+		timestamp_str = xmlNodeListGetString (doc, node->children, TRUE);
+		milliseconds = strtoull ((gchar*) timestamp_str, NULL, 10);
+		xmlFree (timestamp_str);
+
+		timestamp.tv_sec = (glong) (milliseconds / 1000);
+		timestamp.tv_usec = (glong) ((milliseconds % 1000) * 1000);
+
+		gdata_picasaweb_file_set_timestamp (self, &timestamp);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "commentingEnabled") == 0) {
+		/* gphoto:commentingEnabled */
+		xmlChar *is_commenting_enabled = xmlNodeListGetString (doc, node->children, TRUE);
+		self->priv->is_commenting_enabled = (strncasecmp ("true", (gchar*) is_commenting_enabled, 5) == 0 ? TRUE : FALSE);
+		xmlFree (is_commenting_enabled);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "commentCount") == 0) {
+		/* gphoto:commentCount */
+		xmlChar *comment_count = xmlNodeListGetString (doc, node->children, TRUE);
+		self->priv->comment_count = strtoul ((gchar*) comment_count, NULL, 10);
+		xmlFree (comment_count);
+	} else if (xmlStrcmp (node->name, (xmlChar*) "videostatus") == 0) {
+		/* gphoto:videostatus */
+		xmlChar *video_status = xmlNodeListGetString (doc, node->children, TRUE);
+		if (self->priv->video_status != NULL) {
+			xmlFree (video_status);
+			return gdata_parser_error_duplicate_element (node, error);
+		}
+		self->priv->video_status = (gchar*) video_status;
+	} else if (xmlStrcmp (node->name, (xmlChar*) "rotation") == 0) {
+		/* gphoto:rotation */
+		xmlChar *rotation = xmlNodeListGetString (doc, node->children, TRUE);
+		gdata_picasaweb_file_set_rotation (self, strtoul ((gchar*) rotation, NULL, 10));
+		xmlFree (rotation);
+	} else if (GDATA_PARSABLE_CLASS (gdata_picasaweb_file_parent_class)->parse_xml (parsable, doc, node, user_data, error) == FALSE) {
+		/* Error! */
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static void
+get_xml (GDataParsable *parsable, GString *xml_string)
+{
+	gchar *xml;
+	GDataPicasaWebFilePrivate *priv = GDATA_PICASAWEB_FILE (parsable)->priv;
+
+	/* Chain up to the parent class */
+	GDATA_PARSABLE_CLASS (gdata_picasaweb_file_parent_class)->get_xml (parsable, xml_string);
+
+	/* Add all the PicasaWeb-specific XML */
+	if (priv->version != NULL)
+		g_string_append_printf (xml_string, "<gphoto:version>%s</gphoto:version>", priv->version);
+
+	g_string_append_printf (xml_string, "<gphoto:position>%f</gphoto:position>", priv->position);
+
+	if (priv->album_id != NULL)
+		g_string_append_printf (xml_string, "<gphoto:albumid>%s</gphoto:albumid>", priv->album_id);
+
+	if (priv->client != NULL) {
+		gchar *client = g_markup_escape_text (priv->client, -1);
+		g_string_append_printf (xml_string, "<gphoto:client>%s</gphoto:client>", client);
+		g_free (client);
+	}
+
+	if (priv->checksum != NULL) {
+		gchar *checksum = g_markup_escape_text (priv->checksum, -1);
+		g_string_append_printf (xml_string, "<gphoto:checksum>%s</gphoto:checksum>", checksum);
+		g_free (checksum);
+	}
+
+	if (priv->timestamp.tv_sec != 0 || priv->timestamp.tv_usec != 0) {
+		/* timestamp is in milliseconds */
+		g_string_append_printf (xml_string, "<gphoto:timestamp>%lu</gphoto:timestamp>",
+					priv->timestamp.tv_sec * 1000 + priv->timestamp.tv_usec);
+	}
+
+	if (priv->is_commenting_enabled == TRUE)
+		g_string_append (xml_string, "<gphoto:commentingEnabled>true</gphoto:commentingEnabled>");
+	else
+		g_string_append (xml_string, "<gphoto:commentingEnabled>false</gphoto:commentingEnabled>");
+
+	if (priv->rotation > 0)
+		g_string_append_printf (xml_string, "<gphoto:rotation>%u</gphoto:rotation>", priv->rotation);
+
+	/* media:group */
+	xml = _gdata_parsable_get_xml (GDATA_PARSABLE (priv->media_group), "media:group", FALSE);
+	g_string_append (xml_string, xml);
+	g_free (xml);
+
+	/* TODO:
+	 * - Finish supporting all tags
+	 * - Check all tags here are valid for insertions and updates
+	 * - Check things are escaped (or not) as appropriate
+	 * - Write a function to encapsulate g_markup_escape_text and
+	 *   g_string_append_printf to reduce the number of allocations
+	 */
+}
+
+static void
+get_namespaces (GDataParsable *parsable, GHashTable *namespaces)
+{
+	GDataPicasaWebFilePrivate *priv = GDATA_PICASAWEB_FILE (parsable)->priv;
+
+	/* Chain up to the parent class */
+	GDATA_PARSABLE_CLASS (gdata_picasaweb_file_parent_class)->get_namespaces (parsable, namespaces);
+
+	g_hash_table_insert (namespaces, (gchar*) "gphoto", (gchar*) "http://schemas.google.com/photos/2007";);
+	g_hash_table_insert (namespaces, (gchar*) "app", (gchar*) "http://www.w3.org/2007/app";);
+
+	/* Add the media:group namespaces */
+	GDATA_PARSABLE_GET_CLASS (priv->media_group)->get_namespaces (GDATA_PARSABLE (priv->media_group), namespaces);
+}
+
+/**
+ * gdata_picasaweb_file_new:
+ * @id: the file's ID, or %NULL
+ *
+ * Creates a new #GDataPicasaWebFile with the given ID and default properties.
+ *
+ * Return value: a new #GDataPicasaWebFile; unref with g_object_unref()
+ *
+ * Since: 0.4.0
+ **/
+GDataPicasaWebFile *
+gdata_picasaweb_file_new (const gchar *id)
+{
+	return g_object_new (GDATA_TYPE_PICASAWEB_FILE, "id", id, NULL);
+}
+
+/**
+ * gdata_picasaweb_file_new_from_xml:
+ * @xml: an XML string
+ * @length: the length in characters of @xml, or %-1
+ * @error: a #GError, or %NULL
+ *
+ * Creates a new #GDataPicasaWebFile from an XML string. If @length is %-1, the length of
+ * the string will be calculated.
+ *
+ * Errors from #GDataParserError can be returned if problems are found in the XML.
+ *
+ * Return value: a new #GDataPicasaWebFile, or %NULL; unref with g_object_unref()
+ *
+ * Since: 0.4.0
+ **/
+GDataPicasaWebFile *
+gdata_picasaweb_file_new_from_xml (const gchar *xml, gint length, GError **error)
+{
+	return GDATA_PICASAWEB_FILE (_gdata_entry_new_from_xml (GDATA_TYPE_PICASAWEB_FILE, xml, length, error));
+}
+
+/**
+ * gdata_picasaweb_file_get_edited:
+ * @self: a #GDataPicasaWebFile
+ * @edited: a #GTimeVal
+ *
+ * Gets the #GDataPicasaWebFile:edited property and puts it in @edited. If the property is unset,
+ * both fields in the #GTimeVal will be set to %0.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_file_get_edited (GDataPicasaWebFile *self, GTimeVal *edited)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_FILE (self));
+	g_return_if_fail (edited != NULL);
+	*edited = self->priv->edited;
+}
+
+/**
+ * gdata_picasaweb_file_get_version:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:version property.
+ *
+ * Return value: the file's version number, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_file_get_version (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), NULL);
+	return self->priv->version;
+}
+
+/**
+ * gdata_picasaweb_file_get_position:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:position property.
+ *
+ * Return value: the file's ordinal position in the album
+ *
+ * Since: 0.4.0
+ **/
+gdouble
+gdata_picasaweb_file_get_position (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), 0.0);
+	return self->priv->position;
+}
+
+/**
+ * gdata_picasaweb_file_set_position:
+ * @self: a #GDataPicasaWebFile
+ * @position: the file's new position in the album
+ *
+ * Sets the #GDataPicasaWebFile:position property.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_file_set_position (GDataPicasaWebFile *self, gdouble position)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_FILE (self));
+	self->priv->position = position;
+	g_object_notify (G_OBJECT (self), "position");
+}
+
+/**
+ * gdata_picasaweb_file_get_album_id:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:album-id property.
+ *
+ * Return value: the ID of the album containing the #GDataPicasaWebFile
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_file_get_album_id (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), NULL);
+	return self->priv->album_id;
+}
+
+/**
+ * gdata_picasaweb_file_set_album_id:
+ * @self: a #GDataPicasaWebFile
+ * @album_id: the ID of the new album for this file
+ *
+ * Sets the #GDataPicasaWebFile:album-id property, effectively moving the file to the album.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_file_set_album_id (GDataPicasaWebFile *self, const gchar *album_id)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_FILE (self));
+	g_return_if_fail (album_id != NULL && *album_id != '\0');
+
+	g_free (self->priv->album_id);
+	self->priv->album_id = g_strdup (album_id);
+	g_object_notify (G_OBJECT (self), "album-id");
+}
+
+/**
+ * gdata_picasaweb_file_get_width:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:width property.
+ *
+ * Return value: the width of the image or video, in pixels
+ *
+ * Since: 0.4.0
+ **/
+guint
+gdata_picasaweb_file_get_width (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), 0);
+	return self->priv->width;
+}
+
+/**
+ * gdata_picasaweb_file_get_height:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:height property.
+ *
+ * Return value: the height of the image or video, in pixels
+ *
+ * Since: 0.4.0
+ **/
+guint
+gdata_picasaweb_file_get_height (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), 0);
+	return self->priv->height;
+}
+
+/**
+ * gdata_picasaweb_file_get_size:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:size property.
+ *
+ * Return value: the size of the file, in bytes
+ *
+ * Since: 0.4.0
+ **/
+gsize
+gdata_picasaweb_file_get_size (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), 0);
+	return self->priv->size;
+}
+
+/**
+ * gdata_picasaweb_file_get_client:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:client property.
+ *
+ * Return value: the name of the software which created the photo, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_file_get_client (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), NULL);
+	return self->priv->client;
+}
+
+/**
+ * gdata_picasaweb_file_set_client:
+ * @self: a #GDataPicasaWebFile
+ * @client: the name of the software which created or modified the photo, or %NULL
+ *
+ * Sets the #GDataPicasaWebFile:client property to @client.
+ *
+ * Set @client to %NULL to unset the property.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_file_set_client (GDataPicasaWebFile *self, const gchar *client)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_FILE (self));
+
+	g_free (self->priv->client);
+	self->priv->client = g_strdup (client);
+	g_object_notify (G_OBJECT (self), "client");
+}
+
+/**
+ * gdata_picasaweb_file_get_checksum:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:checksum property.
+ *
+ * Return value: the checksum assigned to this file, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_file_get_checksum (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), NULL);
+	return self->priv->checksum;
+}
+
+/**
+ * gdata_picasaweb_file_set_checksum:
+ * @self: a #GDataPicasaWebFile
+ * @checksum: the new checksum for this file, or %NULL
+ *
+ * Sets the #GDataPicasaWebFile:checksum property to @checksum.
+ *
+ * Set @checksum to %NULL to unset the property.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_file_set_checksum (GDataPicasaWebFile *self, const gchar *checksum)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_FILE (self));
+
+	g_free (self->priv->checksum);
+	self->priv->checksum = g_strdup (checksum);
+	g_object_notify (G_OBJECT (self), "checksum");
+}
+
+/**
+ * gdata_picasaweb_file_get_timestamp:
+ * @self: a #GDataPicasaWebFile
+ * @timestamp: a #GTimeVal
+ *
+ * Gets the #GDataPicasaWebFile:timestamp property and puts it in @timestamp. If the property is unset,
+ * both fields in the #GTimeVal will be set to %0.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_file_get_timestamp (GDataPicasaWebFile *self, GTimeVal *timestamp)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_FILE (self));
+	g_return_if_fail (timestamp != NULL);
+	*timestamp = self->priv->timestamp;
+}
+
+/**
+ * gdata_picasaweb_file_set_timestamp:
+ * @self: a #GDataPicasaWebFile
+ * @timestamp: a #GTimeVal, or %NULL
+ *
+ * Sets the #GDataPicasaWebFile:timestamp property from values supplied by @timestamp. If @timestamp is %NULL,
+ * the property will be unset.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_file_set_timestamp (GDataPicasaWebFile *self, GTimeVal *timestamp)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_FILE (self));
+	if (timestamp == NULL)
+		self->priv->timestamp.tv_sec = self->priv->timestamp.tv_usec = 0;
+	else
+		self->priv->timestamp = *timestamp;
+	g_object_notify (G_OBJECT (self), "timestamp");
+}
+
+/**
+ * gdata_picasaweb_file_is_commenting_enabled:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:is-commenting-enabled property.
+ *
+ * Return value: %TRUE if commenting is enabled, %FALSE otherwise
+ *
+ * Since: 0.4.0
+ **/
+gboolean
+gdata_picasaweb_file_is_commenting_enabled (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), FALSE);
+	return self->priv->is_commenting_enabled;
+}
+
+/**
+ * gdata_picasaweb_file_set_is_commenting_enabled:
+ * @self: a #GDataPicasaWebFile
+ * @is_commenting_enabled: %TRUE if commenting should be enabled for the file, %FALSE otherwise
+ *
+ * Sets the #GDataPicasaWebFile:is-commenting-enabled property to @is_commenting_enabled.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_file_set_is_commenting_enabled (GDataPicasaWebFile *self, gboolean is_commenting_enabled)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_FILE (self));
+	self->priv->is_commenting_enabled = is_commenting_enabled;
+	g_object_notify (G_OBJECT (self), "is-commenting-enabled");
+}
+
+/**
+ * gdata_picasaweb_file_get_comment_count:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:comment-count property.
+ *
+ * Return value: the number of comments on the file
+ *
+ * Since: 0.4.0
+ **/
+guint
+gdata_picasaweb_file_get_comment_count (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), 0);
+	return self->priv->comment_count;
+}
+
+/**
+ * gdata_picasaweb_file_get_rotation:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:rotation property.
+ *
+ * Return value: the image's rotation, in degrees
+ *
+ * Since: 0.4.0
+ **/
+guint
+gdata_picasaweb_file_get_rotation (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), 0);
+	return self->priv->rotation;
+}
+
+/**
+ * gdata_picasaweb_file_set_rotation:
+ * @self: a #GDataPicasaWebFile
+ * @rotation: the new rotation for the image, in degrees
+ *
+ * Sets the #GDataPicasaWebFile:rotation property to @rotation.
+ *
+ * The rotation is absolute, rather than cumulative, through successive calls to gdata_picasaweb_file_set_rotation(),
+ * so calling it with 90° then 20° will result in a final rotation of 20°.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_file_set_rotation (GDataPicasaWebFile *self, guint rotation)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_FILE (self));
+	self->priv->rotation = rotation % 360;
+	g_object_notify (G_OBJECT (self), "rotation");
+}
+
+
+/**
+ * gdata_picasaweb_file_get_video_status:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:video-status property.
+ *
+ * Return value: the status of this video ("pending", "ready", "final" or "failed"), or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_file_get_video_status (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), NULL);
+	return self->priv->video_status;
+}
+
+/**
+ * gdata_picasaweb_file_get_tags:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:tags property.
+ *
+ * Return value: a comma-separated list of tags associated with the file, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_file_get_tags (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), NULL);
+	return gdata_media_group_get_keywords (self->priv->media_group);
+}
+
+/**
+ * gdata_picasaweb_file_set_tags:
+ * @self: a #GDataPicasaWebFile
+ * @tags: a new comma-separated list of tags, or %NULL
+ *
+ * Sets the #GDataPicasaWebFile:tags property to @tags.
+ *
+ * Set @tags to %NULL to unset the property.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_file_set_tags (GDataPicasaWebFile *self, const gchar *tags)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_FILE (self));
+
+	gdata_media_group_set_keywords (self->priv->media_group, tags);
+	g_object_notify (G_OBJECT (self), "tags");
+}
+
+/**
+ * gdata_picasaweb_file_get_credit:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:credit property.
+ *
+ * Return value: the nickname of the user credited with this file
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_file_get_credit (GDataPicasaWebFile *self)
+{
+	GDataMediaCredit *credit;
+
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), NULL);
+
+	credit = gdata_media_group_get_credit (self->priv->media_group);
+	return (credit == NULL) ? NULL : gdata_media_credit_get_credit (credit);
+}
+
+/**
+ * gdata_picasaweb_file_get_caption:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Gets the #GDataPicasaWebFile:caption property.
+ *
+ * Return value: the file's descriptive caption, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_file_get_caption (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), NULL);
+	return gdata_entry_get_summary (GDATA_ENTRY (self));
+}
+
+/**
+ * gdata_picasaweb_file_set_caption:
+ * @self: a #GDataPicasaWebFile
+ * @caption: the file's new caption, or %NULL
+ *
+ * Sets the #GDataPicasaWebFile:caption property to @caption.
+ *
+ * Set @caption to %NULL to unset the file's caption.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_file_set_caption (GDataPicasaWebFile *self, const gchar *caption)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_FILE (self));
+
+	gdata_entry_set_summary (GDATA_ENTRY (self), caption);
+	gdata_media_group_set_description (self->priv->media_group, caption);
+	g_object_notify (G_OBJECT (self), "caption");
+}
+
+/**
+ * gdata_picasaweb_file_get_contents:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Returns a list of media content, e.g. the actual photo or video.
+ *
+ * Return value: a #GList of #GDataMediaContent items
+ *
+ * Since: 0.4.0
+ **/
+GList *
+gdata_picasaweb_file_get_contents (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), NULL);
+	return gdata_media_group_get_contents (self->priv->media_group);
+}
+
+/**
+ * gdata_picasaweb_file_get_thumbnails:
+ * @self: a #GDataPicasaWebFile
+ *
+ * Returns a list of thumbnails, often at different sizes, for this file.
+ *
+ * Return value: a #GList of #GDataMediaThumbnail<!-- -->s, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+GList *
+gdata_picasaweb_file_get_thumbnails (GDataPicasaWebFile *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (self), NULL);
+	return gdata_media_group_get_thumbnails (self->priv->media_group);
+}
diff --git a/gdata/services/picasaweb/gdata-picasaweb-file.h b/gdata/services/picasaweb/gdata-picasaweb-file.h
new file mode 100644
index 0000000..a7f1763
--- /dev/null
+++ b/gdata/services/picasaweb/gdata-picasaweb-file.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Richard Schwarting 2009 <aquarichy gmail com>
+ * Copyright (C) Philip Withnall 2009 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GDATA_PICASAWEB_FILE_H
+#define GDATA_PICASAWEB_FILE_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gdata/gdata-entry.h>
+#include <gdata/gdata-types.h>
+
+G_BEGIN_DECLS
+
+#define GDATA_TYPE_PICASAWEB_FILE 		(gdata_picasaweb_file_get_type ())
+#define GDATA_PICASAWEB_FILE(o)			(G_TYPE_CHECK_INSTANCE_CAST ((o), GDATA_TYPE_PICASAWEB_FILE, GDataPicasaWebFile))
+#define GDATA_PICASAWEB_FILE_CLASS(k)		(G_TYPE_CHECK_CLASS_CAST((k), GDATA_TYPE_PICASAWEB_FILE, GDataPicasaWebFileClass))
+#define GDATA_IS_PICASAWEB_FILE(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), GDATA_TYPE_PICASAWEB_FILE))
+#define GDATA_IS_PICASAWEB_FILE_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), GDATA_TYPE_PICASAWEB_FILE))
+#define GDATA_PICASAWEB_FILE_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), GDATA_TYPE_PICASAWEB_FILE, GDataPicasaWebFileClass))
+
+typedef struct _GDataPicasaWebFilePrivate	GDataPicasaWebFilePrivate;
+
+/**
+ * GDataPicasaWebFile:
+ *
+ * All the fields in the #GDataPicasaWebFile structure are private and should never be accessed directly.
+ *
+ * Since: 0.4.0
+ **/
+typedef struct {
+	GDataEntry parent;
+	GDataPicasaWebFilePrivate *priv;
+} GDataPicasaWebFile;
+
+/**
+ * GDataPicasaWebFileClass:
+ *
+ * All the fields in the #GDataPicasaWebFileClass structure are private and should never be accessed directly.
+ *
+ * Since: 0.4.0
+ **/
+typedef struct {
+	/*< private >*/
+	GDataEntryClass parent;
+} GDataPicasaWebFileClass;
+
+GType gdata_picasaweb_file_get_type (void) G_GNUC_CONST;
+
+GDataPicasaWebFile *gdata_picasaweb_file_new (const gchar *id) G_GNUC_WARN_UNUSED_RESULT;
+GDataPicasaWebFile *gdata_picasaweb_file_new_from_xml (const gchar *xml, gint length, GError **error) G_GNUC_WARN_UNUSED_RESULT;
+
+void gdata_picasaweb_file_get_edited (GDataPicasaWebFile *self, GTimeVal *edited);
+const gchar *gdata_picasaweb_file_get_version (GDataPicasaWebFile *self);
+gdouble gdata_picasaweb_file_get_position (GDataPicasaWebFile *self);
+void gdata_picasaweb_file_set_position (GDataPicasaWebFile *self, gdouble position);
+const gchar *gdata_picasaweb_file_get_album_id (GDataPicasaWebFile *self);
+void gdata_picasaweb_file_set_album_id (GDataPicasaWebFile *self, const gchar *album_id); /* TODO should we have a more obvious _move() API too? nah */
+guint gdata_picasaweb_file_get_width (GDataPicasaWebFile *self);
+guint gdata_picasaweb_file_get_height (GDataPicasaWebFile *self);
+gulong gdata_picasaweb_file_get_size (GDataPicasaWebFile *self);
+const gchar *gdata_picasaweb_file_get_client (GDataPicasaWebFile *self);
+void gdata_picasaweb_file_set_client (GDataPicasaWebFile *self, const gchar *client);
+const gchar *gdata_picasaweb_file_get_checksum (GDataPicasaWebFile *self);
+void gdata_picasaweb_file_set_checksum (GDataPicasaWebFile *self, const gchar *checksum);
+void gdata_picasaweb_file_get_timestamp (GDataPicasaWebFile *self, GTimeVal *timestamp);
+void gdata_picasaweb_file_set_timestamp (GDataPicasaWebFile *self, GTimeVal *timestamp);
+gboolean gdata_picasaweb_file_is_commenting_enabled (GDataPicasaWebFile *self);
+void gdata_picasaweb_file_set_is_commenting_enabled (GDataPicasaWebFile *self, gboolean is_commenting_enabled);
+guint gdata_picasaweb_file_get_comment_count (GDataPicasaWebFile *self);
+guint gdata_picasaweb_file_get_rotation (GDataPicasaWebFile *self);
+void gdata_picasaweb_file_set_rotation (GDataPicasaWebFile *self, guint rotation);
+const gchar *gdata_picasaweb_file_get_video_status (GDataPicasaWebFile *self);
+const gchar *gdata_picasaweb_file_get_tags (GDataPicasaWebFile *self);
+void gdata_picasaweb_file_set_tags (GDataPicasaWebFile *self, const gchar *tags);
+const gchar *gdata_picasaweb_file_get_credit (GDataPicasaWebFile *self);
+const gchar *gdata_picasaweb_file_get_caption (GDataPicasaWebFile *self);
+void gdata_picasaweb_file_set_caption (GDataPicasaWebFile *self, const gchar *caption);
+GList *gdata_picasaweb_file_get_contents (GDataPicasaWebFile *self);
+GList *gdata_picasaweb_file_get_thumbnails (GDataPicasaWebFile *self);
+
+/* TODO implement get exif */
+/* TODO implement get thumbnail (from media?) */
+/* TODO implement get link */
+/* TODO implement is video */
+/* TODO implement get content; in what form? */
+/* TODO implement get tags */
+/* TODO implement get comments, get num comments */
+/* TODO implement get location */
+
+G_END_DECLS
+
+#endif /* !GDATA_PICASAWEB_FILE_H */
diff --git a/gdata/services/picasaweb/gdata-picasaweb-query.c b/gdata/services/picasaweb/gdata-picasaweb-query.c
new file mode 100644
index 0000000..7001fad
--- /dev/null
+++ b/gdata/services/picasaweb/gdata-picasaweb-query.c
@@ -0,0 +1,547 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Richard Schwarting 2009 <aquarichy gmail com>
+ * Copyright (C) Philip Withnall 2009 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION:gdata-picasaweb-query
+ * @short_description: GData PicasaWeb query object
+ * @stability: Unstable
+ * @include: gdata/services/picasaweb/gdata-picasaweb-query.h
+ *
+ * #GDataPicasaWebQuery represents a collection of query parameters specific to the Google PicasaWeb service, which go above and beyond
+ * those catered for by #GDataQuery.
+ *
+ * For more information on the custom GData query parameters supported by #GDataPicasaWebQuery, see the <ulink type="http"
+ * url="http://code.google.com/apis/picasaweb/reference.html#Parameters";>online documentation</ulink>.
+ **/
+
+#include <config.h>
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <string.h>
+
+#include "gdata-picasaweb-query.h"
+#include "gdata-query.h"
+#include "gdata-picasaweb-enums.h"
+
+static void gdata_picasaweb_query_finalize (GObject *object);
+static void gdata_picasaweb_query_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec);
+static void gdata_picasaweb_query_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec);
+static void get_query_uri (GDataQuery *self, const gchar *feed_uri, GString *query_uri, gboolean *params_started);
+
+struct _GDataPicasaWebQueryPrivate {
+	GDataPicasaWebVisibility visibility;
+	gchar *thumbnail_size;
+	gchar *image_size;
+	gchar *tag;
+	gchar *location;
+
+	struct {
+		gdouble north;
+		gdouble east;
+		gdouble south;
+		gdouble west;
+	} bounding_box;
+};
+
+enum {
+	PROP_VISIBILITY = 1,
+	PROP_THUMBNAIL_SIZE,
+	PROP_IMAGE_SIZE,
+	PROP_TAG,
+	PROP_LOCATION
+};
+
+G_DEFINE_TYPE (GDataPicasaWebQuery, gdata_picasaweb_query, GDATA_TYPE_QUERY)
+#define GDATA_PICASAWEB_QUERY_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GDATA_TYPE_PICASAWEB_QUERY, GDataPicasaWebQueryPrivate))
+
+static void
+gdata_picasaweb_query_class_init (GDataPicasaWebQueryClass *klass)
+{
+	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+	GDataQueryClass *query_class = GDATA_QUERY_CLASS (klass);
+
+	g_type_class_add_private (klass, sizeof (GDataPicasaWebQueryPrivate));
+
+	gobject_class->get_property = gdata_picasaweb_query_get_property;
+	gobject_class->set_property = gdata_picasaweb_query_set_property;
+	gobject_class->finalize = gdata_picasaweb_query_finalize;
+
+	query_class->get_query_uri = get_query_uri;
+
+	/**
+	 * GDataPicasaWebQuery:visibility:
+	 *
+	 * Specifies which albums should be listed, in terms of their visibility (#GDataPicasaWebAlbum:visibility).
+	 *
+	 * Set the property to %0 to list all albums, regardless of their visibility. Otherwise, use values from #GDataPicasaWebVisibility.
+	 *
+	 * For more information, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#Visibility";>
+	 * online documentation</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_VISIBILITY,
+				g_param_spec_int ("visibility",
+					"Visibility", "Specifies which albums should be listed, in terms of their visibility.",
+					0, GDATA_PICASAWEB_PRIVATE, 0,
+					G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebQuery:thumbnail-size:
+	 *
+	 * A comma-separated list of thumbnail sizes (width in pixels) to return. Only certain sizes are allowed, and whether the thumbnail should be
+	 * cropped or scaled can be specified; for more information, see the
+	 * <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#Parameters";>online documentation</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_THUMBNAIL_SIZE,
+				g_param_spec_string ("thumbnail-size",
+					"Thumbnail size", "A comma-separated list of thumbnail sizes (width in pixels) to return.",
+					NULL,
+					G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebQuery:image-size:
+	 *
+	 * A comma-separated list of image sizes (width in pixels) to return. Only certain sizes are allowed, and whether the image should be
+	 * cropped or scaled can be specified; for more information, see the
+	 * <ulink type="http" url="http://code.google.com/apis/picasaweb/reference.html#Parameters";>online documentation</ulink>.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_IMAGE_SIZE,
+				g_param_spec_string ("image-size",
+					"Image size", "A comma-separated list of image sizes (width in pixels) to return.",
+					NULL,
+					G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebQuery:tag:
+	 *
+	 * A tag which returned results must contain.
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_TAG,
+				g_param_spec_string ("tag",
+					"Tag", "A tag which returned results must contain.",
+					NULL,
+					G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataPicasaWebQuery:location:
+	 *
+	 * A location to search for photos, e.g. "London".
+	 *
+	 * Since: 0.4.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_LOCATION,
+				g_param_spec_string ("location",
+					"Location", "A location to search for photos, e.g. \"London\".",
+					NULL,
+					G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+}
+
+static void
+gdata_picasaweb_query_init (GDataPicasaWebQuery *self)
+{
+	self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_PICASAWEB_QUERY, GDataPicasaWebQueryPrivate);
+}
+
+static void
+gdata_picasaweb_query_finalize (GObject *object)
+{
+	GDataPicasaWebQueryPrivate *priv = GDATA_PICASAWEB_QUERY_GET_PRIVATE (object);
+
+	g_free (priv->thumbnail_size);
+	g_free (priv->image_size);
+	g_free (priv->tag);
+	g_free (priv->location);
+
+	/* Chain up to the parent class */
+	G_OBJECT_CLASS (gdata_picasaweb_query_parent_class)->finalize (object);
+}
+
+static void
+gdata_picasaweb_query_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
+{
+	GDataPicasaWebQueryPrivate *priv = GDATA_PICASAWEB_QUERY_GET_PRIVATE (object);
+
+	switch (property_id) {
+		case PROP_VISIBILITY:
+			g_value_set_int (value, priv->visibility);
+			break;
+		case PROP_THUMBNAIL_SIZE:
+			g_value_set_string (value, priv->thumbnail_size);
+			break;
+		case PROP_IMAGE_SIZE:
+			g_value_set_string (value, priv->image_size);
+			break;
+		case PROP_TAG:
+			g_value_set_string (value, priv->tag);
+			break;
+		case PROP_LOCATION:
+			g_value_set_string (value, priv->location);
+			break;
+		default:
+			/* We don't have any other property... */
+			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+			break;
+	}
+}
+
+static void
+gdata_picasaweb_query_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
+{
+	GDataPicasaWebQuery *self = GDATA_PICASAWEB_QUERY (object);
+
+	switch (property_id) {
+		case PROP_VISIBILITY:
+			gdata_picasaweb_query_set_visibility (self, g_value_get_int (value));
+			break;
+		case PROP_THUMBNAIL_SIZE:
+			gdata_picasaweb_query_set_thumbnail_size (self, g_value_get_string (value));
+			break;
+		case PROP_IMAGE_SIZE:
+			gdata_picasaweb_query_set_image_size (self, g_value_get_string (value));
+			break;
+		case PROP_TAG:
+			gdata_picasaweb_query_set_tag (self, g_value_get_string (value));
+			break;
+		case PROP_LOCATION:
+			gdata_picasaweb_query_set_location (self, g_value_get_string (value));
+			break;
+		default:
+			/* We don't have any other property... */
+			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+			break;
+	}
+}
+
+static void
+get_query_uri (GDataQuery *self, const gchar *feed_uri, GString *query_uri, gboolean *params_started)
+{
+	GDataPicasaWebQueryPrivate *priv = GDATA_PICASAWEB_QUERY (self)->priv;
+
+	#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_picasaweb_query_parent_class)->get_query_uri (self, feed_uri, query_uri, params_started);
+
+	APPEND_SEP
+	if (priv->visibility == 0)
+		g_string_append (query_uri, "access=all");
+	else if (priv->visibility == GDATA_PICASAWEB_PUBLIC)
+		g_string_append (query_uri, "access=public");
+	else if (priv->visibility == GDATA_PICASAWEB_PRIVATE)
+		g_string_append (query_uri, "access=private");
+	else
+		g_assert_not_reached ();
+
+	if (priv->thumbnail_size != NULL) {
+		APPEND_SEP
+		g_string_append (query_uri, "thumbsize=");
+		g_string_append_uri_escaped (query_uri, priv->thumbnail_size, NULL, FALSE);
+	}
+
+	if (priv->image_size != NULL) {
+		APPEND_SEP
+		g_string_append (query_uri, "imgmax=");
+		g_string_append_uri_escaped (query_uri, priv->image_size, NULL, FALSE);
+	}
+
+	if (priv->tag != NULL) {
+		APPEND_SEP
+		g_string_append (query_uri, "tag=");
+		g_string_append_uri_escaped (query_uri, priv->tag, NULL, TRUE);
+	}
+
+	if (priv->bounding_box.north != priv->bounding_box.south && priv->bounding_box.east != priv->bounding_box.west) {
+		APPEND_SEP
+		g_string_append_printf (query_uri, "bbox=%f,%f,%f,%f",
+					priv->bounding_box.west, priv->bounding_box.south,
+					priv->bounding_box.east, priv->bounding_box.north);
+	}
+
+	if (priv->location != NULL) {
+		APPEND_SEP
+		g_string_append (query_uri, "l=");
+		g_string_append_uri_escaped (query_uri, priv->location, NULL, TRUE);
+	}
+}
+
+/**
+ * gdata_picasaweb_query_new:
+ * @q: a query string
+ *
+ * Creates a new #GDataPicasaWebQuery with its #GDataQuery:q property set to @q.
+ *
+ * Return value: a new #GDataPicasaWebQuery
+ *
+ * Since: 0.4.0
+ **/
+GDataPicasaWebQuery *
+gdata_picasaweb_query_new (const gchar *q)
+{
+	return g_object_new (GDATA_TYPE_PICASAWEB_QUERY, "q", q, NULL);
+}
+
+/**
+ * gdata_picasaweb_query_get_visibility:
+ * @self: a #GDataPicasaWebQuery
+ *
+ * Gets the #GDataPicasaWebQuery:visibility property.
+ *
+ * Return value: the visibility of the objects to retrieve, or %0 to retrieve all objects
+ *
+ * Since: 0.4.0
+ **/
+GDataPicasaWebVisibility
+gdata_picasaweb_query_get_visibility (GDataPicasaWebQuery *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_QUERY (self), 0);
+	return self->priv->visibility;
+}
+
+/**
+ * gdata_picasaweb_query_set_visibility:
+ * @self: a #GDataPicasaWebQuery
+ * @visibility: the visibility of the objects to retrieve, or %0 to retrieve all objects
+ *
+ * Sets the #GDataPicasaWebQuery:visibility property to @visibility.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_query_set_visibility (GDataPicasaWebQuery *self, GDataPicasaWebVisibility visibility)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_QUERY (self));
+	self->priv->visibility = visibility;
+	g_object_notify (G_OBJECT (self), "visibility");
+}
+
+/**
+ * gdata_picasaweb_query_get_thumbnail_size:
+ * @self: a #GDataPicasaWebQuery
+ *
+ * Gets the #GDataPicasaWebQuery:thumbnail-size property.
+ *
+ * Return value: a comma-separated list of thumbnail sizes to retrieve, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_query_get_thumbnail_size (GDataPicasaWebQuery *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_QUERY (self), NULL);
+	return self->priv->thumbnail_size;
+}
+
+/**
+ * gdata_picasaweb_query_set_thumbnail_size:
+ * @self: a #GDataPicasaWebQuery
+ * @thumbnail_size: a comma-separated list of thumbnail sizes to retrieve, or %NULL
+ *
+ * Sets the #GDataPicasaWebQuery:thumbnail-size property to @thumbnail_size.
+ *
+ * Set @thumbnail_size to %NULL to unset the property.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_query_set_thumbnail_size (GDataPicasaWebQuery *self, const gchar *thumbnail_size)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_QUERY (self));
+
+	g_free (self->priv->thumbnail_size);
+	self->priv->thumbnail_size = g_strdup (thumbnail_size);
+	g_object_notify (G_OBJECT (self), "thumbnail-size");
+}
+
+/**
+ * gdata_picasaweb_query_get_image_size:
+ * @self: a #GDataPicasaWebQuery
+ *
+ * Gets the #GDataPicasaWebQuery:image-size property.
+ *
+ * Return value: a comma-separated list of image sizes to retrieve, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_query_get_image_size (GDataPicasaWebQuery *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_QUERY (self), NULL);
+	return self->priv->image_size;
+}
+
+/**
+ * gdata_picasaweb_query_set_image_size:
+ * @self: a #GDataPicasaWebQuery
+ * @image_size: a comma-separated list of image sizes to retrieve, or %NULL
+ *
+ * Sets the #GDataPicasaWebQuery:image-size property to @image_size.
+ *
+ * Set @image_size to %NULL to unset the property.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_query_set_image_size (GDataPicasaWebQuery *self, const gchar *image_size)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_QUERY (self));
+
+	g_free (self->priv->image_size);
+	self->priv->image_size = g_strdup (image_size);
+	g_object_notify (G_OBJECT (self), "image-size");
+}
+
+/**
+ * gdata_picasaweb_query_get_tag:
+ * @self: a #GDataPicasaWebQuery
+ *
+ * Gets the #GDataPicasaWebQuery:tag property.
+ *
+ * Return value: a tag which retrieved objects must have, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_query_get_tag (GDataPicasaWebQuery *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_QUERY (self), NULL);
+	return self->priv->tag;
+}
+
+/**
+ * gdata_picasaweb_query_set_tag:
+ * @self: a #GDataPicasaWebQuery
+ * @tag: a tag which retrieved objects must have, or %NULL
+ *
+ * Sets the #GDataPicasaWebQuery:tag property to @tag.
+ *
+ * Set @tag to %NULL to unset the property.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_query_set_tag (GDataPicasaWebQuery *self, const gchar *tag)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_QUERY (self));
+
+	g_free (self->priv->tag);
+	self->priv->tag = g_strdup (tag);
+	g_object_notify (G_OBJECT (self), "tag");
+}
+
+/**
+ * gdata_picasaweb_query_get_bounding_box:
+ * @self: a #GDataPicasaWebQuery
+ * @north: return location for the latitude of the top of the box, or %NULL
+ * @east: return location for the longitude of the right of the box, or %NULL
+ * @south: return location for the latitude of the south of the box, or %NULL
+ * @west: return location for the longitude of the left of the box, or %NULL
+ *
+ * Gets the latitudes and longitudes of a bounding box, inside which all the results must lie.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_query_get_bounding_box (GDataPicasaWebQuery *self, gdouble *north, gdouble *east, gdouble *south, gdouble *west)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_QUERY (self));
+
+	if (north != NULL)
+		*north = self->priv->bounding_box.north;
+	if (east != NULL)
+		*east = self->priv->bounding_box.east;
+	if (south != NULL)
+		*south = self->priv->bounding_box.south;
+	if (west != NULL)
+		*west = self->priv->bounding_box.west;
+}
+
+/**
+ * gdata_picasaweb_query_set_bounding_box:
+ * @self: a #GDataPicasaWebQuery
+ * @north: latitude of the top of the box
+ * @east: longitude of the right of the box
+ * @south: latitude of the bottom of the box
+ * @west: longitude of the left of the box
+ *
+ * Sets a bounding box, inside which all the returned results must lie.
+ *
+ * Set @north, @east, @south and @west to %0 to unset the property.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_query_set_bounding_box (GDataPicasaWebQuery *self, gdouble north, gdouble east, gdouble south, gdouble west)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_QUERY (self));
+	g_return_if_fail (north >= -90.0 && north <= 90.0);
+	g_return_if_fail (south >= -90.0 && south <= 90.0);
+	g_return_if_fail (east >= -180.0 && east <= 180.0);
+	g_return_if_fail (west >= -180.0 && west <= 180.0);
+
+	self->priv->bounding_box.north = north;
+	self->priv->bounding_box.east = east;
+	self->priv->bounding_box.south = south;
+	self->priv->bounding_box.west = west;
+}
+
+/**
+ * gdata_picasaweb_query_get_location:
+ * @self: a #GDataPicasaWebQuery
+ *
+ * Gets the #GDataPicasaWebQuery:location property.
+ *
+ * Return value: a location which returned objects must be near, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+const gchar *
+gdata_picasaweb_query_get_location (GDataPicasaWebQuery *self)
+{
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_QUERY (self), NULL);
+	return self->priv->location;
+}
+
+/**
+ * gdata_picasaweb_query_set_location:
+ * @self: a #GDataPicasaWebQuery
+ * @location: a location which returned objects must be near, or %NULL
+ *
+ * Sets the #GDataPicasaWebQuery:location property to @location.
+ *
+ * Set @location to %NULL to unset the property.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_query_set_location (GDataPicasaWebQuery *self, const gchar *location)
+{
+	g_return_if_fail (GDATA_IS_PICASAWEB_QUERY (self));
+
+	g_free (self->priv->location);
+	self->priv->location = g_strdup (location);
+	g_object_notify (G_OBJECT (self), "location");
+}
diff --git a/gdata/services/picasaweb/gdata-picasaweb-query.h b/gdata/services/picasaweb/gdata-picasaweb-query.h
new file mode 100644
index 0000000..9e55704
--- /dev/null
+++ b/gdata/services/picasaweb/gdata-picasaweb-query.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Richard Schwarting 2009 <aquarichy gmail com>
+ * Copyright (C) Philip Withnall 2009 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GDATA_PICASAWEB_QUERY_H
+#define GDATA_PICASAWEB_QUERY_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gdata/gdata-query.h>
+#include <gdata/services/picasaweb/gdata-picasaweb-album.h>
+
+G_BEGIN_DECLS
+
+#define GDATA_TYPE_PICASAWEB_QUERY		(gdata_picasaweb_query_get_type ())
+#define GDATA_PICASAWEB_QUERY(o)			(G_TYPE_CHECK_INSTANCE_CAST ((o), GDATA_TYPE_PICASAWEB_QUERY, GDataPicasaWebQuery))
+#define GDATA_PICASAWEB_QUERY_CLASS(k)		(G_TYPE_CHECK_CLASS_CAST((k), GDATA_TYPE_PICASAWEB_QUERY, GDataPicasawebQueryClass))
+#define GDATA_IS_PICASAWEB_QUERY(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), GDATA_TYPE_PICASAWEB_QUERY))
+#define GDATA_IS_PICASAWEB_QUERY_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), GDATA_TYPE_PICASAWEB_QUERY))
+#define GDATA_PICASAWEB_QUERY_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), GDATA_TYPE_PICASAWEB_QUERY, GDataPicasawebQueryClass))
+
+typedef struct _GDataPicasaWebQueryPrivate	GDataPicasaWebQueryPrivate;
+
+/**
+ * GDataPicasaWebQuery:
+ *
+ * All the fields in the #GDataPicasaWebQuery structure are private and should never be accessed directly.
+ *
+ * Since: 0.4.0
+ **/
+typedef struct {
+	GDataQuery parent;
+	GDataPicasaWebQueryPrivate *priv;
+} GDataPicasaWebQuery;
+
+/**
+ * GDataPicasaWebQueryClass:
+ *
+ * All the fields in the #GDataPicasaWebQueryClass structure are private and should never be accessed directly.
+ *
+ * Since: 0.4.0
+ **/
+typedef struct {
+	/*< private >*/
+	GDataQueryClass parent;
+} GDataPicasaWebQueryClass;
+
+GType gdata_picasaweb_query_get_type (void) G_GNUC_CONST;
+
+GDataPicasaWebQuery *gdata_picasaweb_query_new (const gchar *q) G_GNUC_WARN_UNUSED_RESULT;
+
+GDataPicasaWebVisibility gdata_picasaweb_query_get_visibility (GDataPicasaWebQuery *self);
+void gdata_picasaweb_query_set_visibility (GDataPicasaWebQuery *self, GDataPicasaWebVisibility visibility);
+const gchar *gdata_picasaweb_query_get_thumbnail_size (GDataPicasaWebQuery *self);
+void gdata_picasaweb_query_set_thumbnail_size (GDataPicasaWebQuery *self, const gchar *thumbnail_size);
+const gchar *gdata_picasaweb_query_get_image_size (GDataPicasaWebQuery *self);
+void gdata_picasaweb_query_set_image_size (GDataPicasaWebQuery *self, const gchar *image_size);
+const gchar *gdata_picasaweb_query_get_tag (GDataPicasaWebQuery *self);
+void gdata_picasaweb_query_set_tag (GDataPicasaWebQuery *self, const gchar *tag);
+void gdata_picasaweb_query_get_bounding_box (GDataPicasaWebQuery *self, gdouble *north, gdouble *east, gdouble *south, gdouble *west);
+void gdata_picasaweb_query_set_bounding_box (GDataPicasaWebQuery *self, gdouble north, gdouble east, gdouble south, gdouble west);
+const gchar *gdata_picasaweb_query_get_location (GDataPicasaWebQuery *self);
+void gdata_picasaweb_query_set_location (GDataPicasaWebQuery *self, const gchar *location);
+
+G_END_DECLS
+
+#endif /* !GDATA_PICASAWEB_QUERY_H */
diff --git a/gdata/services/picasaweb/gdata-picasaweb-service.c b/gdata/services/picasaweb/gdata-picasaweb-service.c
new file mode 100644
index 0000000..c343a4a
--- /dev/null
+++ b/gdata/services/picasaweb/gdata-picasaweb-service.c
@@ -0,0 +1,397 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Richard Schwarting 2009 <aquarichy gmail com>
+ * Copyright (C) Philip Withnall 2009 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION:gdata-picasaweb-service
+ * @short_description: GData PicasaWeb service object
+ * @stability: Unstable
+ * @include: gdata/services/picasaweb/gdata-picasaweb-service.h
+ *
+ * #GDataPicasaWebService is a subclass of #GDataService for communicating with the GData API of Google PicasaWeb. It supports querying for files
+ * and albums, and uploading files.
+ *
+ * For more details of PicasaWeb's GData API, see the <ulink type="http" url="http://code.google.com/apis/picasaweb/developers_guide_protocol.html";>
+ * online documentation</ulink>.
+ **/
+
+#include <config.h>
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include "gdata-service.h"
+#include "gdata-picasaweb-service.h"
+#include "gdata-private.h"
+#include "gdata-parser.h"
+#include "atom/gdata-link.h"
+
+G_DEFINE_TYPE (GDataPicasaWebService, gdata_picasaweb_service, GDATA_TYPE_SERVICE)
+
+static void
+gdata_picasaweb_service_class_init (GDataPicasaWebServiceClass *klass)
+{
+	GDataServiceClass *service_class = GDATA_SERVICE_CLASS (klass);
+	service_class->service_name = "lh2";
+}
+
+static void
+gdata_picasaweb_service_init (GDataPicasaWebService *self)
+{
+	/* Nothing to see here */
+}
+
+/*
+ * This constructs the URI we want to access for querying albums.
+ *
+ * Remember to free the URI in the caller.
+*/
+static gchar *
+create_uri (GDataPicasaWebService *self, const gchar *username)
+{
+	if (username == NULL) {
+		/* Ensure we're authenticated first */
+		if (gdata_service_is_authenticated (GDATA_SERVICE (self)) == FALSE)
+			return NULL;
+
+		/* Querying Picasa albums for the "default" user when logged in returns the albums for the authenticated user */
+		username = "default";
+	}
+
+	return g_strdup_printf ("http://picasaweb.google.com/data/feed/api/user/%s";, username);
+}
+
+/**
+ * gdata_picasaweb_service_new:
+ * @client_id: your application's client ID
+ *
+ * Creates a new #GDataPicasaWebService. The @client_id must be unique for your application, and as registered with Google.
+ *
+ * Return value: a new #GDataPicasaWebService, or %NULL
+ *
+ * Since: 0.4.0
+ **/
+GDataPicasaWebService *
+gdata_picasaweb_service_new (const gchar *client_id)
+{
+	g_return_val_if_fail (client_id != NULL, NULL);
+
+	return g_object_new (GDATA_TYPE_PICASAWEB_SERVICE,
+			     "client-id", client_id,
+			     NULL);
+}
+
+/**
+ * gdata_picasaweb_service_query_all_albums:
+ * @self: a #GDataPicasaWebService
+ * @query: a #GDataQuery with the query parameters, or %NULL
+ * @username: the username of the user whose albums you wish to retrieve, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @progress_callback: a #GDataQueryProgressCallback to call when an entry is loaded, or %NULL
+ * @progress_user_data: data to pass to the @progress_callback function
+ * @error: a #GError, or %NULL
+ *
+ * Queries the service to return a list of all albums belonging to the specified @username which match the given
+ * @query. If a user is authenticated with the service, @username can be set as %NULL to return a list of albums belonging
+ * to the currently-authenticated user.
+ *
+ * For more details, see gdata_service_query().
+ *
+ * Return value: a #GDataFeed of query results; unref with g_object_unref()
+ *
+ * Since: 0.4.0
+ **/
+GDataFeed *
+gdata_picasaweb_service_query_all_albums (GDataPicasaWebService *self, GDataQuery *query, const gchar *username, GCancellable *cancellable,
+					  GDataQueryProgressCallback progress_callback, gpointer progress_user_data, GError **error)
+{
+	gchar *uri;
+	GDataFeed *album_feed;
+
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_SERVICE (self), NULL);
+	g_return_val_if_fail (query == NULL || GDATA_IS_QUERY (query), NULL);
+
+	uri = create_uri (self, username);
+	if (uri == NULL) {
+		g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED,
+				     _("You must specify a username or be authenticated to query all albums."));
+		return NULL;
+	}
+
+	/* Execute the query */
+	album_feed = gdata_service_query (GDATA_SERVICE (self), uri, query, GDATA_TYPE_PICASAWEB_ALBUM,
+					  cancellable, progress_callback, progress_user_data, error);
+	g_free (uri);
+
+	return album_feed;
+}
+
+/**
+ * gdata_picasaweb_service_query_all_albums_async:
+ * @self: a #GDataPicasaWebService
+ * @query: a #GDataQuery with the query parameters, or %NULL
+ * @username: the username of the user whose albums you wish to retrieve, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @progress_callback: a #GDataQueryProgressCallback to call when an entry is loaded, or %NULL
+ * @progress_user_data: data to pass to the @progress_callback function
+ * @callback: a #GAsyncReadyCallback to call when authentication is finished
+ * @user_data: data to pass to the @callback function
+ *
+ * Queries the service to return a list of all albums belonging to the specified @username which match the given
+ * @query. @self, @query and @username are all reffed/copied when this function is called, so can safely be unreffed/freed after
+ * this function returns.
+ *
+ * For more details, see gdata_picasaweb_service_query_all_albums(), which is the synchronous version of
+ * this function, and gdata_service_query_async(), which is the base asynchronous query function.
+ *
+ * Since: 0.4.0
+ **/
+void
+gdata_picasaweb_service_query_all_albums_async (GDataPicasaWebService *self, GDataQuery *query, const gchar *username,
+						GCancellable *cancellable, GDataQueryProgressCallback progress_callback, gpointer progress_user_data,
+						GAsyncReadyCallback callback, gpointer user_data)
+{
+	gchar *uri;
+
+	g_return_if_fail (GDATA_IS_PICASAWEB_SERVICE (self));
+	g_return_if_fail (query == NULL || GDATA_IS_QUERY (query));
+	g_return_if_fail (callback != NULL);
+
+	uri = create_uri (self, username);
+	if (uri == NULL) {
+		g_simple_async_report_error_in_idle (G_OBJECT (self), callback, user_data,
+						     GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED,
+						     _("You must specify a username or be authenticated to query all albums."));
+		return;
+	}
+
+	/* Schedule the async query */
+	gdata_service_query_async (GDATA_SERVICE (self), uri, query, GDATA_TYPE_PICASAWEB_ALBUM, cancellable, progress_callback, progress_user_data,
+				   callback, user_data);
+	g_free (uri);
+}
+
+/**
+ * gdata_picasaweb_service_query_files:
+ * @self: a #GDataPicasaWebService
+ * @album: a #GDataPicasaWebAlbum from which to retrieve the files, or %NULL
+ * @query: a #GDataQuery with the query parameters, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @progress_callback: a #GDataQueryProgressCallback to call when an entry is loaded, or %NULL
+ * @progress_user_data: data to pass to the @progress_callback function
+ * @error: a #GError, or %NULL
+ *
+ * Queries the specified @album for a list of the files which match the given @query. If @album is %NULL and a user is
+ * authenticated with the service, the user's default album will be queried.
+ *
+ * For more details, see gdata_service_query().
+ *
+ * Return value: a #GDataFeed of query results; unref with g_object_unref()
+ *
+ * Since: 0.4.0
+ **/
+GDataFeed *
+gdata_picasaweb_service_query_files (GDataPicasaWebService *self, GDataPicasaWebAlbum *album, GDataQuery *query, GCancellable *cancellable,
+				     GDataQueryProgressCallback progress_callback, gpointer progress_user_data, GError **error)
+{
+	/* TODO: Async variant */
+	const gchar *uri;
+
+	if (album != NULL) {
+		GDataLink *link = gdata_entry_look_up_link (GDATA_ENTRY (album), "http://schemas.google.com/g/2005#feed";);
+		if (link == NULL) {
+			/* Error */
+			g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
+					     _("The album did not have a feed link."));
+			return NULL;
+		}
+		uri = gdata_link_get_uri (link);
+	} else {
+		/* Default URI */
+		uri = "http://picasaweb.google.com/data/feed/api/user/default/albumid/default";;
+	}
+
+	/* Execute the query */
+	return gdata_service_query (GDATA_SERVICE (self), uri, GDATA_QUERY (query), GDATA_TYPE_PICASAWEB_FILE, cancellable,
+				    progress_callback, progress_user_data, error);
+}
+
+/**
+ * gdata_picasaweb_service_upload_file:
+ * @self: a #GDataPicasaWebService
+ * @album: a #GDataPicasaWebAlbum into which to insert the file, or %NULL
+ * @file: a #GDataPicasaWebFile to insert
+ * @actual_file: the actual file to upload
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Uploads a file (photo or video) to the given PicasaWeb @album, using the @actual_file from disk and the metadata from @file. If @album is
+ * %NULL, the file will be uploaded to the currently-authenticated user's "Drop Box" album. A user must be authenticated to use this function.
+ *
+ * If @file has already been inserted, a %GDATA_SERVICE_ERROR_ENTRY_ALREADY_INSERTED error will be returned. If no user is authenticated
+ * with the service, %GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED will be returned.
+ *
+ * If there is a problem reading @actual_file, an error from g_file_load_contents() or g_file_query_info() will be returned. Other errors from
+ * #GDataServiceError can be returned for other exceptional conditions, as determined by the server.
+ *
+ * Return value: the inserted #GDataPicasaWebFile with updated properties from @file; unref with g_object_unref()
+ *
+ * Since: 0.4.0
+ **/
+GDataPicasaWebFile *
+gdata_picasaweb_service_upload_file (GDataPicasaWebService *self, GDataPicasaWebAlbum *album, GDataPicasaWebFile *file, GFile *actual_file,
+				     GCancellable *cancellable, GError **error)
+{
+	/* TODO: Async variant */
+	#define BOUNDARY_STRING "0xdeadbeef6e0808d5e6ed8bc168390bcc"
+
+	GDataServiceClass *klass;
+	SoupMessage *message;
+	gchar *entry_xml, *upload_uri, *second_chunk_header, *upload_data, *file_contents, *i;
+	const gchar *first_chunk_header, *footer, *album_id, *user_id;
+	guint status;
+	GFileInfo *actual_file_info;
+	gsize content_length, first_chunk_header_length, second_chunk_header_length, entry_xml_length, file_length, footer_length;
+
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_SERVICE (self), NULL);
+	g_return_val_if_fail (GDATA_IS_PICASAWEB_FILE (file), NULL);
+	g_return_val_if_fail (G_IS_FILE (actual_file), NULL);
+
+	if (gdata_entry_is_inserted (GDATA_ENTRY (file)) == TRUE) {
+		g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_ENTRY_ALREADY_INSERTED,
+				     _("The entry has already been inserted."));
+		return NULL;
+	}
+
+	if (gdata_service_is_authenticated (GDATA_SERVICE (self)) == FALSE) {
+		g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED,
+				     _("You must be authenticated to upload a file."));
+		return NULL;
+	}
+
+	/* PicasaWeb allows you to post to a default Dropbox */
+	if (album == NULL)
+		album_id = "default";
+	else
+		album_id = gdata_entry_get_id (GDATA_ENTRY (album));
+	user_id = gdata_service_get_username (GDATA_SERVICE (self));
+
+	upload_uri = g_strdup_printf ("http://picasaweb.google.com/data/feed/api/user/%s/albumid/%s";, user_id, album_id);
+	message = soup_message_new (SOUP_METHOD_POST, upload_uri);
+	g_free (upload_uri);
+
+	/* Make sure subclasses set their headers */
+	klass = GDATA_SERVICE_GET_CLASS (self);
+	if (klass->append_query_headers != NULL)
+		klass->append_query_headers (GDATA_SERVICE (self), message);
+
+	/* Get the data early so we can calculate the content length */
+	if (g_file_load_contents (actual_file, NULL, &file_contents, &file_length, NULL, error) == FALSE) {
+		g_object_unref (message);
+		return NULL;
+	}
+
+	entry_xml = gdata_entry_get_xml (GDATA_ENTRY (file));
+
+	/* Check for cancellation */
+	if (g_cancellable_set_error_if_cancelled (cancellable, error) == TRUE) {
+		g_object_unref (message);
+		g_free (entry_xml);
+		return NULL;
+	}
+
+	actual_file_info = g_file_query_info (actual_file, "standard::display-name,standard::content-type", G_FILE_QUERY_INFO_NONE, NULL, error);
+	if (actual_file_info == NULL) {
+		g_object_unref (message);
+		g_free (entry_xml);
+		return NULL;
+	}
+
+	/* Check for cancellation */
+	if (g_cancellable_set_error_if_cancelled (cancellable, error) == TRUE) {
+		g_object_unref (message);
+		g_free (entry_xml);
+		g_object_unref (actual_file_info);
+		return NULL;
+	}
+
+	/* Add file-upload--specific headers */
+	soup_message_headers_append (message->request_headers, "Slug", g_file_info_get_display_name (actual_file_info));
+
+	first_chunk_header = "--" BOUNDARY_STRING "\nContent-Type: application/atom+xml; charset=UTF-8\n\n<?xml version='1.0'?>";
+	second_chunk_header = g_strdup_printf ("\n--" BOUNDARY_STRING "\nContent-Type: %s\nContent-Transfer-Encoding: binary\n\n",
+					       g_file_info_get_content_type (actual_file_info));
+	footer = "\n--" BOUNDARY_STRING "--";
+
+	g_object_unref (actual_file_info);
+
+	first_chunk_header_length = strlen (first_chunk_header);
+	second_chunk_header_length = strlen (second_chunk_header);
+	footer_length = strlen (footer);
+	entry_xml_length = strlen (entry_xml);
+
+	content_length = first_chunk_header_length + entry_xml_length + second_chunk_header_length + file_length + footer_length;
+
+	/* Build the upload data */
+	upload_data = i = g_malloc (content_length);
+
+	memcpy (upload_data, first_chunk_header, first_chunk_header_length);
+	i += first_chunk_header_length;
+
+	memcpy (i, entry_xml, entry_xml_length);
+	i += entry_xml_length;
+	g_free (entry_xml);
+
+	memcpy (i, second_chunk_header, second_chunk_header_length);
+	g_free (second_chunk_header);
+	i += second_chunk_header_length;
+
+	memcpy (i, file_contents, file_length);
+	g_free (file_contents);
+	i += file_length;
+
+	memcpy (i, footer, footer_length);
+
+	/* Append the data */
+	soup_message_set_request (message, "multipart/related; boundary=" BOUNDARY_STRING, SOUP_MEMORY_TAKE, upload_data, content_length);
+
+	/* Send the message */
+	status = _gdata_service_send_message (GDATA_SERVICE (self), message, error);
+	if (status == SOUP_STATUS_NONE) {
+		g_object_unref (message);
+		return NULL;
+	}
+
+	/* Check for cancellation */
+	if (g_cancellable_set_error_if_cancelled (cancellable, error) == TRUE) {
+		g_object_unref (message);
+		return NULL;
+	}
+
+	if (status != 201) {
+		/* Error */
+		klass->parse_error_response (GDATA_SERVICE (self), GDATA_SERVICE_ERROR_WITH_INSERTION, status, message->reason_phrase,
+					     message->response_body->data, message->response_body->length, error);
+		g_object_unref (message);
+		return NULL;
+	}
+
+	g_assert (message->response_body->data != NULL);
+
+	return gdata_picasaweb_file_new_from_xml (message->response_body->data, (gint) message->response_body->length, error);
+}
diff --git a/gdata/services/picasaweb/gdata-picasaweb-service.h b/gdata/services/picasaweb/gdata-picasaweb-service.h
new file mode 100644
index 0000000..a4c28a6
--- /dev/null
+++ b/gdata/services/picasaweb/gdata-picasaweb-service.h
@@ -0,0 +1,87 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Richard Schwarting 2009 <aquarichy gmail com>
+ * Copyright (C) Philip Withnall 2009 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GDATA_PICASAWEB_SERVICE_H
+#define GDATA_PICASAWEB_SERVICE_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gdata/gdata-service.h>
+#include <gdata/services/picasaweb/gdata-picasaweb-album.h>
+
+G_BEGIN_DECLS
+
+#define GDATA_TYPE_PICASAWEB_SERVICE		(gdata_picasaweb_service_get_type ())
+#define GDATA_PICASAWEB_SERVICE(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), GDATA_TYPE_PICASAWEB_SERVICE, GDataPicasaWebService))
+#define GDATA_PICASAWEB_SERVICE_CLASS(k)	(G_TYPE_CHECK_CLASS_CAST((k), GDATA_TYPE_PICASAWEB_SERVICE, GDataPicasaWebServiceClass))
+#define GDATA_IS_PICASAWEB_SERVICE(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), GDATA_TYPE_PICASAWEB_SERVICE))
+#define GDATA_IS_PICASAWEB_SERVICE_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), GDATA_TYPE_PICASAWEB_SERVICE))
+#define GDATA_PICASAWEB_SERVICE_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), GDATA_TYPE_PICASAWEB_SERVICE, GDataPicasaWebServiceClass))
+
+/**
+ * GDataPicasaWebService:
+ *
+ * All the fields in the #GDataPicasaWebService structure are private and should never be accessed directly.
+ *
+ * Since: 0.4.0
+ **/
+typedef struct {
+	GDataService parent;
+} GDataPicasaWebService;
+
+/**
+ * GDataPicasaWebServiceClass:
+ *
+ * All the fields in the #GDataPicasaWebServiceClass structure are private and should never be accessed directly.
+ *
+ * Since: 0.4.0
+ **/
+typedef struct {
+	/*< private >*/
+	GDataServiceClass parent;
+} GDataPicasaWebServiceClass;
+
+GType gdata_picasaweb_service_get_type (void) G_GNUC_CONST;
+
+GDataPicasaWebService *gdata_picasaweb_service_new (const gchar *client_id) G_GNUC_WARN_UNUSED_RESULT;
+
+#include <gdata/services/picasaweb/gdata-picasaweb-query.h>
+
+GDataFeed *gdata_picasaweb_service_query_all_albums (GDataPicasaWebService *self, GDataQuery *query, const gchar *username,
+						     GCancellable *cancellable, GDataQueryProgressCallback progress_callback, gpointer progress_user_data,
+						     GError **error) G_GNUC_WARN_UNUSED_RESULT;
+void gdata_picasaweb_service_query_all_albums_async (GDataPicasaWebService *self, GDataQuery *query, const gchar *username,
+						     GCancellable *cancellable, GDataQueryProgressCallback progress_callback, gpointer progress_user_data,
+						     GAsyncReadyCallback callback, gpointer user_data);
+
+GDataFeed *gdata_picasaweb_service_query_files (GDataPicasaWebService *self, GDataPicasaWebAlbum *album, GDataQuery *query,
+						GCancellable *cancellable, GDataQueryProgressCallback progress_callback, gpointer progress_user_data,
+						GError **error) G_GNUC_WARN_UNUSED_RESULT;
+
+#include <gdata/services/picasaweb/gdata-picasaweb-file.h>
+
+GDataPicasaWebFile *gdata_picasaweb_service_upload_file (GDataPicasaWebService *self, GDataPicasaWebAlbum *album, GDataPicasaWebFile *file,
+							 GFile *actual_file, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT;
+/* TODO: async version */
+
+G_END_DECLS
+
+#endif /* !GDATA_PICASAWEB_SERVICE_H */
diff --git a/gdata/tests/Makefile.am b/gdata/tests/Makefile.am
index b6e647f..7db3992 100644
--- a/gdata/tests/Makefile.am
+++ b/gdata/tests/Makefile.am
@@ -29,6 +29,9 @@ calendar_SOURCES		 = calendar.c $(TEST_SRCS)
 TEST_PROGS			+= contacts
 contacts_SOURCES		 = contacts.c $(TEST_SRCS)
 
+TEST_PROGS			+= picasaweb
+picasaweb_SOURCES		 = picasaweb.c $(TEST_SRCS)
+
 TEST_PROGS			+= memory
 memory_SOURCES			 = memory.c $(TEST_SRCS)
 
diff --git a/gdata/tests/picasaweb.c b/gdata/tests/picasaweb.c
new file mode 100644
index 0000000..d31edc9
--- /dev/null
+++ b/gdata/tests/picasaweb.c
@@ -0,0 +1,554 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Richard Schwarting 2009 <aquarichy gmail com>
+ * Copyright (C) Philip Withnall 2009 <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
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GData Client is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include "gdata.h"
+#include "common.h"
+
+#define PW_USERNAME "libgdata picasaweb gmail com"
+/* the following two properties will change if a new album is added */
+#define NUM_ALBUMS 3
+#define TEST_ALBUM_INDEX 2
+
+/* TODO: probably a better way to do this; some kind of data associated with the test suite? */
+static GDataService *service = NULL;
+static GMainLoop *main_loop = NULL;
+
+static void
+test_authentication (void)
+{
+	gboolean retval;
+	GError *error = NULL;
+
+	/* Create a service */
+	service = GDATA_SERVICE (gdata_picasaweb_service_new (CLIENT_ID));
+
+	g_assert (service != NULL);
+	g_assert (GDATA_IS_SERVICE (service));
+	g_assert_cmpstr (gdata_service_get_client_id (service), ==, CLIENT_ID);
+
+	/* Log in */
+	retval = gdata_service_authenticate (service, PW_USERNAME, PASSWORD, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (retval == TRUE);
+	g_clear_error (&error);
+
+	/* Check all is as it should be */
+	g_assert (gdata_service_is_authenticated (service) == TRUE);
+	g_assert_cmpstr (gdata_service_get_username (service), ==, PW_USERNAME);
+	g_assert_cmpstr (gdata_service_get_password (service), ==, PASSWORD);
+}
+
+static void
+test_authentication_async_cb (GDataService *service, GAsyncResult *async_result, gpointer user_data)
+{
+	gboolean retval;
+	GError *error = NULL;
+
+	retval = gdata_service_authenticate_finish (service, async_result, &error);
+	g_assert_no_error (error);
+	g_assert (retval == TRUE);
+	g_clear_error (&error);
+
+	g_main_loop_quit (main_loop);
+
+	/* Check all is as it should be */
+	g_assert (gdata_service_is_authenticated (service) == TRUE);
+	g_assert_cmpstr (gdata_service_get_username (service), ==, PW_USERNAME);
+	g_assert_cmpstr (gdata_service_get_password (service), ==, PASSWORD);
+}
+
+
+static void
+test_authentication_async (void)
+{
+	/* Create a service */
+	service = GDATA_SERVICE (gdata_picasaweb_service_new (CLIENT_ID));
+
+	g_assert (service != NULL);
+	g_assert (GDATA_IS_SERVICE (service));
+
+	gdata_service_authenticate_async (service, PW_USERNAME, PASSWORD, NULL, (GAsyncReadyCallback) test_authentication_async_cb, NULL);
+
+	main_loop = g_main_loop_new (NULL, TRUE);
+	g_main_loop_run (main_loop);
+	g_main_loop_unref (main_loop);
+}
+
+static void
+test_upload_simple (void)
+{
+	GDataCategory *category;
+	GDataPicasaWebFile *photo;
+	GDataPicasaWebFile *photo_new;
+	GFile *photo_file;
+	gchar *xml;
+	GError *error = NULL;
+
+	g_assert (service != NULL);
+
+	photo = gdata_picasaweb_file_new (NULL);
+
+	gdata_entry_set_title (GDATA_ENTRY (photo), "Photo Entry Title");
+	gdata_picasaweb_file_set_caption (photo, "Photo Summary");
+	/* TODO: Have it add this category automatically? Same for GDataCalendarEvent */
+	category = gdata_category_new ("http://schemas.google.com/photos/2007#photo";, "http://schemas.google.com/g/2005#kind";, NULL);
+	gdata_entry_add_category (GDATA_ENTRY (photo), category);
+
+	/* Check the XML */
+	xml = gdata_entry_get_xml (GDATA_ENTRY (photo));
+	g_assert_cmpstr (xml, ==,
+			 "<entry "
+				"xmlns='http://www.w3.org/2005/Atom' "
+				"xmlns:gphoto='http://schemas.google.com/photos/2007' "
+				"xmlns:media='http://video.search.yahoo.com/mrss' "
+				"xmlns:gd='http://schemas.google.com/g/2005' "
+				"xmlns:app='http://www.w3.org/2007/app'>"
+				"<title type='text'>Photo Entry Title</title>"
+				"<summary type='text'>Photo Summary</summary>"
+				"<category term='http://schemas.google.com/photos/2007#photo' scheme='http://schemas.google.com/g/2005#kind'/>"
+				"<gphoto:position>0.000000</gphoto:position>"
+				"<gphoto:commentingEnabled>true</gphoto:commentingEnabled>"
+				"<media:group>"
+					"<media:title type='plain'>Photo Entry Title</media:title>"
+					"<media:description type='plain'>Photo Summary</media:description>"
+				"</media:group>"
+			 "</entry>");
+	g_free(xml);
+
+	/* File is public domain: http://en.wikipedia.org/wiki/File:German_garden_gnome_cropped.jpg */
+	photo_file = g_file_new_for_path ("photo.jpg"); // TODO make sure file is loaded from tests/ directory
+
+	/* Upload the photo */
+	/* TODO right now, it will just go to the default album, we want an uploading one :| */
+	photo_new = gdata_picasaweb_service_upload_file (GDATA_PICASAWEB_SERVICE (service), NULL, photo, photo_file, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_PICASAWEB_FILE (photo_new));
+	g_clear_error (&error);
+
+	/* TODO: check entries and feed properties */
+
+	g_object_unref (photo);
+	g_object_unref (photo_new);
+	g_object_unref (photo_file);
+}
+
+
+static void
+test_photo (void)
+{
+	GError *error = NULL;
+	GDataFeed *album_feed;
+	GDataFeed *photo_feed;
+	GList *albums;
+	GList *files;
+	GDataEntry *album_entry;
+	GDataEntry *photo_entry;
+	GDataPicasaWebAlbum *album;
+	GDataPicasaWebFile *photo;
+	GList *list;
+	GDataMediaContent *content;
+	GDataMediaThumbnail *thumbnail;
+	GTimeVal _time;
+	gchar *str;
+
+	album_feed = gdata_picasaweb_service_query_all_albums (GDATA_PICASAWEB_SERVICE (service), NULL, NULL, NULL, NULL, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (album_feed));
+	g_clear_error (&error);
+
+	albums = gdata_feed_get_entries (album_feed);
+	album_entry = GDATA_ENTRY (g_list_nth_data (albums, TEST_ALBUM_INDEX));
+	album = GDATA_PICASAWEB_ALBUM (album_entry);
+
+	photo_feed = gdata_picasaweb_service_query_files (GDATA_PICASAWEB_SERVICE (service), GDATA_PICASAWEB_ALBUM (album), NULL, NULL, NULL,
+							  NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (photo_feed));
+	g_clear_error (&error);
+
+	files = gdata_feed_get_entries (photo_feed);
+	photo_entry = GDATA_ENTRY (g_list_nth_data (files, 0));
+	photo = GDATA_PICASAWEB_FILE (photo_entry);
+
+	gdata_picasaweb_file_get_edited (photo, &_time);
+	str = g_time_val_to_iso8601 (&_time);
+	g_assert_cmpstr (str, ==, "2009-04-26T06:57:03.474000Z");
+	g_free (str);
+
+	/* tests */
+
+	g_assert_cmpstr (gdata_picasaweb_file_get_caption (photo), ==, "Ginger cookie caption");
+	g_assert_cmpstr (gdata_picasaweb_file_get_version (photo), ==, "29"); /* 1240729023474000"); */ /* TODO check how constant this even is */
+	g_assert_cmpfloat (gdata_picasaweb_file_get_position (photo), ==, 0.0);
+	g_assert_cmpstr (gdata_picasaweb_file_get_album_id (photo), ==, "5328889949261497249");
+	g_assert_cmpuint (gdata_picasaweb_file_get_width (photo), ==, 2576);
+	g_assert_cmpuint (gdata_picasaweb_file_get_height (photo), ==, 1932);
+	g_assert_cmpuint (gdata_picasaweb_file_get_size (photo), ==, 1124730);
+	// TODO: file wasn't uploaded with client assigned; g_assert_cmpstr (gdata_picasaweb_file_get_client (photo), ==, ??);
+	// TODO: file wasn't uploaded with checksum assigned; g_assert_cmpstr (gdata_picasaweb_file_get_checksum (photo), ==, ??);
+
+	gdata_picasaweb_file_get_timestamp (photo, &_time);
+	str = g_time_val_to_iso8601 (&_time);
+	g_assert_cmpstr (str, ==, "2008-12-06T18:32:10Z");
+	g_free (str);
+
+	g_assert_cmpstr (gdata_picasaweb_file_get_video_status (photo), ==, NULL);
+	/* todo: not a good test of video status; want to upload a video for it */
+	g_assert_cmpuint (gdata_picasaweb_file_is_commenting_enabled (photo), ==, TRUE);
+	g_assert_cmpuint (gdata_picasaweb_file_get_comment_count (photo), ==, 1);
+	g_assert_cmpuint (gdata_picasaweb_file_get_rotation (photo), ==, 0);
+
+	g_assert_cmpstr (gdata_picasaweb_file_get_caption (photo), ==, "Ginger cookie caption");
+	g_assert_cmpstr (gdata_picasaweb_file_get_tags (photo), ==, "cookies");
+	g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (photo)), ==, "100_0269.jpg");
+
+	g_assert_cmpstr (gdata_picasaweb_file_get_credit (photo), ==, "libgdata.picasaweb");
+
+	/* TODO put up warning not to free this, if we are going to be passing them our own internal copy! */
+	list = gdata_picasaweb_file_get_contents (photo);
+
+	g_assert_cmpuint (g_list_length (list), ==, 1);
+	content = GDATA_MEDIA_CONTENT (list->data);
+	g_assert_cmpstr (gdata_media_content_get_uri (content), ==,
+			 "http://lh3.ggpht.com/_1kdcGyvOb8c/SfQFWPnuovI/AAAAAAAAAB0/MI0L4Sd11Eg/100_0269.jpg";);
+	g_assert_cmpstr (gdata_media_content_get_content_type (content), ==, "image/jpeg");
+	g_assert_cmpuint (gdata_media_content_is_default (content), ==, FALSE);
+	g_assert_cmpint (gdata_media_content_get_duration (content), ==, 0); /* doesn't apply to photos, but let's sanity-check it */
+
+	list = gdata_picasaweb_file_get_thumbnails (photo);
+
+	g_assert_cmpuint (g_list_length (list), ==, 3);
+	thumbnail = GDATA_MEDIA_THUMBNAIL (list->data);
+	g_assert_cmpstr (gdata_media_thumbnail_get_uri (thumbnail), ==,
+			 "http://lh3.ggpht.com/_1kdcGyvOb8c/SfQFWPnuovI/AAAAAAAAAB0/MI0L4Sd11Eg/s288/100_0269.jpg";);
+	g_assert_cmpuint (gdata_media_thumbnail_get_width (thumbnail), ==, 288);
+	g_assert_cmpuint (gdata_media_thumbnail_get_height (thumbnail), ==, 216);
+	/* TODO consider testing time, gint64 */
+}
+
+static void
+test_photo_feed_entry (void)
+{
+	GDataFeed *album_feed;
+	GDataFeed *photo_feed;
+	GError *error = NULL;
+	GDataEntry *entry;
+	GDataPicasaWebAlbum *album;
+	GList *albums;
+	GList *files;
+	GDataEntry *photo_entry;
+	gchar *str;
+	GTimeVal _time;
+
+	album_feed = gdata_picasaweb_service_query_all_albums (GDATA_PICASAWEB_SERVICE (service), NULL, NULL, NULL, NULL, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (album_feed));
+	g_clear_error (&error);
+
+	albums = gdata_feed_get_entries (album_feed);
+	entry = GDATA_ENTRY (g_list_nth_data (albums, TEST_ALBUM_INDEX));
+	album = GDATA_PICASAWEB_ALBUM (entry);
+
+	photo_feed = gdata_picasaweb_service_query_files (GDATA_PICASAWEB_SERVICE (service), GDATA_PICASAWEB_ALBUM (album), NULL, NULL, NULL,
+							  NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (photo_feed));
+	g_clear_error (&error);
+
+	files = gdata_feed_get_entries (photo_feed);
+	photo_entry = GDATA_ENTRY (g_list_nth_data (files, 0));
+
+	/* tests */
+
+	g_assert_cmpuint (g_list_length (files), ==, 1);
+
+	g_assert_cmpstr (gdata_entry_get_title (photo_entry), ==, "100_0269.jpg");
+	g_assert_cmpstr (gdata_entry_get_id (photo_entry), ==, "5328890138794566386");
+	g_assert_cmpstr (gdata_entry_get_etag (photo_entry), !=, NULL);
+
+	gdata_entry_get_updated (photo_entry, &_time);
+	str = g_time_val_to_iso8601 (&_time);
+	g_assert_cmpstr (str, ==, "2009-04-26T06:57:03.474000Z");
+	g_free (str);
+
+	gdata_entry_get_published (photo_entry, &_time);
+	str = g_time_val_to_iso8601 (&_time);
+	g_assert_cmpstr (str, ==, "2009-04-26T06:55:20Z");
+	g_free (str);
+
+	g_assert_cmpstr (gdata_entry_get_content (photo_entry), ==,
+			 "http://lh3.ggpht.com/_1kdcGyvOb8c/SfQFWPnuovI/AAAAAAAAAB0/MI0L4Sd11Eg/100_0269.jpg";);
+	g_assert_cmpstr (gdata_entry_get_xml (photo_entry), !=, NULL);
+	g_assert_cmpuint (strlen (gdata_entry_get_xml (photo_entry)), >, 0);
+}
+
+static void
+test_photo_feed (void)
+{
+	GError *error = NULL;
+	GDataFeed *album_feed;
+	GDataFeed *photo_feed;
+	GDataEntry *entry;
+	GDataPicasaWebAlbum *album;
+	GList *albums;
+
+	album_feed = gdata_picasaweb_service_query_all_albums (GDATA_PICASAWEB_SERVICE (service), NULL, NULL, NULL, NULL, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (album_feed));
+	g_clear_error (&error);
+
+	albums = gdata_feed_get_entries (album_feed);
+	entry = GDATA_ENTRY (g_list_nth_data (albums, TEST_ALBUM_INDEX));
+	album = GDATA_PICASAWEB_ALBUM (entry);
+
+	/* tests */
+
+	photo_feed = gdata_picasaweb_service_query_files (GDATA_PICASAWEB_SERVICE (service), GDATA_PICASAWEB_ALBUM (album), NULL, NULL, NULL,
+							  NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (photo_feed));
+	g_clear_error (&error);
+
+	g_assert_cmpstr (gdata_feed_get_title (photo_feed), ==, "Test Album 1 - Venice - Public");
+	g_assert_cmpstr (gdata_feed_get_id (photo_feed), ==,
+			 "http://picasaweb.google.com/data/feed/user/libgdata.picasaweb/albumid/5328889949261497249";);
+	g_assert_cmpstr (gdata_feed_get_etag (photo_feed), !=, NULL);
+	g_assert_cmpuint (gdata_feed_get_items_per_page (photo_feed), ==, 1000);
+	g_assert_cmpuint (gdata_feed_get_start_index (photo_feed), ==, 1);
+	g_assert_cmpuint (gdata_feed_get_total_results (photo_feed), ==, 1);
+}
+
+static void
+test_album (void)
+{
+	GDataFeed *album_feed;
+	GError *error = NULL;
+	GDataPicasaWebAlbum *album;
+	GList *albums;
+	GTimeVal _time;
+	gchar *str;
+
+	album_feed = gdata_picasaweb_service_query_all_albums (GDATA_PICASAWEB_SERVICE (service), NULL, NULL, NULL, NULL, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (album_feed));
+	g_clear_error (&error);
+
+	albums = gdata_feed_get_entries (album_feed);
+	album = GDATA_PICASAWEB_ALBUM (g_list_nth_data (albums, TEST_ALBUM_INDEX));
+
+	/* Tests */
+	g_assert_cmpstr (gdata_picasaweb_album_get_user (album), ==, "libgdata.picasaweb");
+	g_assert_cmpstr (gdata_picasaweb_album_get_nickname (album), ==, "libgdata.picasaweb");
+
+	gdata_picasaweb_album_get_edited (album, &_time);
+	str = g_time_val_to_iso8601 (&_time);
+	g_assert_cmpstr (str, ==, "2009-04-26T06:57:03.474000Z");
+	g_free (str);
+
+	g_assert_cmpstr (gdata_picasaweb_album_get_description (album), ==, "This is the test description.  This album should be in Venice.");
+	g_assert_cmpint (gdata_picasaweb_album_get_visibility (album), ==, GDATA_PICASAWEB_PUBLIC);
+	/* Google doesn't seem to be returning this one any more */
+	/*g_assert_cmpstr (gdata_picasaweb_album_get_name (album), ==, "TestAlbum1VenicePublic");*/
+	g_assert_cmpstr (gdata_picasaweb_album_get_location (album), ==, "Venice");
+
+	gdata_picasaweb_album_get_timestamp (album, &_time);
+	str = g_time_val_to_iso8601 (&_time);
+	g_assert_cmpstr (str, ==, "2009-04-26T07:00:00Z");
+	g_free (str);
+
+	g_assert_cmpuint (gdata_picasaweb_album_get_num_photos (album), ==, 1);
+	g_assert_cmpuint (gdata_picasaweb_album_get_num_photos_remaining (album), ==, 499);
+	g_assert_cmpuint (gdata_picasaweb_album_get_bytes_used (album), ==, 1124730);
+}
+
+static void
+test_album_feed_entry (void)
+{
+	GDataFeed *album_feed;
+	GError *error = NULL;
+	GDataEntry *entry;
+	GList *albums;
+	gchar *str, *xml;
+	GTimeVal _time;
+
+	album_feed = gdata_picasaweb_service_query_all_albums (GDATA_PICASAWEB_SERVICE (service), NULL, NULL, NULL, NULL, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (album_feed));
+	g_clear_error (&error);
+
+	albums = gdata_feed_get_entries (album_feed);
+	g_assert_cmpuint (g_list_length (albums), ==, NUM_ALBUMS);
+
+	entry = GDATA_ENTRY (g_list_nth_data (albums, TEST_ALBUM_INDEX));
+	g_assert (entry != NULL);
+
+	g_object_ref (entry);
+	g_object_unref (album_feed);
+
+	/* Tests */
+	g_assert_cmpstr (gdata_entry_get_title (entry), ==, "Test Album 1 - Venice - Public");
+	g_assert_cmpstr (gdata_entry_get_id (entry), ==, "5328889949261497249");
+	g_assert_cmpstr (gdata_entry_get_etag (entry), !=, NULL);
+
+	gdata_entry_get_updated (entry, &_time);
+	str = g_time_val_to_iso8601 (&_time);
+	g_assert_cmpstr (str, ==, "2009-04-26T06:57:03.474000Z");
+	g_free (str);
+
+	gdata_entry_get_published (entry, &_time);
+	str = g_time_val_to_iso8601 (&_time);
+	g_assert_cmpstr (str, ==, "2009-04-26T07:00:00Z");
+	g_free (str);
+
+	// g_assert_cmpstr (gdata_entry_get_content (entry), !=, NULL);
+	/* TODO */
+	printf("** WARNING:%s:%d: gdata_entry_get_content(entry) returns null; valid?\n", __FILE__, __LINE__);
+	xml = gdata_entry_get_xml (entry);
+	g_assert_cmpstr (xml, !=, NULL);
+	g_assert_cmpuint (strlen (xml), >, 0);
+	g_free (xml);
+
+	g_object_unref (entry);
+}
+
+static void
+test_album_feed (void)
+{
+	GDataFeed *album_feed;
+	GError *error = NULL;
+
+	album_feed = gdata_picasaweb_service_query_all_albums (GDATA_PICASAWEB_SERVICE (service), NULL, NULL, NULL, NULL, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (album_feed));
+	g_clear_error (&error);
+
+	/* tests */
+
+	g_assert_cmpstr (gdata_feed_get_title (album_feed), ==, "libgdata.picasaweb");
+	// TODO find out why subtitle == null when returned: no subtitle for feed? // printf("feed subtitle: %s\n", gdata_feed_get_subtitle(feed));
+	g_assert_cmpstr (gdata_feed_get_id (album_feed), ==, "http://picasaweb.google.com/data/feed/user/libgdata.picasaweb";);
+	g_assert_cmpstr (gdata_feed_get_etag (album_feed), !=, NULL); /* this varies as albums change, like when a new image is uploaded in our test! */
+	g_assert_cmpuint (gdata_feed_get_items_per_page (album_feed), ==, 1000);
+	g_assert_cmpuint (gdata_feed_get_start_index (album_feed), ==, 1);
+	g_assert_cmpuint (gdata_feed_get_total_results (album_feed), ==, NUM_ALBUMS);
+}
+
+static void
+test_query_all_albums (void)
+{
+	GDataFeed *album_feed;
+	GDataFeed *photo_feed;
+	GError *error = NULL;
+	GList *albums;
+	GDataEntry *entry;
+	GDataPicasaWebAlbum *album;
+
+	g_assert (service != NULL);
+
+	/* TODO: find out whether I need to free this; probably */
+	album_feed = gdata_picasaweb_service_query_all_albums (GDATA_PICASAWEB_SERVICE (service), NULL, NULL, NULL, NULL, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (album_feed));
+	g_clear_error (&error);
+
+	albums = gdata_feed_get_entries (album_feed);
+	/* g_object_unref (feed); TODO find out why this complains about not being an object */
+	entry = GDATA_ENTRY (g_list_nth_data (albums, TEST_ALBUM_INDEX));
+	album = GDATA_PICASAWEB_ALBUM (entry);
+
+	photo_feed = gdata_picasaweb_service_query_files (GDATA_PICASAWEB_SERVICE (service), GDATA_PICASAWEB_ALBUM (album), NULL, NULL, NULL,
+							  NULL, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (photo_feed));
+	g_clear_error (&error);
+
+	//g_object_unref(photo_feed);
+	//g_object_unref(album_feed);
+
+	g_list_free(albums);
+}
+
+static void
+test_query_all_albums_async_cb (GDataService *service, GAsyncResult *async_result, gpointer user_data)
+{
+	GDataFeed *feed;
+	GError *error;
+
+	feed = gdata_service_query_finish (service, async_result, &error);
+	g_assert_no_error (error);
+	g_assert (GDATA_IS_FEED (feed));
+	g_clear_error (&error);
+
+	/* @TODO: Tests? */
+	g_main_loop_quit (main_loop);
+
+	g_object_unref (feed);
+}
+
+static void
+test_query_all_albums_async (void)
+{
+	g_assert (service != NULL);
+
+	gdata_picasaweb_service_query_all_albums_async (GDATA_PICASAWEB_SERVICE (service), NULL, NULL, NULL, NULL,
+							NULL, (GAsyncReadyCallback) test_query_all_albums_async_cb, NULL);
+
+	main_loop = g_main_loop_new (NULL, TRUE);
+	g_main_loop_run (main_loop);
+	g_main_loop_unref (main_loop);
+}
+
+/* TODO: test private, public albums, test uploading */
+
+int
+main (int argc, char *argv[])
+{
+	gint retval;
+
+	g_type_init ();
+	g_thread_init (NULL);
+	g_test_init (&argc, &argv, NULL);
+	g_test_bug_base ("http://bugzilla.gnome.org/show_bug.cgi?id=";);
+
+	g_test_add_func ("/picasaweb/authentication", test_authentication);
+	if (g_test_thorough () == TRUE)
+		g_test_add_func ("/picasaweb/authentication_async", test_authentication_async);
+	g_test_add_func ("/picasaweb/query/all_albums", test_query_all_albums);
+	if (g_test_thorough () == TRUE)
+		g_test_add_func ("/picasaweb/query/all_albums_async", test_query_all_albums_async);
+	g_test_add_func ("/picasaweb/query/album_feed", test_album_feed);
+	g_test_add_func ("/picasaweb/query/album_feed_entry", test_album_feed_entry);
+	g_test_add_func ("/picasaweb/query/album", test_album);
+	g_test_add_func ("/picasaweb/query/photo_feed", test_photo_feed);
+	g_test_add_func ("/picasaweb/query/photo_feed_entry", test_photo_feed_entry);
+	g_test_add_func ("/picasaweb/query/photo", test_photo);
+	g_test_add_func ("/picasaweb/upload/photo", test_upload_simple);
+
+	retval = g_test_run ();
+	if (service != NULL)
+		g_object_unref (service);
+
+	return retval;
+}
+
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 56a2470..5a29e9e 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -11,5 +11,6 @@ gdata/services/calendar/gdata-calendar-calendar.c
 gdata/services/calendar/gdata-calendar-event.c
 gdata/services/calendar/gdata-calendar-service.c
 gdata/services/contacts/gdata-contacts-service.c
+gdata/services/picasaweb/gdata-picasaweb-service.c
 gdata/services/youtube/gdata-youtube-service.c
 gdata/services/youtube/gdata-youtube-video.c



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