[rhythmbox] add grilo plugin



commit 0296f0fe21708cba3b2402dc99f30a5ff13feac2
Author: Jonathan Matthew <jonathan d14n org>
Date:   Sat Sep 10 23:18:57 2011 +1000

    add grilo plugin
    
    Still a few rough edges, but everything seems to work.  This
    replaces the coherence and jamendo plugins.

 configure.ac                         |   23 +
 data/org.gnome.rhythmbox.gschema.xml |    4 +
 plugins/Makefile.am                  |    4 +
 plugins/grilo/Makefile.am            |   45 ++
 plugins/grilo/grilo.plugin.in        |    8 +
 plugins/grilo/rb-grilo-plugin.c      |  184 ++++++
 plugins/grilo/rb-grilo-source.c      | 1104 ++++++++++++++++++++++++++++++++++
 plugins/grilo/rb-grilo-source.h      |   65 ++
 po/POTFILES.in                       |    3 +
 9 files changed, 1440 insertions(+), 0 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 98776f2..59264ee 100644
--- a/configure.ac
+++ b/configure.ac
@@ -58,6 +58,7 @@ LIBSOUP_REQS=2.26.0
 GUDEV_REQS=143
 LIBMTP_REQS=0.3.0
 LIBPEAS_REQS=0.7.3
+GRILO_REQS=0.1.17
 
 LIBNOTIFY_REQS=0.7.0
 BRASERO_MIN_REQS=2.31.5
@@ -784,6 +785,27 @@ AC_SUBST(JSON_GLIB_CFLAGS)
 AC_SUBST(JSON_GLIB_LIBS)
 
 dnl ================================================================
+dnl grilo plugin
+dnl ================================================================
+AC_ARG_ENABLE(grilo,
+	      AC_HELP_STRING([--disable-grilo],
+			     [Disable Grilo support]),,
+	      enable_grilo=auto)
+if test "x$enable_grilo" != "xno"; then
+	PKG_CHECK_MODULES(GRILO, grilo-0.1 >= $GRILO_REQS, have_grilo=yes, have_grilo=no)
+	if test "x$have_grilo" = "xno" -a "x$enable_grilo" = "xyes"; then
+		AC_MSG_ERROR([Grilo support explicitly requested, but grilo couldn not be found])
+	fi
+	if test "x$have_grilo" = "xyes"; then
+		AC_DEFINE(HAVE_GRILO, 1, [Define if Grilo support is enabled])
+	fi
+fi
+
+AM_CONDITIONAL(ENABLE_GRILO, test x"$have_grilo" = "xyes")
+AC_SUBST(GRILO_CFLAGS)
+AC_SUBST(GRILO_LIBS)
+
+dnl ================================================================
 dnl end-game
 dnl ================================================================
 
@@ -861,6 +883,7 @@ plugins/dbus-media-server/Makefile
 plugins/rbzeitgeist/Makefile
 plugins/notification/Makefile
 plugins/visualizer/Makefile
+plugins/grilo/Makefile
 bindings/Makefile
 bindings/vala/Makefile
 bindings/gi/Makefile
diff --git a/data/org.gnome.rhythmbox.gschema.xml b/data/org.gnome.rhythmbox.gschema.xml
index 4201257..f8986f4 100644
--- a/data/org.gnome.rhythmbox.gschema.xml
+++ b/data/org.gnome.rhythmbox.gschema.xml
@@ -415,4 +415,8 @@
       <description>The frame rate and size to use for visual effects</description>
     </key>
   </schema>
+
+  <schema id="org.gnome.rhythmbox.plugins.grilo" path="/org/gnome/rhythmbox/plugins/grilo/">
+    <child name="source" schema="org.gnome.rhythmbox.source"/>
+  </schema>
 </schemalist>
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 50c9b4f..faa09d7 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -66,3 +66,7 @@ endif
 if USE_NOTIFY
 SUBDIRS += notification
 endif
+
+if ENABLE_GRILO
+SUBDIRS += grilo
+endif
diff --git a/plugins/grilo/Makefile.am b/plugins/grilo/Makefile.am
new file mode 100644
index 0000000..cab8672
--- /dev/null
+++ b/plugins/grilo/Makefile.am
@@ -0,0 +1,45 @@
+plugindir = $(PLUGINDIR)/grilo
+plugindatadir = $(PLUGINDATADIR)/grilo
+plugin_LTLIBRARIES = libgrilo.la
+
+libgrilo_la_SOURCES =					\
+	rb-grilo-plugin.c				\
+	rb-grilo-source.c				\
+	rb-grilo-source.h
+
+libgrilo_la_LDFLAGS = $(PLUGIN_LIBTOOL_FLAGS)
+libgrilo_la_LIBTOOLFLAGS = --tag=disable-static
+
+libgrilo_la_LIBADD = \
+	$(top_builddir)/shell/librhythmbox-core.la	\
+	$(GRILO_LIBS)
+
+INCLUDES = 						\
+        -DGNOMELOCALEDIR=\""$(datadir)/locale"\"        \
+	-DG_LOG_DOMAIN=\"Rhythmbox\"		 	\
+	-I$(top_srcdir) 				\
+	-I$(top_srcdir)/lib                             \
+	-I$(top_srcdir)/rhythmdb                       	\
+	-I$(top_srcdir)/widgets                    	\
+	-I$(top_srcdir)/sources                    	\
+	-I$(top_srcdir)/plugins				\
+	-I$(top_srcdir)/shell				\
+	-DPIXMAP_DIR=\""$(datadir)/pixmaps"\"		\
+	-DSHARE_DIR=\"$(pkgdatadir)\"                   \
+	-DDATADIR=\""$(datadir)"\"			\
+	$(RHYTHMBOX_CFLAGS)				\
+	$(GRILO_CFLAGS)					\
+	-D_XOPEN_SOURCE -D_BSD_SOURCE
+
+libgrilo_la_LIBADD += $(NULL)
+
+plugin_in_files = grilo.plugin.in
+
+%.plugin: %.plugin.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po) ; $(INTLTOOL_MERGE) $(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache
+
+plugin_DATA = $(plugin_in_files:.plugin.in=.plugin)
+
+EXTRA_DIST = $(plugin_in_files)
+
+CLEANFILES = $(plugin_DATA)
+DISTCLEANFILES = $(plugin_DATA)
diff --git a/plugins/grilo/grilo.plugin.in b/plugins/grilo/grilo.plugin.in
new file mode 100644
index 0000000..38df8dd
--- /dev/null
+++ b/plugins/grilo/grilo.plugin.in
@@ -0,0 +1,8 @@
+[Plugin]
+Module=grilo
+IAge=2
+_Name=Grilo media browser
+_Description=Browse various local and Internet media sources
+Authors=Jonathan Matthew
+Copyright=Copyright  2011 Jonathan Matthew
+Website=http://www.rhythmbox.org/
diff --git a/plugins/grilo/rb-grilo-plugin.c b/plugins/grilo/rb-grilo-plugin.c
new file mode 100644
index 0000000..07e6ef0
--- /dev/null
+++ b/plugins/grilo/rb-grilo-plugin.c
@@ -0,0 +1,184 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2011 Jonathan Matthew
+ *
+ * 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, 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 <glib.h>
+#include <glib-object.h>
+#include <glib/gi18n-lib.h>
+#include <gmodule.h>
+#include <gtk/gtk.h>
+
+#include <grilo.h>
+
+#include "rb-plugin-macros.h"
+#include "rb-debug.h"
+#include "rb-shell.h"
+#include "rb-grilo-source.h"
+#include "rb-display-page-group.h"
+
+#define RB_TYPE_GRILO_PLUGIN		(rb_grilo_plugin_get_type ())
+#define RB_GRILO_PLUGIN(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_GRILO_PLUGIN, RBGriloPlugin))
+#define RB_GRILO_PLUGIN_CLASS(k)	(G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_GRILO_PLUGIN, RBGriloPluginClass))
+#define RB_IS_GRILO_PLUGIN(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_GRILO_PLUGIN))
+#define RB_IS_GRILO_PLUGIN_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_GRILO_PLUGIN))
+#define RB_GRILO_PLUGIN_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_GRILO_PLUGIN, RBGriloPluginClass))
+
+static const char *ignored_plugins[] = {
+	"grl-apple-trailers",
+	"grl-bookmarks",
+	"grl-filesystem",
+	"grl-flickr",
+	"grl-podcasts",
+	"grl-tracker",
+	"grl-vimeo",
+	"grl-youtube"
+};
+
+typedef struct
+{
+	PeasExtensionBase parent;
+
+	GrlPluginRegistry *registry;
+	GHashTable *sources;
+} RBGriloPlugin;
+
+typedef struct
+{
+	PeasExtensionBaseClass parent_class;
+} RBGriloPluginClass;
+
+G_MODULE_EXPORT void peas_register_types (PeasObjectModule *module);
+GType	rb_grilo_plugin_get_type		(void) G_GNUC_CONST;
+
+static void rb_grilo_plugin_init (RBGriloPlugin *plugin);
+
+RB_DEFINE_PLUGIN(RB_TYPE_GRILO_PLUGIN, RBGriloPlugin, rb_grilo_plugin,)
+
+static void
+rb_grilo_plugin_init (RBGriloPlugin *plugin)
+{
+	rb_debug ("RBGriloPlugin initialising");
+}
+
+static void
+rb_grilo_plugin_source_deleted (RBGriloSource *source, RBGriloPlugin *plugin)
+{
+	GrlMediaSource *media_source;
+
+	g_object_get (source, "media-source", &media_source, NULL);
+	g_hash_table_remove (plugin->sources, media_source);
+	g_object_unref (media_source);
+}
+
+static void
+grilo_source_added_cb (GrlPluginRegistry *registry, GrlMediaPlugin *grilo_plugin, RBGriloPlugin *plugin)
+{
+	RBSource *grilo_source;
+	RBShell *shell;
+	int i;
+
+	if (GRL_IS_MEDIA_SOURCE (grilo_plugin) == FALSE) {
+		/* TODO use metadata sources for album art and lyrics */
+		rb_debug ("grilo source %s is not interesting",
+			  grl_media_plugin_get_name (grilo_plugin));
+		return;
+	}
+
+	for (i = 0; i < G_N_ELEMENTS (ignored_plugins); i++) {
+		if (g_str_equal (ignored_plugins[i], grl_media_plugin_get_id (grilo_plugin))) {
+			rb_debug ("grilo source %s is blacklisted",
+				  grl_media_plugin_get_name (grilo_plugin));
+			return;
+		}
+	}
+
+	rb_debug ("new grilo source: %s", grl_media_plugin_get_name (grilo_plugin));
+
+	grilo_source = rb_grilo_source_new (G_OBJECT (plugin), GRL_MEDIA_SOURCE (grilo_plugin));
+	g_hash_table_insert (plugin->sources, grilo_plugin, grilo_source);
+
+	/* probably put some sources under 'shared', some under 'stores'? */
+	g_object_get (plugin, "object", &shell, NULL);
+	rb_shell_append_display_page (shell, RB_DISPLAY_PAGE (grilo_source), RB_DISPLAY_PAGE_GROUP_SHARED);
+	g_object_unref (shell);
+}
+
+static void
+impl_activate (PeasActivatable *plugin)
+{
+	RBGriloPlugin *pi = RB_GRILO_PLUGIN (plugin);
+	GError *error = NULL;
+
+	pi->sources = g_hash_table_new_full (g_direct_hash,
+					     g_direct_equal,
+					     g_object_unref,
+					     g_object_unref);
+
+	grl_init (0, NULL);
+	pi->registry = grl_plugin_registry_get_default ();
+	g_signal_connect (pi->registry, "source-added", G_CALLBACK (grilo_source_added_cb), pi);
+	if (grl_plugin_registry_load_all (pi->registry, &error) == FALSE) {
+		g_warning ("Failed to load Grilo plugins: %s", error->message);
+		g_clear_error (&error);
+	}
+}
+
+static void
+_delete_cb (GVolume         *volume,
+	    RBSource        *source,
+	    RBGriloPlugin *plugin)
+{
+	/* block the source deleted handler so we don't modify the hash table
+	 * while iterating it.
+	 */
+	g_signal_handlers_block_by_func (source, rb_grilo_plugin_source_deleted, plugin);
+	rb_display_page_delete_thyself (RB_DISPLAY_PAGE (source));
+}
+
+static void
+impl_deactivate	(PeasActivatable *bplugin)
+{
+	RBGriloPlugin         *plugin = RB_GRILO_PLUGIN (bplugin);
+
+	g_hash_table_foreach (plugin->sources, (GHFunc)_delete_cb, plugin);
+	g_hash_table_destroy (plugin->sources);
+	plugin->sources = NULL;
+
+	g_object_unref (plugin->registry);
+	plugin->registry = NULL;
+}
+
+G_MODULE_EXPORT void
+peas_register_types (PeasObjectModule *module)
+{
+	rb_grilo_plugin_register_type (G_TYPE_MODULE (module));
+	_rb_grilo_source_register_type (G_TYPE_MODULE (module));
+	peas_object_module_register_extension_type (module,
+						    PEAS_TYPE_ACTIVATABLE,
+						    RB_TYPE_GRILO_PLUGIN);
+}
diff --git a/plugins/grilo/rb-grilo-source.c b/plugins/grilo/rb-grilo-source.c
new file mode 100644
index 0000000..88e22c4
--- /dev/null
+++ b/plugins/grilo/rb-grilo-source.c
@@ -0,0 +1,1104 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2011 Jonathan Matthew
+ *
+ *  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 <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include <grilo.h>
+
+#include "rhythmdb.h"
+#include "rb-shell.h"
+#include "rb-shell-player.h"
+#include "rb-grilo-source.h"
+#include "rb-util.h"
+#include "rb-debug.h"
+#include "rb-file-helpers.h"
+#include "rb-gst-media-types.h"
+#include "rb-search-entry.h"
+
+/* number of items to check before giving up on finding any
+ * of a particular type
+ */
+#define CONTAINER_GIVE_UP_POINT		100
+
+/* maximum number of tracks to fetch before stopping and
+ * requiring the user to ask for more.
+ */
+#define CONTAINER_MAX_TRACKS		1000
+
+/* number of items to fetch at once */
+#define CONTAINER_FETCH_SIZE		50
+
+enum {
+	CONTAINER_UNKNOWN_MEDIA = 0,
+	CONTAINER_NO_MEDIA,
+	CONTAINER_HAS_MEDIA
+};
+
+enum
+{
+	PROP_0,
+	PROP_GRILO_SOURCE
+};
+
+static void rb_grilo_source_dispose (GObject *object);
+static void rb_grilo_source_finalize (GObject *object);
+static void rb_grilo_source_constructed (GObject *object);
+static void impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);
+static void impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec);
+
+static void browser_selection_changed_cb (GtkTreeSelection *selection, RBGriloSource *source);
+static void browser_row_expanded_cb (GtkTreeView *tree_view, GtkTreeIter *iter, GtkTreePath *path, RBGriloSource *source);
+static void scroll_adjust_changed_cb (GtkAdjustment *adjustment, RBGriloSource *source);
+static void scroll_adjust_value_changed_cb (GtkAdjustment *adjustment, RBGriloSource *source);
+static gboolean maybe_expand_container (RBGriloSource *source);
+static void fetch_more_cb (GtkInfoBar *bar, gint response, RBGriloSource *source);
+static void search_cb (RBSearchEntry *search, const char *text, RBGriloSource *source);
+static void notify_sort_order_cb (GObject *object, GParamSpec *pspec, RBGriloSource *source);
+
+static void impl_delete_thyself (RBDisplayPage *page);
+static void impl_selected (RBDisplayPage *page);
+
+static RBEntryView *impl_get_entry_view (RBSource *source);
+
+typedef struct
+{
+	GrlData *grilo_data;
+	GrlData *grilo_container;
+} RBGriloEntryData;
+
+struct _RBGriloSourcePrivate
+{
+	GrlMediaSource *grilo_source;
+	GList *grilo_keys;
+
+	RhythmDBEntryType *entry_type;
+
+	/* some widgets and things */
+	GtkWidget *paned;
+	RhythmDBQueryModel *query_model;
+	RBEntryView *entry_view;
+	GtkTreeStore *browser_model;
+	GtkWidget *browser_view;
+	gboolean done_initial_browse;
+	GtkWidget *info_bar;
+	GtkWidget *info_bar_label;
+	RBSearchEntry *search_entry;
+
+	/* current browsing operation (should allow multiple concurrent ops?) */
+	guint browse_op;
+	GrlMedia *browse_container;
+	GtkTreeIter browse_container_iter;
+	guint browse_position;
+	gboolean browse_got_results;
+	gboolean browse_got_media;
+	guint maybe_expand_idle;
+
+	/* current media browse operation */
+	guint media_browse_op;
+	char *search_text;
+	GrlMedia *media_browse_container;
+	GtkTreeIter media_browse_container_iter;
+	guint media_browse_position;
+	gboolean media_browse_got_results;
+	gboolean media_browse_got_containers;
+	guint media_browse_limit;
+
+	RhythmDB *db;
+};
+
+G_DEFINE_DYNAMIC_TYPE (RBGriloSource, rb_grilo_source, RB_TYPE_SOURCE)
+
+/* entry type */
+typedef struct _RhythmDBEntryType RBGriloEntryType;
+typedef struct _RhythmDBEntryTypeClass RBGriloEntryTypeClass;
+
+GType rb_grilo_entry_type_get_type (void);
+
+G_DEFINE_DYNAMIC_TYPE (RBGriloEntryType, rb_grilo_entry_type, RHYTHMDB_TYPE_ENTRY_TYPE);
+
+static void
+rb_grilo_entry_type_destroy_entry (RhythmDBEntryType *etype, RhythmDBEntry *entry)
+{
+	RBGriloEntryData *data;
+
+	data = RHYTHMDB_ENTRY_GET_TYPE_DATA (entry, RBGriloEntryData);
+	g_object_unref (data->grilo_data);
+	if (data->grilo_container != NULL)
+		g_object_unref (data->grilo_container);
+}
+
+static void
+rb_grilo_entry_type_class_init (RBGriloEntryTypeClass *klass)
+{
+	RhythmDBEntryTypeClass *etype_class = RHYTHMDB_ENTRY_TYPE_CLASS (klass);
+	etype_class->can_sync_metadata = (RhythmDBEntryTypeBooleanFunc) rb_true_function;
+	etype_class->sync_metadata = (RhythmDBEntryTypeSyncFunc) rb_null_function;
+	etype_class->destroy_entry = rb_grilo_entry_type_destroy_entry;
+}
+
+static void
+rb_grilo_entry_type_class_finalize (RBGriloEntryTypeClass *klass)
+{
+}
+
+static void
+rb_grilo_entry_type_init (RBGriloEntryType *etype)
+{
+}
+
+static void
+rb_grilo_source_class_init (RBGriloSourceClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+	RBDisplayPageClass *page_class = RB_DISPLAY_PAGE_CLASS (klass);
+	RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
+
+	object_class->constructed = rb_grilo_source_constructed;
+	object_class->dispose = rb_grilo_source_dispose;
+	object_class->finalize = rb_grilo_source_finalize;
+	object_class->set_property = impl_set_property;
+	object_class->get_property = impl_get_property;
+
+	page_class->delete_thyself = impl_delete_thyself;
+	page_class->selected = impl_selected;
+
+	source_class->impl_get_entry_view = impl_get_entry_view;
+
+	g_object_class_install_property (object_class,
+					 PROP_GRILO_SOURCE,
+					 g_param_spec_object ("grilo-source",
+							      "grilo source",
+							      "grilo source object",
+							      GRL_TYPE_MEDIA_SOURCE,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	g_type_class_add_private (klass, sizeof (RBGriloSourcePrivate));
+}
+
+static void
+rb_grilo_source_class_finalize (RBGriloSourceClass *klass)
+{
+}
+
+static void
+rb_grilo_source_init (RBGriloSource *self)
+{
+	self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, RB_TYPE_GRILO_SOURCE, RBGriloSourcePrivate);
+}
+
+static void
+rb_grilo_source_finalize (GObject *object)
+{
+	RBGriloSource *source = RB_GRILO_SOURCE (object);
+
+	g_free (source->priv->search_text);
+
+	g_list_free (source->priv->grilo_keys);
+
+	G_OBJECT_CLASS (rb_grilo_source_parent_class)->finalize (object);
+}
+
+static void
+rb_grilo_source_dispose (GObject *object)
+{
+	RBGriloSource *source = RB_GRILO_SOURCE (object);
+
+	if (source->priv->browse_op != 0) {
+		grl_operation_cancel (source->priv->browse_op);
+		source->priv->browse_op = 0;
+	}
+
+	if (source->priv->media_browse_op != 0) {
+		grl_operation_cancel (source->priv->media_browse_op);
+		source->priv->media_browse_op = 0;
+	}
+
+	if (source->priv->query_model != NULL) {
+		g_object_unref (source->priv->query_model);
+		source->priv->query_model = NULL;
+	}
+
+	if (source->priv->entry_type != NULL) {
+		g_object_unref (source->priv->entry_type);
+		source->priv->entry_type = NULL;
+	}
+
+	if (source->priv->maybe_expand_idle != 0) {
+		g_source_remove (source->priv->maybe_expand_idle);
+		source->priv->maybe_expand_idle = 0;
+	}
+
+	G_OBJECT_CLASS (rb_grilo_source_parent_class)->dispose (object);
+}
+
+static void
+rb_grilo_source_constructed (GObject *object)
+{
+	RBGriloSource *source;
+	RBShell *shell;
+	RBShellPlayer *shell_player;
+	const GList *source_keys;
+	GtkTreeViewColumn *column;
+	GtkCellRenderer *renderer;
+	GtkTreeSelection *selection;
+	GtkWidget *scrolled;
+	GtkWidget *vbox;
+	GtkWidget *mainbox;
+	GtkAdjustment *adjustment;
+
+	RB_CHAIN_GOBJECT_METHOD (rb_grilo_source_parent_class, constructed, object);
+	source = RB_GRILO_SOURCE (object);
+
+	g_object_get (source, "shell", &shell, NULL);
+	g_object_get (shell,
+		      "db", &source->priv->db,
+		      "shell-player", &shell_player,
+		      NULL);
+	g_object_unref (shell);
+
+	g_object_get (source, "entry-type", &source->priv->entry_type, NULL);
+
+	source->priv->entry_view = rb_entry_view_new (source->priv->db, G_OBJECT (shell_player), TRUE, FALSE);
+	g_object_unref (shell_player);
+	g_signal_connect (source->priv->entry_view,
+			  "notify::sort-order",
+			  G_CALLBACK (notify_sort_order_cb),
+			  source);
+
+	source_keys = grl_metadata_source_supported_keys (GRL_METADATA_SOURCE (source->priv->grilo_source));
+
+	if (g_list_find ((GList *)source_keys, GRL_METADATA_KEY_TRACK_NUMBER)) {
+		rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_TRACK_NUMBER, FALSE);
+		source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
+							   GRL_METADATA_KEY_TRACK_NUMBER);
+	}
+
+	if (g_list_find ((GList *)source_keys, GRL_METADATA_KEY_TITLE)) {
+		rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_TITLE, TRUE);
+		source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
+							   GRL_METADATA_KEY_TITLE);
+	}
+
+	if (g_list_find ((GList *)source_keys, GRL_METADATA_KEY_GENRE)) {
+		rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_GENRE, FALSE);
+		source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
+							   GRL_METADATA_KEY_GENRE);
+	}
+	if (g_list_find ((GList *)source_keys, GRL_METADATA_KEY_ARTIST)) {
+		rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_ARTIST, FALSE);
+		source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
+							   GRL_METADATA_KEY_ARTIST);
+	}
+	if (g_list_find ((GList *)source_keys, GRL_METADATA_KEY_ALBUM)) {
+		rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_ALBUM, FALSE);
+		source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
+							   GRL_METADATA_KEY_ALBUM);
+	}
+	/*
+	if (g_list_find ((GList *)source_keys, GRL_METADATA_KEY_DATE)) {
+		rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_YEAR, FALSE);
+		source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
+							   GRL_METADATA_KEY_DATE);
+	}
+	*/
+	if (g_list_find ((GList *)source_keys, GRL_METADATA_KEY_DURATION)) {
+		rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_DURATION, FALSE);
+		source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
+							   GRL_METADATA_KEY_DURATION);
+	}
+
+	source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys, GRL_METADATA_KEY_CHILDCOUNT);
+	source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys, GRL_METADATA_KEY_URL);
+
+	/* probably add an image column too? */
+	source->priv->browser_model = gtk_tree_store_new (4, GRL_TYPE_MEDIA, G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT);
+	source->priv->browser_view = gtk_tree_view_new ();
+	gtk_tree_view_set_model (GTK_TREE_VIEW (source->priv->browser_view), GTK_TREE_MODEL (source->priv->browser_model));
+
+	column = gtk_tree_view_column_new ();
+	renderer = gtk_cell_renderer_text_new ();
+	gtk_tree_view_column_set_title (column, _("Browse"));
+	gtk_tree_view_column_pack_start (column, renderer, FALSE);
+	gtk_tree_view_column_add_attribute (column, renderer, "text", 1);
+	gtk_tree_view_column_set_expand (column, TRUE);
+	gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
+
+	gtk_tree_view_append_column (GTK_TREE_VIEW (source->priv->browser_view), column);
+	gtk_tree_view_set_show_expanders (GTK_TREE_VIEW (source->priv->browser_view), TRUE);
+	gtk_tree_view_set_expander_column (GTK_TREE_VIEW (source->priv->browser_view), column);
+	gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (source->priv->browser_view), TRUE);
+	gtk_tree_view_set_fixed_height_mode (GTK_TREE_VIEW (source->priv->browser_view), TRUE);
+
+	g_signal_connect (source->priv->browser_view, "row-expanded", G_CALLBACK (browser_row_expanded_cb), source);
+	selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (source->priv->browser_view));
+	gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE);	/* should be multiple eventually */
+	g_signal_connect (selection, "changed", G_CALLBACK (browser_selection_changed_cb), source);
+
+	scrolled = gtk_scrolled_window_new (NULL, NULL);
+	gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled), GTK_SHADOW_IN);
+	adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (scrolled));
+	g_signal_connect (adjustment, "changed", G_CALLBACK (scroll_adjust_changed_cb), source);
+	g_signal_connect (adjustment, "value-changed", G_CALLBACK (scroll_adjust_value_changed_cb), source);
+	gtk_container_add (GTK_CONTAINER (scrolled), source->priv->browser_view);
+
+	mainbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+	gtk_box_pack_start (GTK_BOX (source), mainbox, TRUE, TRUE, 0);
+
+	/* search bar (if the source supports searching) */
+	if (grl_metadata_source_supported_operations (GRL_METADATA_SOURCE (source->priv->grilo_source)) & GRL_OP_SEARCH) {
+		GtkWidget *align;
+
+		align = gtk_alignment_new (1.0, 0.5, 0.5, 1.0);
+		source->priv->search_entry = rb_search_entry_new ();
+		g_object_set (source->priv->search_entry, "explicit-mode", TRUE, NULL);
+		g_signal_connect (source->priv->search_entry, "search", G_CALLBACK (search_cb), source);
+		g_signal_connect (source->priv->search_entry, "activate", G_CALLBACK (search_cb), source);
+		gtk_container_add (GTK_CONTAINER (align), GTK_WIDGET (source->priv->search_entry));
+		gtk_box_pack_start (GTK_BOX (mainbox), align, FALSE, FALSE, 0);
+	}
+
+	/* info bar */
+	source->priv->info_bar_label = gtk_label_new ("");
+	source->priv->info_bar = gtk_info_bar_new ();
+	gtk_info_bar_set_message_type (GTK_INFO_BAR (source->priv->info_bar), GTK_MESSAGE_INFO);
+	gtk_info_bar_add_button (GTK_INFO_BAR (source->priv->info_bar), _("Fetch more tracks"), GTK_RESPONSE_OK);
+	gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (GTK_INFO_BAR (source->priv->info_bar))),
+			   source->priv->info_bar_label);
+	gtk_widget_show (GTK_WIDGET (source->priv->info_bar_label));
+	gtk_widget_set_no_show_all (GTK_WIDGET (source->priv->info_bar), TRUE);
+	g_signal_connect (source->priv->info_bar, "response", G_CALLBACK (fetch_more_cb), source);
+
+	/* don't allow the browser to be hidden? */
+	source->priv->paned = gtk_hpaned_new ();
+	rb_source_bind_settings (RB_SOURCE (source), GTK_WIDGET (source->priv->entry_view), source->priv->paned, NULL);
+	gtk_paned_pack1 (GTK_PANED (source->priv->paned), scrolled, FALSE, FALSE);
+
+	vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+	gtk_box_pack_start (GTK_BOX (vbox), GTK_WIDGET (source->priv->entry_view), TRUE, TRUE, 0);
+	gtk_box_pack_start (GTK_BOX (vbox), source->priv->info_bar, FALSE, FALSE, 0);
+	gtk_paned_pack2 (GTK_PANED (source->priv->paned), vbox, TRUE, FALSE);
+
+	gtk_box_pack_start (GTK_BOX (mainbox), source->priv->paned, TRUE, TRUE, 0);
+
+	gtk_widget_show_all (GTK_WIDGET (source));
+}
+
+RBSource *
+rb_grilo_source_new (GObject *plugin, GrlMediaSource *grilo_source)
+{
+	GObject *source;
+	RBShell *shell;
+	GSettings *settings;
+	RhythmDBEntryType *entry_type;
+	RhythmDB *db;
+	char *name;
+
+	name = g_strdup_printf ("grilo:%s", grl_media_plugin_get_id (GRL_MEDIA_PLUGIN (grilo_source)));
+
+	g_object_get (plugin, "object", &shell, NULL);
+	g_object_get (shell, "db", &db, NULL);
+	entry_type = g_object_new (rb_grilo_entry_type_get_type (),
+				   "db", db,
+				   "name", name,
+				   "save-to-disk", FALSE,
+				   "category", RHYTHMDB_ENTRY_NORMAL,
+				   "type-data-size", sizeof(RBGriloEntryData),
+				   NULL);
+	rhythmdb_register_entry_type (db, entry_type);
+	g_object_unref (db);
+	g_free (name);
+
+	settings = g_settings_new ("org.gnome.rhythmbox.plugins.grilo");
+	source = g_object_new (RB_TYPE_GRILO_SOURCE,
+			       "name", grl_metadata_source_get_name (GRL_METADATA_SOURCE (grilo_source)),
+			       "entry-type", entry_type,
+			       "shell", shell,
+			       "plugin", plugin,
+			       "show-browser", FALSE,
+			       "settings", g_settings_get_child (settings, "source"),
+			       "grilo-source", grilo_source,
+			       NULL);
+	g_object_unref (settings);
+
+	rb_shell_register_entry_type_for_source (shell, RB_SOURCE (source), entry_type);
+
+	g_object_unref (shell);
+	return RB_SOURCE (source);
+}
+
+static void
+impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+	RBGriloSource *source = RB_GRILO_SOURCE (object);
+	switch (prop_id) {
+	case PROP_GRILO_SOURCE:
+		source->priv->grilo_source = g_value_get_object (value);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+	RBGriloSource *source = RB_GRILO_SOURCE (object);
+
+	switch (prop_id) {
+	case PROP_GRILO_SOURCE:
+		g_value_set_object (value, source->priv->grilo_source);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+impl_delete_thyself (RBDisplayPage *page)
+{
+	RBGriloSource *source = RB_GRILO_SOURCE (page);
+	RhythmDBEntryType *entry_type;
+
+	if (source->priv->browse_op != 0) {
+		grl_operation_cancel (source->priv->browse_op);
+		source->priv->browse_op = 0;
+	}
+
+	if (source->priv->media_browse_op != 0) {
+		grl_operation_cancel (source->priv->media_browse_op);
+		source->priv->media_browse_op = 0;
+	}
+
+	g_object_get (source, "entry-type", &entry_type, NULL);
+	rhythmdb_entry_delete_by_type (source->priv->db, entry_type);
+	g_object_unref (entry_type);
+
+	rhythmdb_commit (source->priv->db);
+}
+
+void
+_rb_grilo_source_register_type (GTypeModule *module)
+{
+	rb_grilo_source_register_type (module);
+	rb_grilo_entry_type_register_type (module);
+}
+
+/* grilo media -> rhythmdb entry */
+
+static void
+set_string_prop_from_key (RhythmDB *db, RhythmDBEntry *entry, RhythmDBPropType prop, GrlData *data, GrlKeyID key)
+{
+	GValue v = {0,};
+	if (grl_data_has_key (data, key) == FALSE)
+		return;
+
+	g_value_init (&v, G_TYPE_STRING);
+	g_value_set_string (&v, grl_data_get_string (data, key));
+	rhythmdb_entry_set (db, entry, prop, &v);
+	g_value_unset (&v);
+}
+
+static RhythmDBEntry *
+create_entry_for_media (RhythmDB *db, RhythmDBEntryType *entry_type, GrlData *data, GrlData *container)
+{
+	RhythmDBEntry *entry;
+	RBGriloEntryData *entry_data;
+
+	entry = rhythmdb_entry_lookup_by_location (db, grl_media_get_url (GRL_MEDIA (data)));
+	if (entry != NULL) {
+		return entry;
+	}
+
+	rb_debug ("creating entry for %s / %s", grl_media_get_url (GRL_MEDIA (data)), grl_media_get_id (GRL_MEDIA (data)));
+
+	entry = rhythmdb_entry_new (db, entry_type, grl_media_get_url (GRL_MEDIA (data)));	/* just use the url? */
+	if (entry == NULL) {
+		/* crap. */
+		return NULL;
+	}
+
+	set_string_prop_from_key (db, entry, RHYTHMDB_PROP_TITLE, data, GRL_METADATA_KEY_TITLE);
+	set_string_prop_from_key (db, entry, RHYTHMDB_PROP_ALBUM, data, GRL_METADATA_KEY_ALBUM);
+	set_string_prop_from_key (db, entry, RHYTHMDB_PROP_ARTIST, data, GRL_METADATA_KEY_ARTIST);
+	set_string_prop_from_key (db, entry, RHYTHMDB_PROP_GENRE, data, GRL_METADATA_KEY_GENRE);
+	set_string_prop_from_key (db, entry, RHYTHMDB_PROP_TITLE, data, GRL_METADATA_KEY_TITLE);
+
+	if (grl_data_has_key (data, GRL_METADATA_KEY_DATE)) {
+		/* something - grilo has this as a string? */
+	}
+
+	if (grl_data_has_key (data, GRL_METADATA_KEY_BITRATE)) {
+		GValue v = {0,};
+		g_value_init (&v, G_TYPE_ULONG);
+		g_value_set_ulong (&v, grl_data_get_int (data, GRL_METADATA_KEY_BITRATE));
+		rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_BITRATE, &v);
+		g_value_unset (&v);
+	}
+
+	if (grl_data_has_key (data, GRL_METADATA_KEY_DURATION)) {
+		/* this is probably in seconds */
+		GValue v = {0,};
+		g_value_init (&v, G_TYPE_ULONG);
+		g_value_set_ulong (&v, grl_data_get_int (data, GRL_METADATA_KEY_DURATION));
+		rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_DURATION, &v);
+		g_value_unset (&v);
+	}
+
+	if (grl_data_has_key (data, GRL_METADATA_KEY_MIME)) {
+		const char *media_type;
+		media_type = rb_gst_mime_type_to_media_type (grl_data_get_string (data, GRL_METADATA_KEY_MIME));
+		if (media_type) {
+			GValue v = {0,};
+			g_value_init (&v, G_TYPE_STRING);
+			g_value_set_string (&v, media_type);
+			rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_MEDIA_TYPE, &v);
+			g_value_unset (&v);
+		}
+	}
+
+	/* rating and play count? */
+
+
+	entry_data = RHYTHMDB_ENTRY_GET_TYPE_DATA (entry, RBGriloEntryData);
+	entry_data->grilo_data = g_object_ref (data);
+	if (container != NULL) {
+		entry_data->grilo_container = g_object_ref (container);
+	}
+
+	/* might want to consider batching this */
+	rhythmdb_commit (db);
+
+	return entry;
+}
+
+/* container browsing */
+
+static void browse_next (RBGriloSource *source);
+
+static void
+delete_marker_row (RBGriloSource *source, GtkTreeIter *iter)
+{
+	GtkTreeIter marker_iter;
+	if (gtk_tree_model_iter_children (GTK_TREE_MODEL (source->priv->browser_model), &marker_iter, iter)) {
+		do {
+			GrlMedia *container;
+			gtk_tree_model_get (GTK_TREE_MODEL (source->priv->browser_model), &marker_iter,
+					    0, &container,
+					    -1);
+			if (container == NULL) {
+				gtk_tree_store_remove (GTK_TREE_STORE (source->priv->browser_model), &marker_iter);
+				break;
+			}
+		} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (source->priv->browser_model), &marker_iter));
+	}
+}
+
+static void
+set_container_type (RBGriloSource *source, GtkTreeIter *iter, gboolean has_media)
+{
+	int container_type;
+
+	gtk_tree_model_get (GTK_TREE_MODEL (source->priv->browser_model),
+			    iter,
+			    2, &container_type,
+			    -1);
+	if (container_type == CONTAINER_UNKNOWN_MEDIA) {
+		container_type = has_media ? CONTAINER_HAS_MEDIA : CONTAINER_NO_MEDIA;
+	}
+
+	gtk_tree_store_set (source->priv->browser_model,
+			    iter,
+			    2, container_type,
+			    -1);
+}
+
+static void
+grilo_browse_cb (GrlMediaSource *grilo_source, guint operation_id, GrlMedia *media, guint remaining, RBGriloSource *source, const GError *error)
+{
+	if (operation_id != source->priv->browse_op) {
+		return;
+	}
+
+	if (error != NULL) {
+		/* do something? */
+		rb_debug ("got error for %s: %s", grl_metadata_source_get_name (GRL_METADATA_SOURCE (grilo_source)), error->message);
+		return;
+	}
+
+	if (media != NULL) {
+		source->priv->browse_got_results = TRUE;
+		source->priv->browse_position++;
+	}
+
+	if (media && GRL_IS_MEDIA_BOX (media)) {
+
+		GtkTreeIter new_row;
+		if (source->priv->browse_container == NULL) {
+			/* insert at the end */
+			gtk_tree_store_insert_with_values (source->priv->browser_model,
+							   &new_row,
+							   NULL,
+							   -1,
+							   0, g_object_ref (media),
+							   1, grl_media_get_title (media),
+							   2, CONTAINER_UNKNOWN_MEDIA,
+							   3, 0,
+							   -1);
+		} else {
+			int n;
+			/* insert before the expand marker row */
+			n = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (source->priv->browser_model),
+							    &source->priv->browse_container_iter);
+			gtk_tree_store_insert_with_values (source->priv->browser_model,
+							   &new_row,
+							   &source->priv->browse_container_iter,
+							   n - 1,
+							   0, g_object_ref (media),
+							   1, grl_media_get_title (media),
+							   2, CONTAINER_UNKNOWN_MEDIA,
+							   3, 0,
+							   -1);
+		}
+
+		/* and insert an expand marker below it too */
+		gtk_tree_store_insert_with_values (source->priv->browser_model,
+						   NULL,
+						   &new_row,
+						   -1,
+						   0, NULL,
+						   1, "...",	/* needs to be translatable? */
+						   2, CONTAINER_NO_MEDIA,
+						   3, 0,
+						   -1);
+	} else if (media && GRL_IS_MEDIA_AUDIO (media)) {
+		source->priv->browse_got_media = TRUE;
+	}
+
+	if (remaining == 0) {
+		source->priv->browse_op = 0;
+		if (source->priv->browse_got_results == FALSE &&
+		    source->priv->browse_container != NULL) {
+			/* no more results for this container, so delete the marker row */
+			delete_marker_row (source, &source->priv->browse_container_iter);
+
+			set_container_type (source, &source->priv->browse_container_iter, source->priv->browse_got_media);
+			gtk_tree_store_set (source->priv->browser_model,
+					    &source->priv->browse_container_iter,
+					    3, -1,
+					    -1);
+		} else if (source->priv->browse_container != NULL) {
+			if (source->priv->browse_position >= CONTAINER_GIVE_UP_POINT &&
+			    gtk_tree_model_iter_n_children (GTK_TREE_MODEL (source->priv->browser_model),
+							    &source->priv->browse_container_iter) == 1) {
+				/* no containers yet, so remove the marker row */
+				delete_marker_row (source, &source->priv->browse_container_iter);
+			} else {
+				/* store browse position for next time we want more */
+				gtk_tree_store_set (source->priv->browser_model,
+						    &source->priv->browse_container_iter,
+						    3, source->priv->browse_position,
+						    -1);
+				maybe_expand_container (source);
+			}
+
+		} else if (source->priv->browse_got_results && source->priv->browse_container == NULL) {
+			/* get all top-level containers */
+			browse_next (source);
+		}
+	}
+}
+
+static void
+browse_next (RBGriloSource *source)
+{
+	rb_debug ("next browse op for %s (%d)",
+		  grl_metadata_source_get_name (GRL_METADATA_SOURCE (source->priv->grilo_source)),
+		  source->priv->browse_position);
+	source->priv->browse_got_results = FALSE;
+	source->priv->browse_op = grl_media_source_browse (source->priv->grilo_source,
+							   source->priv->browse_container,
+							   source->priv->grilo_keys,
+							   source->priv->browse_position,
+							   CONTAINER_FETCH_SIZE,
+							   GRL_RESOLVE_NORMAL,
+							   (GrlMediaSourceResultCb) grilo_browse_cb,
+							   source);
+}
+
+static void
+start_browse (RBGriloSource *source, GrlMedia *container, GtkTreeIter *container_iter, int position)
+{
+	rb_debug ("starting browse op for %s", grl_metadata_source_get_name (GRL_METADATA_SOURCE (source->priv->grilo_source)));
+
+	/* cancel existing operation? */
+	if (source->priv->browse_op != 0) {
+		grl_operation_cancel (source->priv->browse_op);
+	}
+
+	if (source->priv->browse_container != NULL) {
+		g_object_unref (source->priv->browse_container);
+	}
+	source->priv->browse_container = container;
+	if (container_iter != NULL) {
+		/* hrm, probably have to use row references here.. */
+		source->priv->browse_container_iter = *container_iter;
+	}
+	source->priv->browse_position = position;
+	source->priv->browse_got_media = FALSE;
+
+	browse_next (source);
+}
+
+/* media browsing */
+
+static void media_browse_next (RBGriloSource *source);
+
+static void
+grilo_media_browse_cb (GrlMediaSource *grilo_source, guint operation_id, GrlMedia *media, guint remaining, RBGriloSource *source, const GError *error)
+{
+	if (operation_id != source->priv->media_browse_op) {
+		return;
+	}
+
+	if (error != NULL) {
+		/* do something? */
+		rb_debug ("got error for %s: %s",
+			  grl_metadata_source_get_name (GRL_METADATA_SOURCE (grilo_source)),
+			  error->message);
+		return;
+	}
+
+	GDK_THREADS_ENTER ();
+	if (media != NULL) {
+		source->priv->media_browse_got_results = TRUE;
+		source->priv->media_browse_position++;
+
+		if (GRL_IS_MEDIA_AUDIO (media)) {
+			RhythmDBEntry *entry;
+			entry = create_entry_for_media (source->priv->db,
+							source->priv->entry_type,
+							GRL_DATA (media),
+							GRL_DATA (source->priv->browse_container));
+			if (entry != NULL) {
+				rhythmdb_query_model_add_entry (source->priv->query_model, entry, -1);
+			}
+		} else if (GRL_IS_MEDIA_BOX (media)) {
+			source->priv->media_browse_got_containers = TRUE;
+		}
+	}
+
+	if (remaining == 0) {
+		source->priv->media_browse_op = 0;
+
+		if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (source->priv->query_model), NULL) == 0 &&
+		    source->priv->media_browse_position >= CONTAINER_GIVE_UP_POINT) {
+			/* if we don't find any media within the first 100 or so results,
+			 * assume this container doesn't have any.  otherwise we'd load
+			 * the entire thing when it got selected, which would suck.
+			 */
+			rb_debug ("didn't find any media in %s, giving up", grl_media_get_title (source->priv->media_browse_container));
+			gtk_tree_store_set (source->priv->browser_model,
+					    &source->priv->media_browse_container_iter,
+					    2, CONTAINER_NO_MEDIA,
+					    -1);
+		} else if (source->priv->media_browse_got_results) {
+			if (source->priv->media_browse_position < source->priv->media_browse_limit) {
+				media_browse_next (source);
+			} else {
+				char *text;
+
+				text = g_strdup_printf (_("Only showing %d results"),
+							source->priv->media_browse_position);
+				gtk_label_set_text (GTK_LABEL (source->priv->info_bar_label), text);
+				g_free (text);
+
+				gtk_widget_show (source->priv->info_bar);
+			}
+		} else if (source->priv->media_browse_got_containers == FALSE &&
+			   source->priv->media_browse_container != NULL) {
+			/* make sure there's no marker row for this container */
+			delete_marker_row (source, &source->priv->media_browse_container_iter);
+		}
+	}
+	GDK_THREADS_LEAVE ();
+}
+
+static void
+media_browse_next (RBGriloSource *source)
+{
+	rb_debug ("next media_browse op for %s (%d)",
+		  grl_metadata_source_get_name (GRL_METADATA_SOURCE (source->priv->grilo_source)),
+		  source->priv->media_browse_position);
+
+	source->priv->media_browse_got_results = FALSE;
+	if (source->priv->media_browse_container != NULL) {
+		source->priv->media_browse_op =
+			grl_media_source_browse (source->priv->grilo_source,
+						 source->priv->media_browse_container,
+						 source->priv->grilo_keys,
+						 source->priv->media_browse_position,
+						 CONTAINER_FETCH_SIZE,
+						 GRL_RESOLVE_NORMAL,
+						 (GrlMediaSourceResultCb) grilo_media_browse_cb,
+						 source);
+	} else {
+		source->priv->media_browse_op =
+			grl_media_source_search (source->priv->grilo_source,
+						 source->priv->search_text,
+						 source->priv->grilo_keys,
+						 source->priv->media_browse_position,
+						 CONTAINER_FETCH_SIZE,
+						 GRL_RESOLVE_NORMAL,
+						 (GrlMediaSourceResultCb) grilo_media_browse_cb,
+						 source);
+	}
+}
+
+static void
+start_media_browse (RBGriloSource *source, GrlMedia *container, GtkTreeIter *container_iter, guint limit)
+{
+	rb_debug ("starting media browse for %s",
+		  grl_metadata_source_get_name (GRL_METADATA_SOURCE (source->priv->grilo_source)));
+
+	/* cancel existing operation? */
+	if (source->priv->media_browse_op != 0) {
+		grl_operation_cancel (source->priv->media_browse_op);
+	}
+
+	if (source->priv->media_browse_container != NULL) {
+		g_object_unref (source->priv->media_browse_container);
+	}
+	source->priv->media_browse_container = container;
+	if (container_iter != NULL) {
+		/* hrm, probably have to use row references here.. */
+		source->priv->media_browse_container_iter = *container_iter;
+	}
+	source->priv->media_browse_position = 0;
+	source->priv->media_browse_limit = limit;
+	source->priv->media_browse_got_containers = FALSE;
+
+	if (source->priv->query_model != NULL) {
+		g_object_unref (source->priv->query_model);
+	}
+	source->priv->query_model = rhythmdb_query_model_new_empty (source->priv->db);
+	rb_entry_view_set_model (RB_ENTRY_VIEW (source->priv->entry_view), source->priv->query_model);
+	g_object_set (source, "query-model", source->priv->query_model, NULL);
+
+	media_browse_next (source);
+}
+
+static void
+fetch_more_cb (GtkInfoBar *bar, gint response, RBGriloSource *source)
+{
+	if (response != GTK_RESPONSE_OK) {
+		return;
+	}
+
+	gtk_widget_hide (GTK_WIDGET (bar));
+	source->priv->media_browse_limit += CONTAINER_MAX_TRACKS;
+	media_browse_next (source);
+}
+
+static gboolean
+expand_from_marker (RBGriloSource *source, GtkTreeIter *iter)
+{
+	/* this is a marker row, fetch more containers underneath the parent */
+	GrlMedia *container;
+	GtkTreeIter browse;
+	int position;
+	gtk_tree_model_iter_parent (GTK_TREE_MODEL (source->priv->browser_model), &browse, iter);
+	gtk_tree_model_get (GTK_TREE_MODEL (source->priv->browser_model),
+			    &browse,
+			    0, &container,
+			    3, &position,
+			    -1);
+	if (position >= 0) {
+		start_browse (source, container, &browse, position);
+		return TRUE;
+	}
+
+	return FALSE;
+}
+
+static void
+browser_selection_changed_cb (GtkTreeSelection *selection, RBGriloSource *source)
+{
+	GtkTreeIter iter;
+	GrlMedia *container;
+	int container_type;
+
+	gtk_widget_hide (GTK_WIDGET (source->priv->info_bar));
+	if (gtk_tree_selection_get_selected (selection, NULL, &iter) == FALSE) {
+		rb_debug ("nothing selected");
+		return;
+	}
+
+	rb_search_entry_clear (source->priv->search_entry);
+
+	gtk_tree_model_get (GTK_TREE_MODEL (source->priv->browser_model), &iter,
+			    0, &container,
+			    2, &container_type,
+			    -1);
+
+	if (container == NULL) {
+		expand_from_marker (source, &iter);
+	} else if (container_type != CONTAINER_NO_MEDIA) {
+		/* fetch media directly under this container */
+		start_media_browse (source, container, &iter, CONTAINER_MAX_TRACKS);
+	} else {
+		/* clear the track list? */
+	}
+}
+
+static gboolean
+maybe_expand_container (RBGriloSource *source)
+{
+	GtkTreePath *path;
+	GtkTreePath *end;
+	GtkTreeIter iter;
+	GtkTreeIter end_iter;
+	GtkTreeIter next;
+	GrlMedia *container;
+	gboolean last;
+
+	source->priv->maybe_expand_idle = 0;
+
+	if (source->priv->browse_op != 0) {
+		rb_debug ("not expanding, already browsing");
+		return FALSE;
+	}
+
+	/* if we find a visible marker row, find more results */
+	if (gtk_tree_view_get_visible_range (GTK_TREE_VIEW (source->priv->browser_view), &path, &end) == FALSE) {
+		rb_debug ("not expanding, nothing to expand");
+		return FALSE;
+	}
+
+	gtk_tree_model_get_iter (GTK_TREE_MODEL (source->priv->browser_model), &iter, path);
+	gtk_tree_model_get_iter (GTK_TREE_MODEL (source->priv->browser_model), &end_iter, end);
+
+	do {
+		gtk_tree_path_free (path);
+		path = gtk_tree_model_get_path (GTK_TREE_MODEL (source->priv->browser_model), &iter);
+		last = (gtk_tree_path_compare (path, end) >= 0);
+		gtk_tree_model_get (GTK_TREE_MODEL (source->priv->browser_model), &iter,
+				    0, &container,
+				    -1);
+		if (container == NULL) {
+			if (expand_from_marker (source, &iter)) {
+				rb_debug ("expanding");
+				break;
+			}
+		}
+
+		next = iter;
+		if (gtk_tree_view_row_expanded (GTK_TREE_VIEW (source->priv->browser_view), path) &&
+		    gtk_tree_model_iter_has_child (GTK_TREE_MODEL (source->priv->browser_model), &iter)) {
+			gtk_tree_model_iter_children (GTK_TREE_MODEL (source->priv->browser_model), &iter, &next);
+		} else if (gtk_tree_model_iter_next (GTK_TREE_MODEL (source->priv->browser_model), &next)) {
+			iter = next;
+		} else {
+			if (gtk_tree_model_iter_parent (GTK_TREE_MODEL (source->priv->browser_model), &next, &iter) == FALSE) {
+				break;
+			}
+			iter = next;
+			if (gtk_tree_model_iter_next (GTK_TREE_MODEL (source->priv->browser_model), &iter) == FALSE) {
+				break;
+			}
+		}
+	} while (last == FALSE);
+
+	gtk_tree_path_free (path);
+	gtk_tree_path_free (end);
+	return FALSE;
+}
+
+static void
+maybe_expand_container_idle (RBGriloSource *source)
+{
+	if (source->priv->maybe_expand_idle == 0) {
+		source->priv->maybe_expand_idle = g_idle_add ((GSourceFunc)maybe_expand_container, source);
+	}
+}
+
+static void
+browser_row_expanded_cb (GtkTreeView *tree_view, GtkTreeIter *iter, GtkTreePath *path, RBGriloSource *source)
+{
+	maybe_expand_container_idle (source);
+}
+
+static void
+scroll_adjust_changed_cb (GtkAdjustment *adjustment, RBGriloSource *source)
+{
+	maybe_expand_container_idle (source);
+}
+
+static void
+scroll_adjust_value_changed_cb (GtkAdjustment *adjustment, RBGriloSource *source)
+{
+	maybe_expand_container_idle (source);
+}
+
+static void
+impl_selected (RBDisplayPage *page)
+{
+	RBGriloSource *source = RB_GRILO_SOURCE (page);
+	if (source->priv->done_initial_browse == FALSE) {
+		source->priv->done_initial_browse = TRUE;
+		start_browse (source, NULL, NULL, 0);
+	}
+}
+
+static RBEntryView *
+impl_get_entry_view (RBSource *bsource)
+{
+	RBGriloSource *source = RB_GRILO_SOURCE (bsource);
+	return source->priv->entry_view;
+}
+
+static void
+search_cb (RBSearchEntry *search, const char *text, RBGriloSource *source)
+{
+	g_free (source->priv->search_text);
+	source->priv->search_text = g_strdup (text);
+
+	gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (GTK_TREE_VIEW (source->priv->browser_view)));
+
+	start_media_browse (source, NULL, NULL, CONTAINER_MAX_TRACKS);
+}
+
+static void
+notify_sort_order_cb (GObject *object, GParamSpec *pspec, RBGriloSource *source)
+{
+	rb_entry_view_resort_model (RB_ENTRY_VIEW (object));
+}
diff --git a/plugins/grilo/rb-grilo-source.h b/plugins/grilo/rb-grilo-source.h
new file mode 100644
index 0000000..78ddf07
--- /dev/null
+++ b/plugins/grilo/rb-grilo-source.h
@@ -0,0 +1,65 @@
+/*
+ *  Copyright (C) 2011 Jonathan Matthew
+ *
+ *  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_GRILO_SOURCE_H
+#define __RB_GRILO_SOURCE_H
+
+#include <grilo.h>
+
+#include "rhythmdb.h"
+#include "rb-source.h"
+
+G_BEGIN_DECLS
+
+#define RB_TYPE_GRILO_SOURCE         (rb_grilo_source_get_type ())
+#define RB_GRILO_SOURCE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_GRILO_SOURCE, RBGriloSource))
+#define RB_GRILO_SOURCE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_GRILO_SOURCE, RBGriloSourceClass))
+#define RB_IS_GRILO_SOURCE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_GRILO_SOURCE))
+#define RB_IS_GRILO_SOURCE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_GRILO_SOURCE))
+#define RB_GRILO_SOURCE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_GRILO_SOURCE, RBGriloSourceClass))
+
+typedef struct _RBGriloSourcePrivate RBGriloSourcePrivate;
+
+typedef struct
+{
+	RBSource parent;
+	RBGriloSourcePrivate *priv;
+} RBGriloSource;
+
+typedef struct
+{
+	RBSourceClass parent;
+} RBGriloSourceClass;
+
+RBSource *		rb_grilo_source_new	(GObject *plugin,
+						 GrlMediaSource *grilo_source);
+GType			rb_grilo_source_get_type (void);
+void			_rb_grilo_source_register_type	(GTypeModule *module);
+
+G_END_DECLS
+
+#endif /* __RB_GRILO_SOURCE_H */
diff --git a/po/POTFILES.in b/po/POTFILES.in
index ba7a9f4..6950795 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -80,6 +80,9 @@ plugins/generic-player/rb-generic-player-plugin.c
 plugins/generic-player/rb-generic-player-source.c
 plugins/generic-player/rb-nokia770-source.c
 plugins/generic-player/rb-psp-source.c
+[type: gettext/ini]plugins/grilo/grilo.plugin.in
+plugins/grilo/rb-grilo-plugin.c
+plugins/grilo/rb-grilo-source.c
 [type: gettext/ini]plugins/im-status/im-status.plugin.in
 [type: gettext/ini]plugins/ipod/ipod.plugin.in
 [type: gettext/glade]plugins/ipod/ipod-info.ui



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