[rhythmbox] add extdb, a database for storing external metadata



commit 7a0eb6d33860fc7cf47475907dd8cdf12f79864f
Author: Jonathan Matthew <jonathan d14n org>
Date:   Fri Dec 16 08:34:30 2011 +1000

    add extdb, a database for storing external metadata
    
    extdb associates external metadata items, such as album art images,
    with sets of properties and values, such as album and artist names.
    It uses tdb to store the associations, and (for now) keeps the
    actual data in separate files.
    
    Lookups are based on keys that contain sets of required and optional
    properties.  Required properties must be present in the item found,
    and optional properties must match if present.  When making a request,
    additional information can be provided, such as musicbrainz album IDs,
    that will not be stored, but only used to facilitate searches.
    
    When a request fails, extdb emits a signal to allow metadata providers
    to perform a search and store the resulting data, which then answers
    the request.
    
    When new data is stored in an extdb instance, it emits a signal allowing
    consumers to make use of it.
    
    extdb will initially be used for album art, but will also store
    lyrics and perhaps other data in the future.

 bindings/gi/Makefile.am  |    4 +
 configure.ac             |    5 +-
 lib/rb-util.c            |   64 +++-
 lib/rb-util.h            |    8 +
 metadata/Makefile.am     |    4 +
 metadata/rb-ext-db-key.c |  431 +++++++++++++++++++
 metadata/rb-ext-db-key.h |   88 ++++
 metadata/rb-ext-db.c     | 1060 ++++++++++++++++++++++++++++++++++++++++++++++
 metadata/rb-ext-db.h     |  124 ++++++
 9 files changed, 1785 insertions(+), 3 deletions(-)
---
diff --git a/bindings/gi/Makefile.am b/bindings/gi/Makefile.am
index 3a55f67..7cf1c83 100644
--- a/bindings/gi/Makefile.am
+++ b/bindings/gi/Makefile.am
@@ -29,6 +29,10 @@ rb_introspection_sources = \
 		lib/rb-string-value-map.c \
 		lib/rb-util.h \
 		lib/rb-util.c \
+		metadata/rb-ext-db.h \
+		metadata/rb-ext-db.c \
+		metadata/rb-ext-db-key.h \
+		metadata/rb-ext-db-key.c \
 		metadata/rb-metadata.h \
 		metadata/rb-metadata-dbus-client.c \
 		podcast/rb-podcast-manager.h \
diff --git a/configure.ac b/configure.ac
index 87273dd..3b8f98c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -102,8 +102,9 @@ PKG_CHECK_MODULES(RHYTHMBOX,				\
 		  gio-unix-2.0 >= $GLIB_REQS		\
 		  libsoup-2.4 >= $LIBSOUP_REQS		\
 		  libsoup-gnome-2.4 >= $LIBSOUP_REQS	\
-		  libpeas-1.0 >= $LIBPEAS_REQS
-		  libpeas-gtk-1.0 >= $LIBPEAS_REQS)
+		  libpeas-1.0 >= $LIBPEAS_REQS		\
+		  libpeas-gtk-1.0 >= $LIBPEAS_REQS	\
+		  tdb >= 1.2.6)
 
 PKG_CHECK_MODULES(TOTEM_PLPARSER, totem-plparser >= $TOTEM_PLPARSER_REQS, have_totem_plparser=yes, have_totem_plparser=no)
 if test x$have_totem_plparser != xyes; then
diff --git a/lib/rb-util.c b/lib/rb-util.c
index a4341e4..d3f62ab 100644
--- a/lib/rb-util.c
+++ b/lib/rb-util.c
@@ -1032,7 +1032,39 @@ rb_signal_accumulator_object_handled (GSignalInvocationHint *hint,
 	g_value_unset (return_accu);
 	g_value_init (return_accu, G_VALUE_TYPE (handler_return));
 	g_value_copy (handler_return, return_accu);
-	
+
+	return FALSE;
+}
+
+/**
+ * rb_signal_accumulator_value_handled: (skip):
+ * @hint: a #GSignalInvocationHint
+ * @return_accu: holds the accumulated return value
+ * @handler_return: holds the return value to be accumulated
+ * @dummy: user data (unused)
+ *
+ * A #GSignalAccumulator that aborts the signal emission after the
+ * first handler to return a value, and returns the value returned by
+ * that handler.  This is the opposite behaviour from what you get when
+ * no accumulator is specified, where the last signal handler wins.
+ *
+ * Return value: %FALSE to abort signal emission, %TRUE to continue
+ */
+gboolean
+rb_signal_accumulator_value_handled (GSignalInvocationHint *hint,
+				     GValue *return_accu,
+				     const GValue *handler_return,
+				     gpointer dummy)
+{
+	if (handler_return == NULL ||
+	    !G_VALUE_HOLDS (handler_return, G_TYPE_VALUE) ||
+	    g_value_get_boxed (handler_return) == NULL)
+		return TRUE;
+
+	g_value_unset (return_accu);
+	g_value_init (return_accu, G_VALUE_TYPE (handler_return));
+	g_value_copy (handler_return, return_accu);
+
 	return FALSE;
 }
 
@@ -1088,6 +1120,36 @@ rb_signal_accumulator_value_array (GSignalInvocationHint *hint,
 }
 
 /**
+ * rb_signal_accumulator_boolean_or: (skip):
+ * @hint: a #GSignalInvocationHint
+ * @return_accu: holds the accumulated return value
+ * @handler_return: holds the return value to be accumulated
+ * @dummy: user data (unused)
+ *
+ * A #GSignalAccumulator used to return the boolean OR of all
+ * returned (boolean) values.
+ *
+ * Return value: %FALSE to abort signal emission, %TRUE to continue
+ */
+gboolean
+rb_signal_accumulator_boolean_or (GSignalInvocationHint *hint,
+				  GValue *return_accu,
+				  const GValue *handler_return,
+				  gpointer dummy)
+{
+	if (handler_return != NULL && G_VALUE_HOLDS_BOOLEAN (handler_return)) {
+		if (G_VALUE_HOLDS_BOOLEAN (return_accu) == FALSE ||
+		    g_value_get_boolean (return_accu) == FALSE) {
+			g_value_unset (return_accu);
+			g_value_init (return_accu, G_TYPE_BOOLEAN);
+			g_value_set_boolean (return_accu, g_value_get_boolean (handler_return));
+		}
+	}
+
+	return TRUE;
+}
+
+/**
  * rb_value_array_append_data: (skip):
  * @array: #GValueArray to append to
  * @type: #GType of the value being appended
diff --git a/lib/rb-util.h b/lib/rb-util.h
index d72765b..2532e72 100644
--- a/lib/rb-util.h
+++ b/lib/rb-util.h
@@ -84,6 +84,14 @@ gboolean rb_signal_accumulator_object_handled (GSignalInvocationHint *hint,
 					       GValue *return_accu,
 					       const GValue *handler_return,
 					       gpointer dummy);
+gboolean rb_signal_accumulator_value_handled (GSignalInvocationHint *hint,
+					      GValue *return_accu,
+					      const GValue *handler_return,
+					      gpointer dummy);
+gboolean rb_signal_accumulator_boolean_or (GSignalInvocationHint *hint,
+					   GValue *return_accu,
+					   const GValue *handler_return,
+					   gpointer dummy);
 gboolean rb_signal_accumulator_value_array (GSignalInvocationHint *hint,
 					    GValue *return_accu,
 					    const GValue *handler_return,
diff --git a/metadata/Makefile.am b/metadata/Makefile.am
index 3bb3622..ae64fac 100644
--- a/metadata/Makefile.am
+++ b/metadata/Makefile.am
@@ -25,6 +25,10 @@ metadatainclude_HEADERS = rb-metadata.h
 noinst_LTLIBRARIES = librbmetadata.la
 
 librbmetadata_la_SOURCES =				\
+	rb-ext-db-key.h					\
+	rb-ext-db-key.c					\
+	rb-ext-db.h					\
+	rb-ext-db.c					\
 	rb-metadata.h					\
 	rb-metadata-common.c				\
 	rb-metadata-dbus.h				\
diff --git a/metadata/rb-ext-db-key.c b/metadata/rb-ext-db-key.c
new file mode 100644
index 0000000..717599e
--- /dev/null
+++ b/metadata/rb-ext-db-key.c
@@ -0,0 +1,431 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2011  Jonathan Matthew  <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "rb-ext-db-key.h"
+
+#include "rb-debug.h"
+
+/**
+ * SECTION:rb-ext-db-key
+ * @short_description: key for external metadata lookups
+ *
+ * An external metadata key consists of one or more required fields (such as
+ * the album name for album art lookups), zero or more optional fields
+ * (such as the artist name), and zero or more informational fields (such as
+ * the musicbrainz album ID).
+ */
+
+typedef struct
+{
+	char *name;
+	char *value;
+	RBExtDBFieldType type;
+} RBExtDBField;
+
+struct _RBExtDBKey
+{
+	GList *fields;
+};
+
+static void
+rb_ext_db_field_free (RBExtDBField *field)
+{
+	g_free (field->name);
+	g_free (field->value);
+	g_slice_free (RBExtDBField, field);
+}
+
+static RBExtDBField *
+rb_ext_db_field_copy (RBExtDBField *field)
+{
+	RBExtDBField *copy;
+	copy = g_slice_new0 (RBExtDBField);
+	copy->name = g_strdup (field->name);
+	copy->value = g_strdup (field->value);
+	copy->type = field->type;
+	return copy;
+}
+
+GType
+rb_ext_db_key_get_type (void)
+{
+	static GType type = 0;
+
+	if (G_UNLIKELY (type == 0)) {
+		type = g_boxed_type_register_static ("RBExtDBKey",
+						     (GBoxedCopyFunc) rb_ext_db_key_copy,
+						     (GBoxedFreeFunc) rb_ext_db_key_free);
+	}
+
+	return type;
+}
+
+/**
+ * rb_ext_db_key_copy:
+ * @key: a #RBExtDBKey
+ *
+ * Copies a key.
+ *
+ * Return value: copied key
+ */
+RBExtDBKey *
+rb_ext_db_key_copy (RBExtDBKey *key)
+{
+	RBExtDBKey *copy;
+	GList *l;
+
+	copy = g_slice_new0 (RBExtDBKey);
+	for (l = key->fields; l != NULL; l = l->next) {
+		copy->fields = g_list_append (copy->fields, rb_ext_db_field_copy (l->data));
+	}
+	return copy;
+}
+
+/**
+ * rb_ext_db_key_free:
+ * @key: a #RBExtDBKey
+ *
+ * Frees a key
+ */
+void
+rb_ext_db_key_free (RBExtDBKey *key)
+{
+	g_list_free_full (key->fields, (GDestroyNotify) rb_ext_db_field_free);
+	g_slice_free (RBExtDBKey, key);
+}
+
+/**
+ * rb_ext_db_key_create:
+ * @field: required field name
+ * @value: value for field
+ *
+ * Creates a new metadata lookup key with a single required field.
+ * Use @rb_ext_db_key_add_field to add more.
+ *
+ * Return value: the new key
+ */
+RBExtDBKey *
+rb_ext_db_key_create (const char *field, const char *value)
+{
+	RBExtDBKey *key;
+
+	key = g_slice_new0 (RBExtDBKey);
+	rb_ext_db_key_add_field (key, field, RB_EXT_DB_FIELD_REQUIRED, value);
+
+	return key;
+}
+
+/**
+ * rb_ext_db_key_add_field:
+ * @key: a #RBExtDBKey
+ * @field: name of the field to add
+ * @field_type: field type (required, optional, or informational)
+ * @value: field value
+ *
+ * Adds a field to the key.  Does not check that the field does not
+ * already exist.
+ */
+void
+rb_ext_db_key_add_field (RBExtDBKey *key,
+			 const char *field,
+			 RBExtDBFieldType field_type,
+			 const char *value)
+{
+	RBExtDBField *f;
+
+	f = g_slice_new0 (RBExtDBField);
+	f->name = g_strdup (field);
+	f->value = g_strdup (value);
+	f->type = field_type;
+	key->fields = g_list_append (key->fields, f);
+}
+
+/**
+ * rb_ext_db_key_get_field_names:
+ * @key: a #RBExtDBKey
+ *
+ * Returns a NULL-terminated array containing the names of the fields
+ * present in the key.
+ *
+ * Return value: (transfer full): array of field names
+ */
+char **
+rb_ext_db_key_get_field_names (RBExtDBKey *key)
+{
+	char **names;
+	GList *l;
+	int i;
+
+	names = g_new0 (char *, g_list_length (key->fields) + 1);
+	i = 0;
+	for (l = key->fields; l != NULL; l = l->next) {
+		RBExtDBField *f = l->data;
+		names[i++] = g_strdup (f->name);
+	}
+
+	return names;
+}
+
+/**
+ * rb_ext_db_key_get_field:
+ * @key: a #RBExtDBKey
+ * @field: field to retrieve
+ *
+ * Extracts the value for the specified field.
+ *
+ * Return value: field value, or NULL
+ */
+const char *
+rb_ext_db_key_get_field (RBExtDBKey *key, const char *field)
+{
+	GList *l;
+
+	for (l = key->fields; l != NULL; l = l->next) {
+		RBExtDBField *f = l->data;
+		if (g_strcmp0 (field, f->name) == 0) {
+			return f->value;
+		}
+	}
+
+	return NULL;
+}
+
+/**
+ * rb_ext_db_key_get_field_type:
+ * @key: a #RBExtDBKey
+ * @field: field to retrieve
+ *
+ * Extracts the field type for the specified field.
+ *
+ * Return value: field type value
+ */
+RBExtDBFieldType
+rb_ext_db_key_get_field_type (RBExtDBKey *key, const char *field)
+{
+	GList *l;
+
+	for (l = key->fields; l != NULL; l = l->next) {
+		RBExtDBField *f = l->data;
+		if (g_strcmp0 (field, f->name) == 0) {
+			return f->type;
+		}
+	}
+
+	/* check that the field exists before calling this */
+	return RB_EXT_DB_FIELD_INFORMATIONAL;
+}
+
+static gboolean
+match_field (RBExtDBKey *key, RBExtDBField *field)
+{
+	const char *value;
+
+	if (field->type == RB_EXT_DB_FIELD_INFORMATIONAL)
+		return TRUE;
+
+	value = rb_ext_db_key_get_field (key, field->name);
+	if (value == NULL) {
+		if (field->type == RB_EXT_DB_FIELD_REQUIRED)
+			return FALSE;
+	} else {
+		if (g_strcmp0 (value, field->value) != 0)
+			return FALSE;
+	}
+
+	return TRUE;
+}
+
+/**
+ * rb_ext_db_key_matches:
+ * @a: first #RBExtDBKey
+ * @b: second #RBExtDBKey
+ *
+ * Checks whether the fields specified in @a match @b.
+ * For keys to match, they must have the same set of required fields,
+ * and the values for all must match.  Optional fields must have the
+ * same values if present in both.  Informational fields are ignored.
+ *
+ * Return value: %TRUE if the keys match
+ */
+gboolean
+rb_ext_db_key_matches (RBExtDBKey *a, RBExtDBKey *b)
+{
+	GList *l;
+
+	for (l = a->fields; l != NULL; l = l->next) {
+		RBExtDBField *f = l->data;
+		if (match_field (b, f) == FALSE) {
+			return FALSE;
+		}
+	}
+
+	for (l = b->fields; l != NULL; l = l->next) {
+		RBExtDBField *f = l->data;
+		if (match_field (a, f) == FALSE) {
+			return FALSE;
+		}
+	}
+
+	return TRUE;
+}
+
+static void
+create_store_key (RBExtDBKey *key, guint optional_count, guint optional_fields, TDB_DATA *data)
+{
+	GByteArray *k;
+	GList *l;
+	int opt = 0;
+	guint8 nul = '\0';
+
+	k = g_byte_array_sized_new (512);
+	for (l = key->fields; l != NULL; l = l->next) {
+		RBExtDBField *f = l->data;
+		switch (f->type) {
+		case RB_EXT_DB_FIELD_OPTIONAL:
+			/* decide if we want to include this one */
+			if (optional_fields != G_MAXUINT) {
+				int bit = 1 << ((optional_count-1) - opt);
+				opt++;
+				if ((optional_fields & bit) == 0)
+					break;
+			}
+			/* fall through */
+		case RB_EXT_DB_FIELD_REQUIRED:
+			g_byte_array_append (k, (guint8 *)f->name, strlen (f->name));
+			g_byte_array_append (k, &nul, 1);
+			g_byte_array_append (k, (guint8 *)f->value, strlen (f->value));
+			g_byte_array_append (k, &nul, 1);
+			break;
+
+		case RB_EXT_DB_FIELD_INFORMATIONAL:
+			break;
+
+		default:
+			g_assert_not_reached ();
+			break;
+		}
+
+	}
+
+	data->dsize = k->len;
+	data->dptr = g_byte_array_free (k, FALSE);
+}
+
+/**
+ * rb_ext_db_key_lookups: (skip):
+ * @key: a #RBExtDBKey
+ * @callback: a callback to process lookup keys
+ * @user_data: data to pass to @callback
+ *
+ * Generates the set of possible lookup keys for @key and
+ * passes them to @callback in order.  If the callback returns
+ * %FALSE, processing will stop.
+ *
+ * This should only be used by the metadata store itself.
+ * Metadata providers and consumers shouldn't need to do this.
+ */
+void
+rb_ext_db_key_lookups (RBExtDBKey *key,
+		       RBExtDBKeyLookupCallback callback,
+		       gpointer user_data)
+{
+	int optional_count = 0;
+	int optional_keys;
+	GList *l;
+
+	for (l = key->fields; l != NULL; l = l->next) {
+		RBExtDBField *field = l->data;
+		if (field->type == RB_EXT_DB_FIELD_OPTIONAL) {
+			optional_count++;
+		}
+	}
+
+	for (optional_keys = (1<<optional_count)-1; optional_keys >= 0; optional_keys--) {
+		TDB_DATA sk;
+		gboolean result;
+
+		create_store_key (key, optional_count, optional_keys, &sk);
+
+		result = callback (sk, user_data);
+		g_free (sk.dptr);
+
+		if (result == FALSE)
+			break;
+	}
+}
+
+/**
+ * rb_ext_db_key_to_store_key: (skip):
+ * @key: a @RBExtDBKey
+ *
+ * Generates the storage key for @key.  This is the value that should
+ * be used to store an item identified by this key in the store.
+ * The storage key includes all optional fields, so keys passed to
+ * this function should be constructed using only the optional fields
+ * that were used to locate the item.  The caller must free the data
+ * pointer inside @data.
+ *
+ * This should only be used by the metadata store itself.
+ * Metadata providers and consumers shouldn't need to do this.
+ *
+ * Return value: TDB_DATA structure containing storage key
+ */
+TDB_DATA
+rb_ext_db_key_to_store_key (RBExtDBKey *key)
+{
+	TDB_DATA k = {0,};
+	/* include all optional keys */
+	create_store_key (key, G_MAXUINT, G_MAXUINT, &k);
+	return k;
+}
+
+
+
+#define ENUM_ENTRY(NAME, DESC) { NAME, "" #NAME "", DESC }
+
+GType
+rb_ext_db_field_type_get_type (void)
+{
+	static GType etype = 0;
+
+	if (etype == 0) {
+		static const GEnumValue values[] = {
+			ENUM_ENTRY(RB_EXT_DB_FIELD_REQUIRED, "required"),
+			ENUM_ENTRY(RB_EXT_DB_FIELD_OPTIONAL, "optional"),
+			ENUM_ENTRY(RB_EXT_DB_FIELD_INFORMATIONAL, "informational"),
+			{ 0, 0, 0 }
+		};
+		etype = g_enum_register_static ("RBExtDBFieldType", values);
+	}
+
+	return etype;
+}
diff --git a/metadata/rb-ext-db-key.h b/metadata/rb-ext-db-key.h
new file mode 100644
index 0000000..959007f
--- /dev/null
+++ b/metadata/rb-ext-db-key.h
@@ -0,0 +1,88 @@
+/*
+ *  Copyright (C) 2011 Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#ifndef RB_EXT_DB_KEY_H
+#define RB_EXT_DB_KEY_H
+
+#include <glib-object.h>
+
+#include <sys/types.h>
+#include <tdb.h>
+
+#include <rhythmdb/rhythmdb-entry.h>
+
+G_BEGIN_DECLS
+
+
+typedef enum {
+	RB_EXT_DB_FIELD_REQUIRED,		/* results must match this; stored */
+	RB_EXT_DB_FIELD_OPTIONAL,		/* results should match this; stored */
+	RB_EXT_DB_FIELD_INFORMATIONAL		/* may be used to find results, not stored */
+} RBExtDBFieldType;
+
+GType				   rb_ext_db_field_type_get_type (void);
+#define RB_TYPE_EXT_DB_FIELD_TYPE (rb_ext_db_field_type_get_type ())
+
+typedef struct _RBExtDBKey RBExtDBKey;
+struct _RBExtDBKey;
+
+#define RB_TYPE_EXT_DB_KEY	(rb_ext_db_key_get_type ())
+#define RB_EXT_DB_KEY(o)	(G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_EXT_DB_KEY, RBExtDBKey))
+
+GType			rb_ext_db_key_get_type		(void);
+
+RBExtDBKey *		rb_ext_db_key_copy		(RBExtDBKey *key);
+void			rb_ext_db_key_free		(RBExtDBKey *key);
+
+RBExtDBKey *		rb_ext_db_key_create		(const char *field,
+							 const char *value);
+
+void			rb_ext_db_key_add_field		(RBExtDBKey *key,
+							 const char *field,
+							 RBExtDBFieldType field_type,
+							 const char *value);
+
+char **			rb_ext_db_key_get_field_names	(RBExtDBKey *key);
+const char *		rb_ext_db_key_get_field		(RBExtDBKey *key,
+							 const char *field);
+RBExtDBFieldType	rb_ext_db_key_get_field_type	(RBExtDBKey *key,
+							 const char *field);
+
+gboolean		rb_ext_db_key_matches		(RBExtDBKey *a,
+							 RBExtDBKey *b);
+
+typedef gboolean	(*RBExtDBKeyLookupCallback)	(TDB_DATA data, gpointer user_data);
+
+void			rb_ext_db_key_lookups		(RBExtDBKey *key,
+							 RBExtDBKeyLookupCallback callback,
+							 gpointer user_data);
+
+TDB_DATA		rb_ext_db_key_to_store_key	(RBExtDBKey *key);
+
+G_END_DECLS
+
+#endif /* RB_EXT_DB_KEY_H */
diff --git a/metadata/rb-ext-db.c b/metadata/rb-ext-db.c
new file mode 100644
index 0000000..ab90387
--- /dev/null
+++ b/metadata/rb-ext-db.c
@@ -0,0 +1,1060 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2011  Jonathan Matthew  <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#include "config.h"
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include <metadata/rb-ext-db.h>
+#include <lib/rb-file-helpers.h>
+#include <lib/rb-debug.h>
+#include <lib/rb-util.h>
+
+/**
+ * SECTION:rb-ext-db
+ * @short_description: store for external metadata such as album art
+ *
+ * This class simplifies searching for and providing external metadata
+ * such as album art or lyrics.  A metadata provider connects to a signal
+ * on the database and in response provides a URI, a buffer containing the
+ * data, or an object representation of the data (such as a GdkPixbuf).
+ * A metadata requestor calls rb_ext_db_request and specifies a callback,
+ * or alternatively connects to a signal to receive all metadata as it is
+ * stored.
+ */
+
+enum
+{
+	PROP_0,
+	PROP_NAME,
+};
+
+enum
+{
+	ADDED,
+	REQUEST,
+	STORE,
+	LOAD,
+	LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+static GList *instances = NULL;
+
+static void rb_ext_db_class_init (RBExtDBClass *klass);
+static void rb_ext_db_init (RBExtDB *store);
+
+static void maybe_start_store_request (RBExtDB *store);
+
+struct _RBExtDBPrivate
+{
+	char *name;
+
+	struct tdb_context *tdb_context;
+
+	GList *requests;
+	GList *store_queue;
+	GSimpleAsyncResult *store_op;
+};
+
+typedef struct {
+	RBExtDBKey *key;
+	RBExtDBRequestCallback callback;
+	gpointer user_data;
+	GDestroyNotify destroy_notify;
+
+	char *filename;
+	GValue *data;
+} RBExtDBRequest;
+
+typedef struct {
+	RBExtDBKey *key;
+	RBExtDBSourceType source_type;
+	char *uri;
+	GValue *data;
+	GValue *value;
+
+	char *filename;
+	gboolean stored;
+} RBExtDBStoreRequest;
+
+G_DEFINE_TYPE (RBExtDB, rb_ext_db, G_TYPE_OBJECT)
+
+static void
+free_request (RBExtDBRequest *request)
+{
+	rb_ext_db_key_free (request->key);
+
+	g_free (request->filename);
+
+	if (request->data) {
+		g_value_unset (request->data);
+		g_free (request->data);
+	}
+
+	if (request->destroy_notify)
+		request->destroy_notify (request->user_data);
+
+	g_slice_free (RBExtDBRequest, request);
+}
+
+static void
+answer_request (RBExtDBRequest *request,
+		const char *filename,
+		GValue *data)
+{
+	request->callback (request->key, filename, data, request->user_data);
+	free_request (request);
+}
+
+static RBExtDBRequest *
+create_request (RBExtDBKey *key,
+		RBExtDBRequestCallback callback,
+		gpointer user_data,
+		GDestroyNotify destroy_notify)
+{
+	RBExtDBRequest *req = g_slice_new0 (RBExtDBRequest);
+	req->key = rb_ext_db_key_copy (key);
+	req->callback = callback;
+	req->user_data = user_data;
+	req->destroy_notify = destroy_notify;
+	return req;
+}
+
+
+static RBExtDBStoreRequest *
+create_store_request (RBExtDBKey *key,
+		      RBExtDBSourceType source_type,
+		      const char *uri,
+		      GValue *data,
+		      GValue *value)
+{
+	RBExtDBStoreRequest *sreq = g_slice_new0 (RBExtDBStoreRequest);
+	sreq->key = rb_ext_db_key_copy (key);
+	sreq->source_type = source_type;
+	if (uri != NULL) {
+		sreq->uri = g_strdup (uri);
+	}
+	if (data != NULL) {
+		sreq->data = g_new0 (GValue, 1);
+		g_value_init (sreq->data, G_VALUE_TYPE (data));
+		g_value_copy (data, sreq->data);
+	}
+	if (value != NULL) {
+		sreq->value = g_new0 (GValue, 1);
+		g_value_init (sreq->value, G_VALUE_TYPE (value));
+		g_value_copy (value, sreq->value);
+	}
+	return sreq;
+}
+
+static void
+free_store_request (RBExtDBStoreRequest *sreq)
+{
+	if (sreq->data != NULL) {
+		g_value_unset (sreq->data);
+		g_free (sreq->data);
+	}
+	if (sreq->value != NULL) {
+		g_value_unset (sreq->value);
+		g_free (sreq->value);
+	}
+	g_free (sreq->uri);
+	g_slice_free (RBExtDBStoreRequest, sreq);
+}
+
+
+static TDB_DATA
+flatten_data (guint64 search_time, const char *filename, RBExtDBSourceType source_type)
+{
+	GVariantBuilder vb;
+	GVariant *v;
+	TDB_DATA data;
+#if G_BYTE_ORDER == G_LITTLE_ENDIAN
+	GVariant *sv;
+#endif
+
+	g_variant_builder_init (&vb, G_VARIANT_TYPE ("a{sv}"));
+	g_variant_builder_add (&vb, "{sv}", "time", g_variant_new_uint64 (search_time));
+	if (filename != NULL) {
+		g_variant_builder_add (&vb, "{sv}", "file", g_variant_new_string (filename));
+	}
+	if (source_type != RB_EXT_DB_SOURCE_NONE) {
+		g_variant_builder_add (&vb, "{sv}", "srctype", g_variant_new_uint32 (source_type));
+	}
+	v = g_variant_builder_end (&vb);
+
+#if G_BYTE_ORDER == G_LITTLE_ENDIAN
+	sv = g_variant_byteswap (v);
+	g_variant_unref (v);
+	v = sv;
+#endif
+	data.dsize = g_variant_get_size (v);
+	data.dptr = g_malloc0 (data.dsize);
+	g_variant_store (v, data.dptr);
+	return data;
+}
+
+static void
+extract_data (TDB_DATA data, guint64 *search_time, char **filename, RBExtDBSourceType *source_type)
+{
+	GVariant *v;
+	GVariant *sv;
+	GVariantIter iter;
+	GVariant *value;
+	char *key;
+
+	if (data.dptr == NULL || data.dsize == 0) {
+		return;
+	}
+
+	v = g_variant_new_from_data (G_VARIANT_TYPE ("a{sv}"), data.dptr, data.dsize, FALSE, NULL, NULL);
+#if G_BYTE_ORDER == G_LITTLE_ENDIAN
+	sv = g_variant_byteswap (v);
+#else
+	sv = g_variant_get_normal_form (v);
+#endif
+	g_variant_unref (v);
+
+	g_variant_iter_init (&iter, sv);
+	while (g_variant_iter_loop (&iter, "{sv}", &key, &value)) {
+		if (g_strcmp0 (key, "time") == 0) {
+			if (search_time != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_UINT64)) {
+				*search_time = g_variant_get_uint64 (value);
+			}
+		} else if (g_strcmp0 (key, "file") == 0) {
+			if (filename != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) {
+				*filename = g_variant_dup_string (value, NULL);
+			}
+		} else if (g_strcmp0 (key, "srctype") == 0) {
+			if (source_type != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32)) {
+				*source_type = g_variant_get_uint32 (value);
+			}
+		} else {
+			rb_debug ("unknown key %s in metametadata", key);
+		}
+	}
+
+	g_variant_unref (sv);
+}
+
+
+static GValue *
+default_load (RBExtDB *store, GValue *data)
+{
+	GValue *v = g_new0 (GValue, 1);
+	g_value_init (v, G_VALUE_TYPE (data));
+	g_value_copy (data, v);
+	return v;
+}
+
+static GValue *
+default_store (RBExtDB *store, GValue *data)
+{
+	GValue *v = g_new0 (GValue, 1);
+	g_value_init (v, G_VALUE_TYPE (data));
+	g_value_copy (data, v);
+	return v;
+}
+
+static void
+impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+	RBExtDB *store = RB_EXT_DB (object);
+	switch (prop_id) {
+	case PROP_NAME:
+		g_value_set_string (value, store->priv->name);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+	RBExtDB *store = RB_EXT_DB (object);
+	switch (prop_id) {
+	case PROP_NAME:
+		store->priv->name = g_value_dup_string (value);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static GObject *
+impl_constructor (GType type, guint n_construct_properties, GObjectConstructParam *construct_properties)
+{
+	GList *l;
+	int i;
+	const char *name;
+	char *storedir;
+	char *tdbfile;
+	RBExtDB *store;
+
+	/* check for an existing instance of this metadata store */
+	name = NULL;
+	for (i = 0; i < n_construct_properties; i++) {
+		if (g_strcmp0 (g_param_spec_get_name (construct_properties[i].pspec), "name") == 0) {
+			name = g_value_get_string (construct_properties[i].value);
+		}
+	}
+	g_assert (name != NULL);
+
+	for (l = instances; l != NULL; l = l->next) {
+		RBExtDB *inst = l->data;
+		if (g_strcmp0 (name, inst->priv->name) == 0) {
+			rb_debug ("found existing metadata store %s", name);
+			return g_object_ref (inst);
+		}
+	}
+
+	rb_debug ("creating new metadata store instance %s", name);
+	/* construct the new instance */
+	store = RB_EXT_DB (G_OBJECT_CLASS (rb_ext_db_parent_class)->constructor (type, n_construct_properties, construct_properties));
+
+	/* open the cache db */
+	storedir = g_build_filename (rb_user_cache_dir (), name, NULL);
+	if (g_mkdir_with_parents (storedir, 0700) != 0) {
+		/* what can we do now? */
+		g_assert_not_reached ();
+	} else {
+		tdbfile = g_build_filename (storedir, "store.tdb", NULL);
+		store->priv->tdb_context = tdb_open (tdbfile, 999, TDB_INCOMPATIBLE_HASH | TDB_SEQNUM, O_RDWR | O_CREAT, 0600);
+		if (store->priv->tdb_context == NULL) {
+			/* umm */
+			g_assert_not_reached ();
+		}
+	}
+	g_free (storedir);
+
+	/* add to instance list */
+	instances = g_list_append (instances, store);
+
+	return G_OBJECT (store);
+}
+
+static void
+impl_finalize (GObject *object)
+{
+	RBExtDB *store = RB_EXT_DB (object);
+
+	g_free (store->priv->name);
+
+	g_list_free_full (store->priv->requests, (GDestroyNotify) free_request);
+	g_list_free_full (store->priv->store_queue, (GDestroyNotify) free_store_request);
+
+	if (store->priv->tdb_context) {
+		tdb_close (store->priv->tdb_context);
+	}
+
+	instances = g_list_remove (instances, store);
+
+	G_OBJECT_CLASS (rb_ext_db_parent_class)->finalize (object);
+}
+
+static void
+rb_ext_db_init (RBExtDB *store)
+{
+	store->priv = G_TYPE_INSTANCE_GET_PRIVATE (store, RB_TYPE_EXT_DB, RBExtDBPrivate);
+}
+
+static void
+rb_ext_db_class_init (RBExtDBClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+	object_class->set_property = impl_set_property;
+	object_class->get_property = impl_get_property;
+	object_class->constructor = impl_constructor;
+	object_class->finalize = impl_finalize;
+
+	klass->load = default_load;
+	klass->store = default_store;
+
+	/**
+	 * RBExtDB:name
+	 *
+	 * Name of the metadata store.  Used to locate instances.
+	 */
+	g_object_class_install_property (object_class,
+					 PROP_NAME,
+					 g_param_spec_string ("name",
+							      "name",
+							      "name",
+							      NULL,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	/**
+	 * RBExtDB::added:
+	 *
+	 * Emitted when metadata is added to the store.  Metadata consumers
+	 * can use this to process metadata they did not specifically
+	 * request, for example to update album art stored on an attached
+	 * media player.
+	 */
+	signals[ADDED] =
+		g_signal_new ("added",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RBExtDBClass, added),
+			      NULL, NULL, NULL,
+			      G_TYPE_NONE,
+			      3, RB_TYPE_EXT_DB_KEY, G_TYPE_STRING, G_TYPE_VALUE);
+	/**
+	 * RBExtDB::request:
+	 *
+	 * Emitted when a metadata request cannot be satisfied from the local
+	 * store.  Metadata providers initiate searches in response to this
+	 * signal.
+	 */
+	signals[REQUEST] =
+		g_signal_new ("request",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RBExtDBClass, request),
+			      rb_signal_accumulator_boolean_or, NULL, NULL,
+			      G_TYPE_BOOLEAN,
+			      2, RB_TYPE_EXT_DB_KEY, G_TYPE_ULONG);
+	/**
+	 * RBExtDB::store:
+	 *
+	 * Emitted when a metadata item needs to be written to a local file.
+	 * This only needs to be used for metadata that needs to be encoded
+	 * or compressed, such as images.
+	 */
+	signals[STORE] =
+		g_signal_new ("store",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RBExtDBClass, store),
+			      rb_signal_accumulator_value_handled, NULL, NULL,
+			      G_TYPE_VALUE,
+			      1, G_TYPE_VALUE);
+	/**
+	 * RBExtDB::load:
+	 *
+	 * Emitted when loading a metadata item from a local file or from a
+	 * URI.
+	 */
+	signals[LOAD] =
+		g_signal_new ("load",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      G_STRUCT_OFFSET (RBExtDBClass, load),
+			      rb_signal_accumulator_value_handled, NULL, NULL,
+			      G_TYPE_VALUE,
+			      1, G_TYPE_VALUE);
+
+	g_type_class_add_private (klass, sizeof (RBExtDBPrivate));
+}
+
+
+/**
+ * rb_ext_db_new:
+ * @name: name of the metadata store
+ *
+ * Provides access to a metadata store instance.
+ *
+ * Return value: named metadata store instance
+ */
+RBExtDB *
+rb_ext_db_new (const char *name)
+{
+	return RB_EXT_DB (g_object_new (RB_TYPE_EXT_DB, "name", name, NULL));
+}
+
+typedef struct {
+	RBExtDB *store;
+	char **filename;
+	guint64 search_time;
+	RBExtDBSourceType source_type;
+} RBExtDBLookup;
+
+static gboolean
+lookup_cb (TDB_DATA data, gpointer user_data)
+{
+	TDB_DATA tdbvalue;
+	RBExtDBLookup *lookup = user_data;
+	char *fn = NULL;
+	RBExtDBSourceType source_type = RB_EXT_DB_SOURCE_NONE;
+	guint64 search_time = 0;
+
+	tdbvalue = tdb_fetch (lookup->store->priv->tdb_context, data);
+	if (tdbvalue.dptr == NULL) {
+		rb_debug ("lookup failed");
+		return TRUE;
+	}
+
+	extract_data (tdbvalue, &search_time, &fn, &source_type);
+
+	switch (source_type) {
+	case RB_EXT_DB_SOURCE_NONE:
+		if (lookup->search_time == 0)
+			lookup->search_time = search_time;
+		break;
+	default:
+		if (source_type > lookup->source_type && fn != NULL) {
+			g_free (*lookup->filename);
+			*lookup->filename = fn;
+			lookup->source_type = source_type;
+			lookup->search_time = search_time;
+			rb_debug ("found new best match %s, %d", fn, source_type);
+		} else {
+			g_free (fn);
+			rb_debug ("don't care about match %d", source_type);
+		}
+		break;
+	}
+	free (tdbvalue.dptr);
+	return TRUE;
+}
+
+/**
+ * rb_ext_db_lookup:
+ * @store: metadata store instance
+ * @key: metadata lookup key
+ *
+ * Looks up a cached metadata item.
+ *
+ * Return value: name of the file storing the cached metadata item
+ */
+char *
+rb_ext_db_lookup (RBExtDB *store, RBExtDBKey *key)
+{
+	char *fn = NULL;
+	RBExtDBLookup lookup;
+	char *path;
+
+	lookup.store = store;
+	lookup.filename = &fn;
+	lookup.source_type = RB_EXT_DB_SOURCE_NONE;
+	lookup.search_time = 0;
+
+	rb_ext_db_key_lookups (key, lookup_cb, &lookup);
+
+	if (fn == NULL) {
+		return NULL;
+	}
+
+	path = g_build_filename (rb_user_cache_dir (), store->priv->name, fn, NULL);
+	g_free (fn);
+	return path;
+}
+
+static void
+load_request_cb (RBExtDB *store, GAsyncResult *result, gpointer data)
+{
+	RBExtDBRequest *req;
+	req = g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result));
+
+	rb_debug ("finished loading %s", req->filename);
+	req->callback (req->key, req->filename, req->data, req->user_data);
+
+	g_object_unref (result);
+}
+
+static void
+do_load_request (GSimpleAsyncResult *result, GObject *object, GCancellable *cancel)
+{
+	RBExtDBRequest *req;
+	GFile *f;
+	char *file_data;
+	gsize file_data_size;
+	GError *error = NULL;
+
+	req = g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result));
+
+	rb_debug ("loading data from %s", req->filename);
+	f = g_file_new_for_path (req->filename);
+	g_file_load_contents (f, NULL, &file_data, &file_data_size, NULL, &error);
+	if (error != NULL) {
+		rb_debug ("unable to load item %s: %s", req->filename, error->message);
+		g_clear_error (&error);
+
+		/* probably need to delete the item from the db */
+	} else {
+		GString *s;
+		GValue d = G_VALUE_INIT;
+
+		/* convert the encoded data into a useful object */
+		rb_debug ("converting %" G_GSIZE_FORMAT " bytes of file data", file_data_size);
+		s = g_string_new_len (file_data, file_data_size);
+		g_value_init (&d, G_TYPE_GSTRING);
+		g_value_take_boxed (&d, s);
+		g_signal_emit (object, signals[LOAD], 0, &d, &req->data);
+		g_value_unset (&d);
+
+		rb_debug ("converted data into value of type %s", G_VALUE_TYPE_NAME (req->data));
+	}
+
+	g_object_unref (f);
+}
+
+
+/**
+ * rb_ext_db_request:
+ * @store: metadata store instance
+ * @key: metadata lookup key
+ * @callback: callback to call with results
+ * @user_data: user data to pass to the callback
+ * @destroy: destroy function for @user_data
+ *
+ * Requests a metadata item.  If the item is cached, the callback will be called
+ * synchronously.  Otherwise, metadata providers will provide results asynchronously.
+ *
+ * Return value: %TRUE if results may be provided after returning
+ */
+gboolean
+rb_ext_db_request (RBExtDB *store,
+			      RBExtDBKey *key,
+			      RBExtDBRequestCallback callback,
+			      gpointer user_data,
+			      GDestroyNotify destroy)
+{
+	RBExtDBRequest *req;
+	gboolean result;
+	gulong last_time;
+	TDB_DATA tdbvalue;
+	TDB_DATA tdbkey;
+	char *filename;
+	GList *l;
+	gboolean emit_request = TRUE;
+
+	rb_debug ("starting metadata request");
+
+	filename = rb_ext_db_lookup (store, key);
+	if (filename != NULL) {
+		GSimpleAsyncResult *load_op;
+		rb_debug ("found cached match %s", filename);
+		load_op = g_simple_async_result_new (G_OBJECT (store),
+						     (GAsyncReadyCallback) load_request_cb,
+						     NULL,
+						     rb_ext_db_request);
+
+		req = create_request (key, callback, user_data, destroy);
+		req->filename = filename;
+		g_simple_async_result_set_op_res_gpointer (load_op, req, (GDestroyNotify) free_request);
+
+		g_simple_async_result_run_in_thread (load_op,
+						     do_load_request,
+						     G_PRIORITY_DEFAULT,
+						     NULL);	/* no cancel? */
+		return FALSE;
+	}
+
+	/* discard duplicate requests */
+	for (l = store->priv->requests; l != NULL; l = l->next) {
+		req = l->data;
+		if (req->callback == callback &&
+		    req->user_data == user_data &&
+		    req->destroy_notify == destroy &&
+		    rb_ext_db_key_matches (key, req->key)) {
+			rb_debug ("found matching existing request");
+			if (destroy)
+				destroy (user_data);
+			return TRUE;
+		} else if (rb_ext_db_key_matches (key, req->key)) {
+			rb_debug ("found existing equivalent request");
+			emit_request = FALSE;
+		}
+	}
+
+	/* lookup previous request time */
+	tdbkey = rb_ext_db_key_to_store_key (key);
+
+	tdbvalue = tdb_fetch (store->priv->tdb_context, tdbkey);
+	if (tdbvalue.dptr != NULL) {
+		extract_data (tdbvalue, &last_time, NULL, NULL);
+		free (tdbvalue.dptr);
+	} else {
+		last_time = 0;
+	}
+
+	/* add stuff to list of outstanding requests */
+	req = create_request (key, callback, user_data, destroy);
+	store->priv->requests = g_list_append (store->priv->requests, req);
+
+	/* and let metadata providers request it */
+	if (emit_request) {
+		result = FALSE;
+		g_signal_emit (store, signals[REQUEST], 0, req->key, last_time, &result);
+	} else {
+		result = TRUE;
+	}
+
+	/* free the request if result == FALSE? */
+	return result;
+}
+
+
+static void
+store_request_cb (RBExtDB *store, GAsyncResult *result, gpointer data)
+{
+	RBExtDBStoreRequest *sreq;
+	sreq = g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result));
+
+	if (sreq == NULL) {
+		/* do nothing */
+	} else if (sreq->stored) {
+		GList *l;
+
+		/* answer any matching queries */
+		l = store->priv->requests;
+		while (l != NULL) {
+			RBExtDBRequest *req = l->data;
+			if (rb_ext_db_key_matches (sreq->key, req->key)) {
+				GList *n = l->next;
+				rb_debug ("answering metadata request %p", req);
+				answer_request (req, sreq->filename, sreq->value);
+				store->priv->requests = g_list_delete_link (store->priv->requests, l);
+				l = n;
+			} else {
+				l = l->next;
+			}
+		}
+
+		/* let passive metadata consumers see it too */
+		rb_debug ("added; filename = %s, value type = %s", sreq->filename, sreq->value ? G_VALUE_TYPE_NAME (sreq->value) : "<none>");
+		g_signal_emit (store, signals[ADDED], 0, sreq->key, sreq->filename, sreq->value);
+	} else {
+		rb_debug ("no metadata was stored");
+	}
+
+	g_object_unref (store->priv->store_op);
+	store->priv->store_op = NULL;
+
+	/* start another store request if we have one */
+	maybe_start_store_request (store);
+}
+
+static void
+do_store_request (GSimpleAsyncResult *result, GObject *object, GCancellable *cancel)
+{
+	RBExtDB *store = RB_EXT_DB (object);
+	RBExtDBStoreRequest *req;
+	RBExtDBSourceType last_source_type = RB_EXT_DB_SOURCE_NONE;
+	guint64 last_time = 0;
+	const char *file_data;
+	char *filename = NULL;
+	gssize file_data_size;
+	GTimeVal now;
+	TDB_DATA tdbkey;
+	TDB_DATA tdbdata;
+	gboolean ignore;
+
+	if (store->priv->store_queue == NULL) {
+		rb_debug ("nothing to do");
+		g_simple_async_result_set_op_res_gpointer (result, NULL, NULL);
+		return;
+	}
+
+	req = store->priv->store_queue->data;
+	store->priv->store_queue = g_list_delete_link (store->priv->store_queue,
+						  store->priv->store_queue);
+	g_simple_async_result_set_op_res_gpointer (result, req, (GDestroyNotify)free_store_request);
+
+	/* convert key to storage blob */
+	tdbkey = rb_ext_db_key_to_store_key (req->key);
+
+	/* fetch current contents, if any */
+	tdbdata = tdb_fetch (store->priv->tdb_context, tdbkey);
+	extract_data (tdbdata, &last_time, &filename, &last_source_type);
+
+	if (req->source_type == last_source_type) {
+		/* ignore new data if it just comes from a search,
+		 * but otherwise update.
+		 */
+		ignore = (last_source_type == RB_EXT_DB_SOURCE_SEARCH);
+	} else {
+		/* ignore if from a lower priority source */
+		ignore = (req->source_type < last_source_type);
+	}
+
+	if (ignore) {
+		/* don't replace it */
+		rb_debug ("existing result is from a higher or equal priority source");
+		g_free (filename);
+		g_free (tdbkey.dptr);
+		if (tdbdata.dptr != NULL)
+			free (tdbdata.dptr);
+		return;
+	}
+
+	/* if the metadata item is specified by a uri, retrieve the data */
+	if (req->uri != NULL) {
+		GFile *f;
+		GError *error = NULL;
+		char *data;
+		gsize data_size;
+
+		rb_debug ("fetching uri %s", req->uri);
+		f = g_file_new_for_uri (req->uri);
+		g_file_load_contents (f, NULL, &data, &data_size, NULL, &error);
+		if (error != NULL) {
+			/* complain a bit */
+			rb_debug ("unable to read %s: %s", req->uri, error->message);
+			g_clear_error (&error);
+			/* leave req->data alone so we fall into the failure branch? */
+		} else {
+			GString *s;
+			rb_debug ("got %" G_GSIZE_FORMAT " bytes from uri %s", data_size, req->uri);
+			s = g_string_new_len (data, data_size);
+			req->data = g_new0 (GValue, 1);
+			g_value_init (req->data, G_TYPE_GSTRING);
+			g_value_take_boxed (req->data, s);
+		}
+
+		g_object_unref (f);
+	}
+
+	if (req->data != NULL && req->value != NULL) {
+		/* how did this happen? */
+	} else if (req->data != NULL) {
+		/* we got encoded data from somewhere; load it so we can
+		 * pass it out to metadata consumers
+		 */
+		g_signal_emit (store, signals[LOAD], 0, req->data, &req->value);
+		rb_debug ("converted encoded data into value of type %s", G_VALUE_TYPE_NAME (req->value));
+	} else if (req->value != NULL) {
+		/* we got an object representing the data; store it so we
+		 * can write it to a file
+		 */
+		g_signal_emit (store, signals[STORE], 0, req->value, &req->data);
+		rb_debug ("stored value into encoded data of type %s", G_VALUE_TYPE_NAME (req->data));
+	} else {
+		/* indicates we actually didn't get anything, as opposed to communication errors etc.
+		 * providers just shouldn't call rb_ext_db_store_* in that case.
+		 */
+		req->source_type = RB_EXT_DB_SOURCE_NONE;
+	}
+
+	/* get data to write to file */
+	file_data = NULL;
+	file_data_size = 0;
+	if (req->data == NULL) {
+		/* do nothing */
+	} else if (G_VALUE_HOLDS_STRING (req->data)) {
+		file_data = g_value_get_string (req->data);
+		file_data_size = strlen (file_data);
+	} else if (G_VALUE_HOLDS (req->data, G_TYPE_BYTE_ARRAY)) {
+		GByteArray *bytes = g_value_get_boxed (req->data);
+		file_data = (const char *)bytes->data;
+		file_data_size = bytes->len;
+	} else if (G_VALUE_HOLDS (req->data, G_TYPE_GSTRING)) {
+		GString *str = g_value_get_boxed (req->data);
+		file_data = (const char *)str->str;
+		file_data_size = str->len;
+	} else {
+		/* warning? */
+		rb_debug ("don't know how to save data of type %s", G_VALUE_TYPE_NAME (req->data));
+	}
+
+	if (file_data != NULL && file_data_size > 0) {
+		GFile *f;
+		GError *error = NULL;
+
+		if (filename == NULL) {
+			filename = g_strdup_printf ("%8.8x", tdb_get_seqnum (store->priv->tdb_context));
+			rb_debug ("generated filename %s", filename);
+		} else {
+			rb_debug ("using existing filename %s", filename);
+		}
+
+		req->filename = g_build_filename (rb_user_cache_dir (),
+						  store->priv->name,
+						  filename,
+						  NULL);
+		f = g_file_new_for_path (req->filename);
+
+		g_file_replace_contents (f,
+					 file_data,
+					 file_data_size,
+					 NULL,
+					 FALSE,
+					 G_FILE_CREATE_REPLACE_DESTINATION,
+					 NULL,
+					 NULL,
+					 &error);
+		if (error != NULL) {
+			rb_debug ("error saving %s: %s", req->filename, error->message);
+			g_clear_error (&error);
+		} else {
+			req->stored = TRUE;
+		}
+		g_object_unref (f);
+	} else if (req->source_type == RB_EXT_DB_SOURCE_NONE) {
+		req->stored = TRUE;
+	}
+
+	if (req->stored) {
+		TDB_DATA store_data;
+
+		g_get_current_time (&now);
+		rb_debug ("actually storing this in the database");
+		store_data = flatten_data (now.tv_sec, filename, req->source_type);
+		tdb_store (store->priv->tdb_context, tdbkey, store_data, 0);
+		/* XXX warn on error.. */
+		g_free (store_data.dptr);
+	}
+
+	if (tdbdata.dptr) {
+		free (tdbdata.dptr);
+		tdbdata.dptr = NULL;
+	}
+
+	g_free (filename);
+
+	g_free (tdbkey.dptr);
+}
+
+static void
+maybe_start_store_request (RBExtDB *store)
+{
+	if (store->priv->store_op != NULL) {
+		rb_debug ("already doing something");
+		return;
+	}
+
+	if (store->priv->store_queue == NULL) {
+		rb_debug ("nothing to do");
+		return;
+	}
+
+	store->priv->store_op = g_simple_async_result_new (G_OBJECT (store),
+							   (GAsyncReadyCallback) store_request_cb,
+							   NULL,
+							   maybe_start_store_request);
+	g_simple_async_result_run_in_thread (store->priv->store_op,
+					     do_store_request,
+					     G_PRIORITY_DEFAULT,
+					     NULL);	/* no cancel? */
+}
+
+static void
+store_metadata (RBExtDB *store, RBExtDBStoreRequest *req)
+{
+	store->priv->store_queue = g_list_append (store->priv->store_queue, req);
+	rb_debug ("now %d entries in store queue", g_list_length (store->priv->store_queue));
+	maybe_start_store_request (store);
+}
+
+
+/**
+ * rb_ext_db_store_uri:
+ * @store: metadata store instance
+ * @key: metadata storage key
+ * @source_type: metadata source type
+ * @uri: (allow-none): URI of the item to store
+ *
+ * Stores an item identified by @uri in the metadata store so that
+ * lookups matching @key will return it.
+ */
+void
+rb_ext_db_store_uri (RBExtDB *store,
+		     RBExtDBKey *key,
+		     RBExtDBSourceType source_type,
+		     const char *uri)
+{
+	rb_debug ("storing uri %s", uri);
+	store_metadata (store, create_store_request (key, source_type, uri, NULL, NULL));
+}
+
+/**
+ * rb_ext_db_store:
+ * @store: metadata store instance
+ * @key: metadata storage key
+ * @source_type: metadata source type
+ * @data: (allow-none): data to store
+ *
+ * Stores an item in the metadata store so that lookups matching @key will
+ * return it.  @data should contain an object that must be transformed using
+ * the RBExtDB::store signal before being stored.  For example,
+ * the album art cache expects #GdkPixbuf objects here, rather than buffers
+ * containing JPEG encoded files.
+ */
+void
+rb_ext_db_store (RBExtDB *store,
+		 RBExtDBKey *key,
+		 RBExtDBSourceType source_type,
+		 GValue *data)
+{
+	rb_debug ("storing value of type %s", data ? G_VALUE_TYPE_NAME (data) : "<none>");
+	store_metadata (store, create_store_request (key, source_type, NULL, NULL, data));
+}
+
+/**
+ * rb_ext_db_store_raw:
+ * @store: metadata store instance
+ * @key: metadata storage key
+ * @source_type: metadata source type
+ * @data: (allow-none): data to store
+ *
+ * Stores an item in the metadata store so that lookpus matching @key
+ * will return it.  @data should contain the data to be written to the
+ * store, either as a string or as a #GByteArray.
+ */
+void
+rb_ext_db_store_raw (RBExtDB *store,
+		     RBExtDBKey *key,
+		     RBExtDBSourceType source_type,
+		     GValue *data)
+{
+	rb_debug ("storing encoded data of type %s", data ? G_VALUE_TYPE_NAME (data) : "<none>");
+	store_metadata (store, create_store_request (key, source_type, NULL, data, NULL));
+}
+
+#define ENUM_ENTRY(NAME, DESC) { NAME, "" #NAME "", DESC }
+
+GType
+rb_ext_db_source_type_get_type (void)
+{
+	static GType etype = 0;
+
+	if (etype == 0) {
+		static const GEnumValue values[] = {
+			ENUM_ENTRY(RB_EXT_DB_SOURCE_NONE, "none"),
+			ENUM_ENTRY(RB_EXT_DB_SOURCE_SEARCH, "search"),
+			ENUM_ENTRY(RB_EXT_DB_SOURCE_EMBEDDED, "embedded"),
+			ENUM_ENTRY(RB_EXT_DB_SOURCE_USER, "user"),
+			ENUM_ENTRY(RB_EXT_DB_SOURCE_USER_EXPLICIT, "user-explicit"),
+			{ 0, 0, 0 }
+		};
+		etype = g_enum_register_static ("RBExtDBSourceType", values);
+	}
+
+	return etype;
+}
diff --git a/metadata/rb-ext-db.h b/metadata/rb-ext-db.h
new file mode 100644
index 0000000..03b9fcf
--- /dev/null
+++ b/metadata/rb-ext-db.h
@@ -0,0 +1,124 @@
+/*
+ *  Copyright (C) 2011 Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#ifndef RB_EXT_DB_H
+#define RB_EXT_DB_H
+
+#include <glib-object.h>
+
+#include <metadata/rb-ext-db-key.h>
+
+G_BEGIN_DECLS
+
+typedef struct _RBExtDB RBExtDB;
+typedef struct _RBExtDBClass RBExtDBClass;
+typedef struct _RBExtDBPrivate RBExtDBPrivate;
+
+#define RB_TYPE_EXT_DB      	(rb_ext_db_get_type ())
+#define RB_EXT_DB(o)        	(G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_EXT_DB, RBExtDB))
+#define RB_EXT_DB_CLASS(k)  	(G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_EXT_DB, RBExtDBClass))
+#define RB_IS_EXT_DB(o)     	(G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_EXT_DB))
+#define RB_IS_EXT_DB_CLASS(k) 	(G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_EXT_DB))
+#define RB_EXT_DB_GET_CLASS(o) 	(G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_EXT_DB, RBExtDBClass))
+
+/* these are in priority order - higher is preferred to lower */
+typedef enum {
+	RB_EXT_DB_SOURCE_NONE,		/* nothing */
+	RB_EXT_DB_SOURCE_SEARCH,	/* found by external search */
+	RB_EXT_DB_SOURCE_EMBEDDED,	/* embedded in media itself */
+	RB_EXT_DB_SOURCE_USER,		/* provided by user (eg image in same dir) */
+	RB_EXT_DB_SOURCE_USER_EXPLICIT	/* provided explicitly by user */
+} RBExtDBSourceType;
+
+GType		rb_ext_db_source_type_get_type (void);
+#define RB_TYPE_EXT_DB_SOURCE_TYPE (rb_ext_db_source_type_get_type ())
+
+
+struct _RBExtDB
+{
+	GObject parent;
+
+	RBExtDBPrivate *priv;
+};
+
+struct _RBExtDBClass
+{
+	GObjectClass parent;
+
+	/* requestor signals */
+	void		(*added)	(RBExtDB *store,
+					 RBExtDBKey *key,
+					 const char *filename,
+					 GValue *data);
+
+	/* provider signals */
+	gboolean	(*request)	(RBExtDB *store,
+					 RBExtDBKey *key,
+					 guint64 last_time);
+
+	/* data format conversion signals */
+	GValue *	(*store)	(RBExtDB *store,
+					 GValue *data);
+	GValue *	(*load)		(RBExtDB *store,
+					 GValue *data);
+};
+
+typedef void (*RBExtDBRequestCallback) (RBExtDBKey *key, const char *filename, GValue *data, gpointer user_data);
+
+GType			rb_ext_db_get_type 		(void);
+
+RBExtDB *		rb_ext_db_new			(const char *name);
+
+/* for requestors */
+char *			rb_ext_db_lookup		(RBExtDB *store,
+							 RBExtDBKey *key);
+
+gboolean		rb_ext_db_request		(RBExtDB *store,
+							 RBExtDBKey *key,
+							 RBExtDBRequestCallback callback,
+							 gpointer user_data,
+							 GDestroyNotify destroy);
+
+/* for providers */
+void			rb_ext_db_store_uri		(RBExtDB *store,
+							 RBExtDBKey *key,
+							 RBExtDBSourceType source_type,
+							 const char *uri);
+
+void			rb_ext_db_store			(RBExtDB *store,
+							 RBExtDBKey *key,
+							 RBExtDBSourceType source_type,
+							 GValue *data);
+
+void			rb_ext_db_store_raw		(RBExtDB *store,
+							 RBExtDBKey *key,
+							 RBExtDBSourceType source_type,
+							 GValue *data);
+
+G_END_DECLS
+
+#endif /* RB_EXT_DB_H */



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