[rhythmbox] mtp: huge rewrite to do all device operations in a separate thread



commit c5c82a696448d53b16be94d774f5628f73371ddb
Author: Jonathan Matthew <jonathan d14n org>
Date:   Sat Oct 10 15:16:47 2009 +1000

    mtp: huge rewrite to do all device operations in a separate thread
    
    This moves all MTP device operations to a separate thread, including
    uploads and downloads, so the main UI thread never gets blocked.
    
    Noteworthy things: the source object is now created based on a raw
    device, so it has to open the device.  The source destroys itself if the
    device cannot be opened.
    
    There is now a GStreamer MTP sink element that writes data to a
    temporary file, then uploads the file when it receives an EOS event.
    
    Fixes bug #534981.

 plugins/mtpdevice/Makefile.am       |    5 +-
 plugins/mtpdevice/rb-mtp-gst-sink.c |  475 ++++++++++++++++++++
 plugins/mtpdevice/rb-mtp-gst-src.c  |  174 ++++----
 plugins/mtpdevice/rb-mtp-plugin.c   |  246 +++++------
 plugins/mtpdevice/rb-mtp-source.c   |  776 +++++++++++++++------------------
 plugins/mtpdevice/rb-mtp-source.h   |    4 +-
 plugins/mtpdevice/rb-mtp-thread.c   |  809 +++++++++++++++++++++++++++++++++++
 plugins/mtpdevice/rb-mtp-thread.h   |  125 ++++++
 po/POTFILES.in                      |    1 +
 9 files changed, 1964 insertions(+), 651 deletions(-)
---
diff --git a/plugins/mtpdevice/Makefile.am b/plugins/mtpdevice/Makefile.am
index 71175aa..151d231 100644
--- a/plugins/mtpdevice/Makefile.am
+++ b/plugins/mtpdevice/Makefile.am
@@ -6,8 +6,11 @@ plugin_LTLIBRARIES = libmtpdevice.la
 libmtpdevice_la_SOURCES = \
 	rb-mtp-plugin.c	\
 	rb-mtp-gst-src.c \
+	rb-mtp-gst-sink.c \
 	rb-mtp-source.c	\
-	rb-mtp-source.h
+	rb-mtp-source.h \
+	rb-mtp-thread.c \
+	rb-mtp-thread.h
 	
 libmtpdevice_la_LDFLAGS = $(PLUGIN_LIBTOOL_FLAGS)
 libmtpdevice_la_LIBTOOLFLAGS = --tag=disable-static
diff --git a/plugins/mtpdevice/rb-mtp-gst-sink.c b/plugins/mtpdevice/rb-mtp-gst-sink.c
new file mode 100644
index 0000000..e710e20
--- /dev/null
+++ b/plugins/mtpdevice/rb-mtp-gst-sink.c
@@ -0,0 +1,475 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2009  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 <glib/gi18n.h>
+#include <libmtp.h>
+#include <gst/gst.h>
+
+#include "rb-mtp-thread.h"
+#include "rb-debug.h"
+#include "rb-file-helpers.h"
+
+#define RB_TYPE_MTP_SINK (rb_mtp_sink_get_type())
+#define RB_MTP_SINK(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),RB_TYPE_MTP_SINK,RBMTPSink))
+#define RB_MTP_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),RB_TYPE_MTP_SINK,RBMTPSinkClass))
+#define RB_IS_MTP_SINK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),RB_TYPE_MTP_SINK))
+#define RB_IS_MTP_SINK_CLASS(obj) (G_TYPE_CHECK_CLASS_TYPE((klass),RB_TYPE_MTP_SINK))
+
+typedef struct _RBMTPSink RBMTPSink;
+typedef struct _RBMTPSinkClass RBMTPSinkClass;
+
+struct _RBMTPSink
+{
+	GstBin parent;
+
+	RBMtpThread *device_thread;
+
+	LIBMTP_track_t *track;
+	char *tempfile;
+
+	GstElement *fdsink;
+	GstPad *ghostpad;
+
+	GError *upload_error;
+	GMutex *upload_mutex;
+	GCond *upload_cond;
+	gboolean upload_done;
+};
+
+struct _RBMTPSinkClass
+{
+	GstBinClass parent_class;
+};
+
+enum
+{
+	PROP_0,
+	PROP_URI,
+	PROP_MTP_TRACK,
+	PROP_DEVICE_THREAD
+};
+
+static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
+	GST_PAD_SINK,
+	GST_PAD_ALWAYS,
+	GST_STATIC_CAPS_ANY);
+
+static GstElementDetails rb_mtp_sink_details =
+GST_ELEMENT_DETAILS ("RB MTP Sink",
+	"Sink/File",
+	"Uploads tracks to MTP devices",
+	"Jonathan Matthew <jonathan d14n org>");
+
+static void rb_mtp_sink_uri_handler_init (gpointer g_iface, gpointer iface_data);
+
+static void
+_do_init (GType mtp_sink_type)
+{
+	static const GInterfaceInfo urihandler_info = {
+		rb_mtp_sink_uri_handler_init,
+		NULL,
+		NULL
+	};
+
+	g_type_add_interface_static (mtp_sink_type, GST_TYPE_URI_HANDLER,
+			&urihandler_info);
+}
+
+GST_BOILERPLATE_FULL (RBMTPSink, rb_mtp_sink, GstBin, GST_TYPE_BIN, _do_init);
+
+static void
+rb_mtp_sink_base_init (gpointer g_class)
+{
+	GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);
+	gst_element_class_add_pad_template (element_class,
+		gst_static_pad_template_get (&sinktemplate));
+	gst_element_class_set_details (element_class, &rb_mtp_sink_details);
+}
+
+static void
+rb_mtp_sink_init (RBMTPSink *sink, RBMTPSinkClass *klass)
+{
+	GstPad *pad;
+
+	sink->upload_mutex = g_mutex_new ();
+	sink->upload_cond = g_cond_new ();
+
+	/* create actual sink */
+	sink->fdsink = gst_element_factory_make ("fdsink", NULL);
+	if (sink->fdsink == NULL) {
+		g_warning ("couldn't create fdsink element");
+		return;
+	}
+
+	gst_bin_add (GST_BIN (sink), sink->fdsink);
+	gst_object_ref (sink->fdsink);
+
+	/* create ghost pad */
+	pad = gst_element_get_pad (sink->fdsink, "sink");
+	sink->ghostpad = gst_ghost_pad_new ("sink", pad);
+	gst_element_add_pad (GST_ELEMENT (sink), sink->ghostpad);
+	gst_object_ref (sink->ghostpad);
+	gst_object_unref (pad);
+
+}
+
+static GstStateChangeReturn
+rb_mtp_sink_open_tempfile (RBMTPSink *sink)
+{
+	int fd;
+	GError *tmperror = NULL;
+
+	fd = g_file_open_tmp ("rb-mtp-temp-XXXXXX", &sink->tempfile, &tmperror);
+	if (fd == -1) {
+		GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE, (_("Unable to open temporary file: %s"), tmperror->message), NULL);
+		return GST_STATE_CHANGE_FAILURE;
+	}
+	rb_debug ("opened temporary file %s", sink->tempfile);
+
+	g_object_set (sink->fdsink, "fd", fd, "sync", FALSE, NULL);
+	return GST_STATE_CHANGE_SUCCESS;
+}
+
+static GstStateChangeReturn
+rb_mtp_sink_close_tempfile (RBMTPSink *sink)
+{
+	if (sink->tempfile != NULL) {
+		rb_debug ("deleting tempfile %s", sink->tempfile);
+		remove (sink->tempfile);
+		g_free (sink->tempfile);
+		sink->tempfile = NULL;
+	}
+
+	return GST_STATE_CHANGE_SUCCESS;
+}
+
+static void
+upload_callback (LIBMTP_track_t *track, GError *error, RBMTPSink *sink)
+{
+	rb_debug ("mtp upload callback for %s: item ID %d", track->filename, track->item_id);
+	g_mutex_lock (sink->upload_mutex);
+
+	if (error != NULL) {
+		sink->upload_error = g_error_copy (error);
+	}
+	sink->upload_done = TRUE;
+
+	g_cond_signal (sink->upload_cond);
+	g_mutex_unlock (sink->upload_mutex);
+}
+
+static void
+rb_mtp_sink_handle_message (GstBin *bin, GstMessage *message)
+{
+	/* when we get an EOS message from the fdsink, close the fd and upload the
+	 * file to the device.
+	 */
+	if (GST_MESSAGE_TYPE (message) == GST_MESSAGE_EOS) {
+		int fd;
+		struct stat stat_buf;
+
+		RBMTPSink *sink = RB_MTP_SINK (bin);
+
+		/* fill in the file size and close the fd */
+		g_object_get (sink->fdsink, "fd", &fd, NULL);
+		fstat (fd, &stat_buf);
+		sink->track->filesize = stat_buf.st_size;
+		close (fd);
+
+		rb_debug ("handling EOS from fdsink; file size is %" G_GUINT64_FORMAT, sink->track->filesize);
+
+		/* start the upload, then wait for it to finish.
+		 * we're on a streaming thread here, so blocking is no problem.
+		 */
+		g_mutex_lock (sink->upload_mutex);
+
+		sink->upload_done = FALSE;
+		rb_mtp_thread_upload_track (sink->device_thread,
+					    sink->track,
+					    sink->tempfile,
+					    (RBMtpUploadCallback) upload_callback,
+					    g_object_ref (sink),
+					    g_object_unref);
+
+		while (sink->upload_done == FALSE) {
+			g_cond_wait (sink->upload_cond, sink->upload_mutex);
+		}
+		g_mutex_unlock (sink->upload_mutex);
+
+		/* post error message if the upload failed - this should get there before
+		 * this EOS message does, so it should work OK.
+		 */
+		if (sink->upload_error != NULL) {
+			int code;
+
+			switch (sink->upload_error->code) {
+			case RB_MTP_THREAD_ERROR_NO_SPACE:
+				code = GST_RESOURCE_ERROR_NO_SPACE_LEFT;
+				break;
+
+			default:
+			case RB_MTP_THREAD_ERROR_SEND_TRACK:
+				code = GST_RESOURCE_ERROR_WRITE;
+				break;
+			}
+
+			GST_WARNING_OBJECT (sink, "error: %s", sink->upload_error->message);
+			gst_element_message_full (GST_ELEMENT (sink),
+						  GST_MESSAGE_ERROR,
+						  GST_RESOURCE_ERROR, code,
+						  sink->upload_error->message, NULL,
+						  __FILE__, GST_FUNCTION, __LINE__);
+		}
+	}
+
+	GST_BIN_CLASS (parent_class)->handle_message (bin, message);
+}
+
+static GstStateChangeReturn
+rb_mtp_sink_change_state (GstElement *element, GstStateChange transition)
+{
+	GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
+	RBMTPSink *sink = RB_MTP_SINK (element);
+
+	switch (transition) {
+		case GST_STATE_CHANGE_NULL_TO_READY:
+			ret = rb_mtp_sink_open_tempfile (sink);
+			if (ret != GST_STATE_CHANGE_SUCCESS)
+				return ret;
+			break;
+
+		case GST_STATE_CHANGE_READY_TO_PAUSED:
+			break;
+
+		case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
+			break;
+
+		default:
+			break;
+	}
+
+	ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
+
+	switch (transition) {
+		case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
+			break;
+		case GST_STATE_CHANGE_PAUSED_TO_READY:
+			break;
+		case GST_STATE_CHANGE_READY_TO_NULL:
+			ret = rb_mtp_sink_close_tempfile (sink);
+			break;
+		default:
+			break;
+	}
+
+	return ret;
+}
+
+static void
+rb_mtp_sink_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+	RBMTPSink *sink = RB_MTP_SINK (object);
+
+	switch (prop_id) {
+	case PROP_MTP_TRACK:
+		sink->track = g_value_get_pointer (value);
+		break;
+	case PROP_DEVICE_THREAD:
+		sink->device_thread = g_value_dup_object (value);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+rb_mtp_sink_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+	RBMTPSink *sink = RB_MTP_SINK (object);
+
+	switch (prop_id) {
+	case PROP_MTP_TRACK:
+		g_value_set_pointer (value, sink->track);
+		break;
+	case PROP_DEVICE_THREAD:
+		g_value_set_object (value, sink->device_thread);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+rb_mtp_sink_dispose (GObject *object)
+{
+	RBMTPSink *sink;
+	sink = RB_MTP_SINK (object);
+
+	if (sink->ghostpad) {
+		gst_object_unref (sink->ghostpad);
+		sink->ghostpad = NULL;
+	}
+
+	if (sink->fdsink) {
+		gst_object_unref (sink->fdsink);
+		sink->fdsink = NULL;
+	}
+
+	if (sink->device_thread) {
+		g_object_unref (sink->device_thread);
+		sink->device_thread = NULL;
+	}
+
+	G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+rb_mtp_sink_finalize (GObject *object)
+{
+	RBMTPSink *sink;
+	sink = RB_MTP_SINK (object);
+
+	g_mutex_free (sink->upload_mutex);
+	g_cond_free (sink->upload_cond);
+
+	/* probably isn't right */
+	if (sink->upload_error) {
+		g_error_free (sink->upload_error);
+	}
+
+	G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+rb_mtp_sink_class_init (RBMTPSinkClass *klass)
+{
+	GObjectClass *gobject_class;
+	GstElementClass *element_class;
+	GstBinClass *bin_class;
+
+	gobject_class = G_OBJECT_CLASS (klass);
+	gobject_class->dispose = rb_mtp_sink_dispose;
+	gobject_class->finalize = rb_mtp_sink_finalize;
+	gobject_class->set_property = rb_mtp_sink_set_property;
+	gobject_class->get_property = rb_mtp_sink_get_property;
+
+	element_class = GST_ELEMENT_CLASS (klass);
+	element_class->change_state = rb_mtp_sink_change_state;
+	
+	bin_class = GST_BIN_CLASS (klass);
+	bin_class->handle_message = rb_mtp_sink_handle_message;
+
+	g_object_class_install_property (gobject_class,
+					 PROP_MTP_TRACK,
+					 g_param_spec_pointer ("mtp-track",
+						 	       "libmtp track",
+							       "libmtp track",
+							       G_PARAM_READWRITE));
+	g_object_class_install_property (gobject_class,
+					 PROP_DEVICE_THREAD,
+					 g_param_spec_object ("device-thread",
+							      "device-thread",
+							      "device handling thread",
+							      RB_TYPE_MTP_THREAD,
+							      G_PARAM_READWRITE));
+}
+
+
+/* URI handler interface */
+
+static guint
+rb_mtp_sink_uri_get_type (void)
+{
+	return GST_URI_SINK;
+}
+
+static gchar **
+rb_mtp_sink_uri_get_protocols (void)
+{
+	static gchar *protocols[] = {"xrbmtp", NULL};
+	return protocols;
+}
+
+static const gchar *
+rb_mtp_sink_uri_get_uri (GstURIHandler *handler)
+{
+	/* more or less */
+	return "xrbmtp://";
+}
+
+static gboolean
+rb_mtp_sink_uri_set_uri (GstURIHandler *handler, const gchar *uri)
+{
+	RBMTPSink *sink = RB_MTP_SINK (handler);
+
+	if (GST_STATE (sink) == GST_STATE_PLAYING || GST_STATE (sink) == GST_STATE_PAUSED) {
+		return FALSE;
+	}
+
+	if (g_str_has_prefix (uri, "xrbmtp://") == FALSE) {
+		return FALSE;
+	}
+
+	/* URI doesn't actually contain any information, it all comes from the track */
+
+	return TRUE;
+}
+
+static void
+rb_mtp_sink_uri_handler_init (gpointer g_iface, gpointer iface_data)
+{
+	GstURIHandlerInterface *iface = (GstURIHandlerInterface *) g_iface;
+
+	iface->get_type = rb_mtp_sink_uri_get_type;
+	iface->get_protocols = rb_mtp_sink_uri_get_protocols;
+	iface->get_uri = rb_mtp_sink_uri_get_uri;
+	iface->set_uri = rb_mtp_sink_uri_set_uri;
+}
+
+static gboolean
+plugin_init (GstPlugin *plugin)
+{
+	gboolean ret = gst_element_register (plugin, "rbmtpsink", GST_RANK_PRIMARY, RB_TYPE_MTP_SINK);
+	return ret;
+}
+
+GST_PLUGIN_DEFINE_STATIC (GST_VERSION_MAJOR,
+			  GST_VERSION_MINOR,
+			  "rbmtpsink",
+			  "element to upload files to MTP devices",
+			  plugin_init,
+			  VERSION,
+			  "GPL",
+			  PACKAGE,
+			  "");
diff --git a/plugins/mtpdevice/rb-mtp-gst-src.c b/plugins/mtpdevice/rb-mtp-gst-src.c
index f7f227d..9430059 100644
--- a/plugins/mtpdevice/rb-mtp-gst-src.c
+++ b/plugins/mtpdevice/rb-mtp-gst-src.c
@@ -34,6 +34,7 @@
 #include <libmtp.h>
 #include <gst/gst.h>
 
+#include "rb-mtp-thread.h"
 #include "rb-debug.h"
 #include "rb-file-helpers.h"
 
@@ -50,7 +51,7 @@ struct _RBMTPSrc
 {
 	GstBin parent;
 
-	LIBMTP_mtpdevice_t *device;
+	RBMtpThread *device_thread;
 
 	char *track_uri;
 	uint32_t track_id;
@@ -58,6 +59,11 @@ struct _RBMTPSrc
 
 	GstElement *filesrc;
 	GstPad *ghostpad;
+
+	GError *download_error;
+	GMutex *download_mutex;
+	GCond *download_cond;
+	GstStateChangeReturn download_result;
 };
 
 struct _RBMTPSrcClass
@@ -69,7 +75,7 @@ enum
 {
 	PROP_0,
 	PROP_URI,
-	PROP_DEVICE
+	PROP_DEVICE_THREAD
 };
 
 static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
@@ -115,6 +121,9 @@ rb_mtp_src_init (RBMTPSrc *src, RBMTPSrcClass *klass)
 {
 	GstPad *pad;
 
+	src->download_mutex = g_mutex_new ();
+	src->download_cond = g_cond_new ();
+
 	/* create actual source */
 	src->filesrc = gst_element_factory_make ("filesrc", NULL);
 	if (src->filesrc == NULL) {
@@ -152,85 +161,65 @@ rb_mtp_src_set_uri (RBMTPSrc *src, const char *uri)
 	return TRUE;
 }
 
+static void
+download_cb (LIBMTP_track_t *track, const char *filename, GError *error, RBMTPSrc *src)
+{
+	rb_debug ("mtp download callback for %s: %s", filename, error ? error->message : "OK");
+	g_mutex_lock (src->download_mutex);
+
+	if (filename == NULL) {
+		src->download_error = g_error_copy (error);
+		src->download_result = GST_STATE_CHANGE_FAILURE;
+	} else {
+		src->download_result = GST_STATE_CHANGE_SUCCESS;
+		src->tempfile = g_strdup (filename);
+	}
+
+	g_cond_signal (src->download_cond);
+	g_mutex_unlock (src->download_mutex);
+}
+
 static GstStateChangeReturn
 rb_mtp_src_get_file (RBMTPSrc *src)
 {
-	LIBMTP_file_t *fileinfo;
-	GFile *dir;
-	GError *error;
-	char *tempfile;
-	int fd;
-	gboolean check;
-	int mtpret;
-
-	/* get file info */
-	fileinfo = LIBMTP_Get_Filemetadata (src->device, src->track_id);
-	if (fileinfo == NULL) {
-		rb_debug ("unable to get mtp file metadata");
-		LIBMTP_error_t *stack;
-
-		stack = LIBMTP_Get_Errorstack (src->device);
-		GST_ELEMENT_ERROR(src, RESOURCE, READ,
-				  (_("Unable to copy file from MTP device: %s"), stack->error_text),
-				  (NULL));
-		LIBMTP_Clear_Errorstack (src->device);
-	}
+	g_mutex_lock (src->download_mutex);
+	src->download_result = GST_STATE_CHANGE_ASYNC;
+	rb_mtp_thread_download_track (src->device_thread, src->track_id, "", (RBMtpDownloadCallback)download_cb, g_object_ref (src), g_object_unref);
 
-	/* check for free space */
-	dir = g_file_new_for_path (g_get_tmp_dir ());
-	rb_debug ("checking we've got %" G_GUINT64_FORMAT " bytes available in %s",
-		  fileinfo->filesize,
-		  g_get_tmp_dir ());
-	check = rb_check_dir_has_space (dir, fileinfo->filesize);
-	LIBMTP_destroy_file_t (fileinfo);
-	g_object_unref (dir);
-	if (check == FALSE) {
-		rb_debug ("not enough space to copy track from MTP device");
-		GST_ELEMENT_ERROR(src, RESOURCE, NO_SPACE_LEFT,
-				  (_("Not enough space in %s"), g_get_tmp_dir ()),
-				  (NULL));
-		g_object_unref (dir);
-		LIBMTP_destroy_file_t (fileinfo);
-		return GST_STATE_CHANGE_FAILURE;
+	while (src->download_result == GST_STATE_CHANGE_ASYNC) {
+		g_cond_wait (src->download_cond, src->download_mutex);
 	}
+	g_mutex_unlock (src->download_mutex);
+	rb_debug ("download completed, state change return %s", gst_element_state_change_return_get_name (src->download_result));
+
+	if (src->download_error) {
+		int code;
+		switch (src->download_error->code) {
+		case RB_MTP_THREAD_ERROR_NO_SPACE:
+			code = GST_RESOURCE_ERROR_NO_SPACE_LEFT;
+			break;
 
-	/* open temporary file */
-	fd = g_file_open_tmp ("rb-mtp-temp-XXXXXX", &tempfile, &error);
-	if (fd == -1) {
-		rb_debug ("failed to open temporary file");
-		GST_ELEMENT_ERROR(src, RESOURCE, OPEN_READ_WRITE,
-				  (_("Unable to open temporary file: %s"), error->message), (NULL));
-		g_error_free (error);
-		return GST_STATE_CHANGE_FAILURE;
-	}
-	rb_debug ("created temporary file %s", tempfile);
-
-	/* copy file from MTP device */
-	mtpret = LIBMTP_Get_Track_To_File_Descriptor (src->device, src->track_id, fd, NULL, NULL);
-	if (mtpret != 0) {
-		rb_debug ("failed to copy file from MTP device");
-		LIBMTP_error_t *stack;
-
-		stack = LIBMTP_Get_Errorstack (src->device);
-		GST_ELEMENT_ERROR(src, RESOURCE, READ,
-				  (_("Unable to copy file from MTP device: %s"), stack->error_text),
-				  (NULL));
-		LIBMTP_Clear_Errorstack (src->device);
-
-		close (fd);
-		remove (tempfile);
-		g_free (tempfile);
-		return GST_STATE_CHANGE_FAILURE;
-	}
+		case RB_MTP_THREAD_ERROR_TEMPFILE:
+			code = GST_RESOURCE_ERROR_OPEN_WRITE;
+			break;
 
-	rb_debug ("copied file from mtp device");
+		default:
+		case RB_MTP_THREAD_ERROR_GET_TRACK:
+			code = GST_RESOURCE_ERROR_READ;
+			break;
 
-	close (fd);
-	src->tempfile = tempfile;
+		}
 
-	/* point the filesrc at the file */
-	g_object_set (src->filesrc, "location", src->tempfile, NULL);
-	return GST_STATE_CHANGE_SUCCESS;
+		GST_WARNING_OBJECT (src, "error: %s", src->download_error->message);
+		gst_element_message_full (GST_ELEMENT (src),
+					  GST_MESSAGE_ERROR,
+					  GST_RESOURCE_ERROR, code,
+					  src->download_error->message, NULL,
+					  __FILE__, GST_FUNCTION, __LINE__);
+	} else if (src->download_result == GST_STATE_CHANGE_SUCCESS) {
+		g_object_set (src->filesrc, "location", src->tempfile, NULL);
+	}
+	return src->download_result;
 }
 
 static GstStateChangeReturn
@@ -295,8 +284,8 @@ rb_mtp_src_set_property (GObject *object, guint prop_id, const GValue *value, GP
 	case PROP_URI:
 		rb_mtp_src_set_uri (src, g_value_get_string (value));
 		break;
-	case PROP_DEVICE:
-		src->device = g_value_get_pointer (value);
+	case PROP_DEVICE_THREAD:
+		src->device_thread = g_value_dup_object (value);
 		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -313,8 +302,8 @@ rb_mtp_src_get_property (GObject *object, guint prop_id, GValue *value, GParamSp
 	case PROP_URI:
 		g_value_set_string (value, src->track_uri);
 		break;
-	case PROP_DEVICE:
-		g_value_set_pointer (value, src->device);
+	case PROP_DEVICE_THREAD:
+		g_value_set_object (value, src->device_thread);
 		break;
 	default:
 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -338,10 +327,31 @@ rb_mtp_src_dispose (GObject *object)
 		src->filesrc = NULL;
 	}
 
+	if (src->device_thread) {
+		g_object_unref (src->device_thread);
+		src->device_thread = NULL;
+	}
+
 	G_OBJECT_CLASS (parent_class)->dispose (object);
 }
 
 static void
+rb_mtp_src_finalize (GObject *object)
+{
+	RBMTPSrc *src;
+	src = RB_MTP_SRC (object);
+
+	g_mutex_free (src->download_mutex);
+	g_cond_free (src->download_cond);
+
+	if (src->download_error) {
+		g_error_free (src->download_error);
+	}
+
+	G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
 rb_mtp_src_class_init (RBMTPSrcClass *klass)
 {
 	GObjectClass *gobject_class;
@@ -349,6 +359,7 @@ rb_mtp_src_class_init (RBMTPSrcClass *klass)
 
 	gobject_class = G_OBJECT_CLASS (klass);
 	gobject_class->dispose = rb_mtp_src_dispose;
+	gobject_class->finalize = rb_mtp_src_finalize;
 	gobject_class->set_property = rb_mtp_src_set_property;
 	gobject_class->get_property = rb_mtp_src_get_property;
 
@@ -363,11 +374,12 @@ rb_mtp_src_class_init (RBMTPSrcClass *klass)
 							      NULL,
 							      G_PARAM_READWRITE));
 	g_object_class_install_property (gobject_class,
-					 PROP_DEVICE,
-					 g_param_spec_pointer ("device",
-							       "device",
-							       "libmtp device",
-							       G_PARAM_READWRITE));
+					 PROP_DEVICE_THREAD,
+					 g_param_spec_object ("device-thread",
+							      "device-thread",
+							      "device handling thread",
+							      RB_TYPE_MTP_THREAD,
+							      G_PARAM_READWRITE));
 }
 
 
diff --git a/plugins/mtpdevice/rb-mtp-plugin.c b/plugins/mtpdevice/rb-mtp-plugin.c
index c1d7053..1a7cf3d 100644
--- a/plugins/mtpdevice/rb-mtp-plugin.c
+++ b/plugins/mtpdevice/rb-mtp-plugin.c
@@ -103,15 +103,16 @@ static void impl_deactivate (RBPlugin *plugin, RBShell *shell);
 #if defined(HAVE_GUDEV)
 static RBSource* create_source_device_cb (RBRemovableMediaManager *rmm, GObject *device, RBMtpPlugin *plugin);
 #else
+static void rb_mtp_plugin_maybe_add_source (RBMtpPlugin *plugin, const char *udi, LIBMTP_raw_device_t *raw_devices, int num);
 static void rb_mtp_plugin_device_added (LibHalContext *context, const char *udi);
 static void rb_mtp_plugin_device_removed (LibHalContext *context, const char *udi);
 static gboolean rb_mtp_plugin_setup_dbus_hal_connection (RBMtpPlugin *plugin);
-static RBSource* create_source_cb (RBMtpPlugin *plugin, LIBMTP_mtpdevice_t *device, const char *udi);
 #endif
 static void rb_mtp_plugin_eject  (GtkAction *action, RBMtpPlugin *plugin);
 static void rb_mtp_plugin_rename (GtkAction *action, RBMtpPlugin *plugin);
 
 GType rb_mtp_src_get_type (void);
+GType rb_mtp_sink_get_type (void);
 
 RB_PLUGIN_REGISTER(RBMtpPlugin, rb_mtp_plugin)
 
@@ -139,8 +140,9 @@ rb_mtp_plugin_class_init (RBMtpPluginClass *klass)
 	/* register types used by the plugin */
 	RB_PLUGIN_REGISTER_TYPE (rb_mtp_source);
 
-	/* ensure the gstreamer src element gets linked in */
+	/* ensure the gstreamer elements get linked in */
 	rb_mtp_src_get_type ();
+	rb_mtp_sink_get_type ();
 }
 
 static void
@@ -168,10 +170,8 @@ impl_activate (RBPlugin *bplugin, RBShell *shell)
 #if defined(HAVE_GUDEV)
 	gboolean rmm_scanned = FALSE;
 #else
-	int num, i, ret;
-	char **devices;
-	LIBMTP_device_entry_t *entries;
-	int numentries;
+	int num_mtp_devices;
+	LIBMTP_raw_device_t *mtp_devices;
 #endif
 
 	plugin->shell = shell;
@@ -214,39 +214,25 @@ impl_activate (RBPlugin *bplugin, RBShell *shell)
 	}
 
 	rb_profile_start ("scanning for MTP devices");
-	devices = libhal_get_all_devices (plugin->hal_context, &num, NULL);
-	ret = LIBMTP_Get_Supported_Devices_List (&entries, &numentries);
-	if (ret == 0) {
-
-		for (i = 0; i < num; i++) {
-			int vendor_id;
-			int product_id;
-			const char *tmpudi;
-			int  p;
-
-			tmpudi = devices[i];
-			vendor_id = libhal_device_get_property_int (plugin->hal_context, tmpudi, "usb.vendor_id", NULL);
-			product_id = libhal_device_get_property_int (plugin->hal_context, tmpudi, "usb.product_id", NULL);
-			for (p = 0; p < numentries; p++) {
-
-				if (entries[p].vendor_id == vendor_id && entries[p].product_id == product_id) {
-					LIBMTP_mtpdevice_t *device = LIBMTP_Get_First_Device ();
-					if (device != NULL) {
-						create_source_cb (plugin, device, tmpudi);
-						break;
-					} else {
-						rb_debug ("error, could not get a hold on the device. Reset and Restart");
-					}
-				}
-			}
+	LIBMTP_Detect_Raw_Devices (&mtp_devices, &num_mtp_devices);
+	if (num_mtp_devices > 0) {
+		int num_hal_devices;
+		char **hal_devices;
+		int i;
+
+		rb_debug ("%d MTP devices found", num_mtp_devices);
+
+		hal_devices = libhal_get_all_devices (plugin->hal_context, &num_hal_devices, NULL);
+		for (i = 0; i < num_hal_devices; i++) {
+			/* should narrow this down a bit - usb only, for a start */
+			rb_mtp_plugin_maybe_add_source (plugin, hal_devices[i], mtp_devices, num_mtp_devices);
 		}
-	} else {
-		rb_debug ("Couldn't list mtp devices");
+		libhal_free_string_array (hal_devices);
+	}
+	if (mtp_devices != NULL) {
+		free (mtp_devices);
 	}
-
-	libhal_free_string_array (devices);
 	rb_profile_end ("scanning for MTP devices");
-
 #endif
 
 	g_object_unref (rmm);
@@ -296,12 +282,61 @@ impl_deactivate (RBPlugin *bplugin, RBShell *shell)
 }
 
 static void
+rb_mtp_plugin_eject (GtkAction *action, RBMtpPlugin *plugin)
+{
+	RBSourceList *sourcelist = NULL;
+	RBSource *source = NULL;
+
+	g_object_get (G_OBJECT (plugin->shell),
+		      "selected-source", &source,
+		      NULL);
+	if ((source == NULL) || !RB_IS_MTP_SOURCE (source)) {
+		g_warning ("got MTPSourceEject action for non-mtp source");
+		if (source != NULL)
+			g_object_unref (source);
+		return;
+	}
+
+	g_object_get (plugin->shell, "sourcelist", &sourcelist, NULL);
+
+	rb_source_delete_thyself (source);
+
+	g_object_unref (sourcelist);
+	g_object_unref (source);
+}
+
+static void
+rb_mtp_plugin_rename (GtkAction *action, RBMtpPlugin *plugin)
+{
+	RBSourceList *sourcelist = NULL;
+	RBSource *source = NULL;
+
+	g_object_get (G_OBJECT (plugin->shell),
+		      "selected-source", &source,
+		      NULL);
+	if ((source == NULL) || !RB_IS_MTP_SOURCE (source)) {
+		g_warning ("got MTPSourceEject action for non-mtp source");
+		if (source != NULL)
+			g_object_unref (source);
+		return;
+	}
+
+	g_object_get (plugin->shell, "sourcelist", &sourcelist, NULL);
+
+	rb_sourcelist_edit_source_name (sourcelist, source);
+
+	g_object_unref (sourcelist);
+	g_object_unref (source);
+}
+
+
+#if defined(HAVE_GUDEV)
+static void
 source_deleted_cb (RBMtpSource *source, RBMtpPlugin *plugin)
 {
 	plugin->mtp_sources = g_list_remove (plugin->mtp_sources, source);
 }
 
-#if defined(HAVE_GUDEV)
 static RBSource *
 create_source_device_cb (RBRemovableMediaManager *rmm, GObject *device, RBMtpPlugin *plugin)
 {
@@ -339,25 +374,16 @@ create_source_device_cb (RBRemovableMediaManager *rmm, GObject *device, RBMtpPlu
 	/* see what devices libmtp can find */
 	if (LIBMTP_Detect_Raw_Devices (&raw_devices, &num_raw_devices) == 0) {
 		for (i = 0; i < num_raw_devices; i++) {
-			LIBMTP_mtpdevice_t *device;
 			RBSource *source;
 
 			rb_debug ("detected mtp device: device number %d", raw_devices[i].devnum);
-
-			/* check bus number/device location somehow */
 			if (devnum != raw_devices[i].devnum) {
 				rb_debug ("device number mismatches: %d vs %d", devnum, raw_devices[i].devnum);
 				continue;
 			}
 
-			device = LIBMTP_Open_Raw_Device (&raw_devices[i]);
-			if (device == NULL) {
-				rb_debug ("unable to open device.  weird.");
-				break;
-			}
-
 			rb_debug ("device matched, creating a source");
-			source = rb_mtp_source_new (plugin->shell, device);
+			source = rb_mtp_source_new (plugin->shell, &raw_devices[i]);
 			plugin->mtp_sources = g_list_prepend (plugin->mtp_sources, source);
 			g_signal_connect_object (G_OBJECT (source),
 						"deleted", G_CALLBACK (source_deleted_cb),
@@ -372,118 +398,60 @@ create_source_device_cb (RBRemovableMediaManager *rmm, GObject *device, RBMtpPlu
 
 #else
 
-static RBSource *
-create_source_cb (RBMtpPlugin *plugin, LIBMTP_mtpdevice_t *device, const char *udi)
+static void
+source_deleted_cb (RBMtpSource *source, RBMtpPlugin *plugin)
 {
-	RBSource *source;
-
-	source = RB_SOURCE (rb_mtp_source_new (plugin->shell, device, udi));
-
-	rb_shell_append_source (plugin->shell, source, NULL);
-	plugin->mtp_sources = g_list_prepend (plugin->mtp_sources, source);
-
-	g_signal_connect_object (G_OBJECT (source),
-				"deleted", G_CALLBACK (source_deleted_cb),
-				plugin, 0);
-
-	return source;
+	plugin->mtp_sources = g_list_remove (plugin->mtp_sources, source);
 }
 
-#endif
-
-
 static void
-rb_mtp_plugin_eject (GtkAction *action, RBMtpPlugin *plugin)
+rb_mtp_plugin_maybe_add_source (RBMtpPlugin *plugin, const char *udi, LIBMTP_raw_device_t *raw_devices, int num_raw_devices)
 {
-	RBSourceList *sourcelist = NULL;
-	RBSource *source = NULL;
+	int i;
+	int device_num = 0;
+	DBusError error;
 
-	g_object_get (G_OBJECT (plugin->shell),
-		      "selected-source", &source,
-		      NULL);
-	if ((source == NULL) || !RB_IS_MTP_SOURCE (source)) {
-		g_warning ("got MTPSourceEject action for non-mtp source");
-		if (source != NULL)
-			g_object_unref (source);
+	rb_debug ("checking if UDI %s matches an MTP device", udi);
+
+	/* get device number */
+	dbus_error_init (&error);
+	device_num = libhal_device_get_property_int (plugin->hal_context, udi, "usb.linux.device_number", &error);
+	if (dbus_error_is_set (&error)) {
+		rb_debug ("unable to get USB device number: %s", error.message);
+		dbus_error_free (&error);
 		return;
 	}
 
-	g_object_get (plugin->shell, "sourcelist", &sourcelist, NULL);
+	rb_debug ("USB device number: %d", device_num);
 
-	rb_source_delete_thyself (source);
-
-	g_object_unref (sourcelist);
-	g_object_unref (source);
-}
+	for (i = 0; i < num_raw_devices; i++) {
+		rb_debug ("detected MTP device: device number %d (bus location %u)", raw_devices[i].devnum, raw_devices[i].bus_location);
+		if (raw_devices[i].devnum == device_num) {
+			RBSource *source;
+			rb_debug ("device matched, creating a source");
+			source = RB_SOURCE (rb_mtp_source_new (plugin->shell, &raw_devices[i], udi));
 
-static void
-rb_mtp_plugin_rename (GtkAction *action, RBMtpPlugin *plugin)
-{
-	RBSourceList *sourcelist = NULL;
-	RBSource *source = NULL;
+			rb_shell_append_source (plugin->shell, source, NULL);
+			plugin->mtp_sources = g_list_prepend (plugin->mtp_sources, source);
 
-	g_object_get (G_OBJECT (plugin->shell),
-		      "selected-source", &source,
-		      NULL);
-	if ((source == NULL) || !RB_IS_MTP_SOURCE (source)) {
-		g_warning ("got MTPSourceEject action for non-mtp source");
-		if (source != NULL)
-			g_object_unref (source);
-		return;
+			g_signal_connect_object (G_OBJECT (source),
+						"deleted", G_CALLBACK (source_deleted_cb),
+						plugin, 0);
+		}
 	}
-
-	g_object_get (plugin->shell, "sourcelist", &sourcelist, NULL);
-
-	rb_sourcelist_edit_source_name (sourcelist, source);
-
-	g_object_unref (sourcelist);
-	g_object_unref (source);
 }
 
-#if !defined(HAVE_GUDEV)
-
 static void
 rb_mtp_plugin_device_added (LibHalContext *context, const char *udi)
 {
 	RBMtpPlugin *plugin = (RBMtpPlugin *) libhal_ctx_get_user_data (context);
-	LIBMTP_device_entry_t *entries;
-	int numentries;
-	int vendor_id;
-	int product_id;
-	int ret;
-
-	if (g_list_length (plugin->mtp_sources) > 0) {
-		rb_debug ("plugin only supports one device at the time right now.");
-		return;
-	}
+	LIBMTP_raw_device_t *mtp_devices;
+	int num_mtp_devices;
 
-	vendor_id = libhal_device_get_property_int (context, udi, "usb.vendor_id", NULL);
-	product_id = libhal_device_get_property_int (context, udi, "usb.product_id", NULL);
-
-	ret = LIBMTP_Get_Supported_Devices_List (&entries, &numentries);
-	if (ret == 0) {
-		int i, p;
-
-		for (i = 0; i < numentries; i++) {
-			if ((entries[i].vendor_id==vendor_id) && (entries[i].product_id == product_id)) {
-				/*
-				 * FIXME:
-				 *
-				 * It usualy takes a while for the device to set itself up.
-				 * Solving that by trying 10 times with some sleep in between.
-				 * There is probably a better solution, but this works.
-				 */
-				rb_debug ("adding device source");
-				for (p = 0; p < 10; p++) {
-					LIBMTP_mtpdevice_t *device = LIBMTP_Get_First_Device ();
-					if (device != NULL) {
-						create_source_cb (plugin, device, udi);
-						break;
-					}
-					usleep (200000);
-				}
-			}
-		}
+	LIBMTP_Detect_Raw_Devices (&mtp_devices, &num_mtp_devices);
+	if (mtp_devices != NULL) {
+		rb_mtp_plugin_maybe_add_source (plugin, udi, mtp_devices, num_mtp_devices);
+		free (mtp_devices);
 	}
 }
 
diff --git a/plugins/mtpdevice/rb-mtp-source.c b/plugins/mtpdevice/rb-mtp-source.c
index e41872f..a51fbe5 100644
--- a/plugins/mtpdevice/rb-mtp-source.c
+++ b/plugins/mtpdevice/rb-mtp-source.c
@@ -50,6 +50,7 @@
 #include "rb-encoder.h"
 
 #include "rb-mtp-source.h"
+#include "rb-mtp-thread.h"
 
 #define CONF_STATE_PANED_POSITION CONF_PREFIX "/state/mtp/paned_position"
 #define CONF_STATE_SHOW_BROWSER   CONF_PREFIX "/state/mtp/show_browser"
@@ -71,8 +72,6 @@ static void rb_mtp_source_get_property (GObject *object,
 static char *impl_get_browser_key (RBSource *source);
 static char *impl_get_paned_key (RBBrowserSource *source);
 
-static void rb_mtp_source_load_tracks (RBMtpSource*);
-
 static void impl_delete (RBSource *asource);
 static gboolean impl_show_popup (RBSource *source);
 static GList* impl_get_ui_actions (RBSource *source);
@@ -82,11 +81,17 @@ static gboolean impl_track_added (RBRemovableMediaSource *source,
 				  RhythmDBEntry *entry,
 				  const char *dest,
 				  const char *mimetype);
+static gboolean impl_track_add_error (RBRemovableMediaSource *source,
+				      RhythmDBEntry *entry,
+				      const char *dest,
+				      GError *error);
 static char* impl_build_dest_uri (RBRemovableMediaSource *source,
 				  RhythmDBEntry *entry,
 				  const char *mimetype,
 				  const char *extension);
 
+static void mtp_device_open_cb (LIBMTP_mtpdevice_t *device, RBMtpSource *source);
+static void mtp_tracklist_cb (LIBMTP_track_t *tracks, RBMtpSource *source);
 static RhythmDB * get_db_for_source (RBMtpSource *source);
 static void artwork_notify_cb (RhythmDB *db,
 			       RhythmDBEntry *entry,
@@ -94,8 +99,6 @@ static void artwork_notify_cb (RhythmDB *db,
 			       const GValue *metadata,
 			       RBMtpSource *source);
 
-static void add_track_to_album (RBMtpSource *source, const char *album_name, LIBMTP_track_t *track);
-
 static void prepare_player_source_cb (RBPlayer *player,
 				      const char *stream_uri,
 				      GstElement *src,
@@ -104,13 +107,18 @@ static void prepare_encoder_source_cb (RBEncoderFactory *factory,
 				       const char *stream_uri,
 				       GObject *src,
 				       RBMtpSource *source);
+static void prepare_encoder_sink_cb (RBEncoderFactory *factory,
+				     const char *stream_uri,
+				     GObject *sink,
+				     RBMtpSource *source);
 
 typedef struct
 {
-	LIBMTP_mtpdevice_t *device;
+	RBMtpThread *device_thread;
+	LIBMTP_raw_device_t *raw_device;
 	GHashTable *entry_map;
-	GHashTable *album_map;
 	GHashTable *artwork_request_map;
+	GHashTable *track_transfer_map;
 #if !defined(HAVE_GUDEV)
 	char *udi;
 #endif
@@ -118,7 +126,6 @@ typedef struct
 	GList *mediatypes;
 	gboolean album_art_supported;
 
-	guint load_songs_idle_id;
 } RBMtpSourcePrivate;
 
 RB_PLUGIN_DEFINE_TYPE(RBMtpSource,
@@ -130,30 +137,11 @@ RB_PLUGIN_DEFINE_TYPE(RBMtpSource,
 enum
 {
 	PROP_0,
-	PROP_LIBMTP_DEVICE,
+	PROP_RAW_DEVICE,
 	PROP_UDI,
 };
 
 static void
-report_libmtp_errors (LIBMTP_mtpdevice_t *device, gboolean use_dialog)
-{
-	LIBMTP_error_t *stack;
-
-	for (stack = LIBMTP_Get_Errorstack (device); stack != NULL; stack = stack->next) {
-		if (use_dialog) {
-			rb_error_dialog (NULL, _("Media player device error"), "%s", stack->error_text);
-
-			/* only display one dialog box per error */
-			use_dialog = FALSE;
-		} else {
-			g_warning ("libmtp error: %s", stack->error_text);
-		}
-	}
-
-	LIBMTP_Clear_Errorstack (device);
-}
-
-static void
 rb_mtp_source_class_init (RBMtpSourceClass *klass)
 {
 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
@@ -184,15 +172,16 @@ rb_mtp_source_class_init (RBMtpSourceClass *klass)
 	browser_source_class->impl_get_paned_key = impl_get_paned_key;
 
 	rms_class->impl_track_added = impl_track_added;
+	rms_class->impl_track_add_error = impl_track_add_error;
 	rms_class->impl_build_dest_uri = impl_build_dest_uri;
 	rms_class->impl_get_mime_types = impl_get_mime_types;
 	rms_class->impl_should_paste = rb_removable_media_source_should_paste_no_duplicate;
 
 	g_object_class_install_property (object_class,
-					 PROP_LIBMTP_DEVICE,
-					 g_param_spec_pointer ("libmtp-device",
-							       "libmtp-device",
-							       "libmtp device",
+					 PROP_RAW_DEVICE,
+					 g_param_spec_pointer ("raw-device",
+							       "raw-device",
+							       "libmtp raw device",
 							       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
 #if !defined(HAVE_GUDEV)
 	g_object_class_install_property (object_class,
@@ -216,9 +205,7 @@ rb_mtp_source_name_changed_cb (RBMtpSource *source,
 	char *name = NULL;
 
 	g_object_get (source, "name", &name, NULL);
-	if (LIBMTP_Set_Friendlyname (priv->device, name) != 0) {
-		report_libmtp_errors (priv->device, TRUE);
-	}
+	rb_mtp_thread_set_device_name (priv->device_thread, name);
 	g_free (name);
 }
 
@@ -231,11 +218,9 @@ rb_mtp_source_init (RBMtpSource *source)
 						 g_direct_equal,
 						 NULL,
 						 (GDestroyNotify) LIBMTP_destroy_track_t);
-	priv->album_map = g_hash_table_new_full (g_str_hash,
-						 g_str_equal,
-						 NULL,
-						 (GDestroyNotify) LIBMTP_destroy_album_t);
 	priv->artwork_request_map = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+	priv->track_transfer_map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
 }
 
 static GObject *
@@ -251,14 +236,16 @@ rb_mtp_source_constructor (GType type, guint n_construct_properties,
 	GtkIconTheme *theme;
 	GdkPixbuf *pixbuf;
 	gint size;
-	guint16 *types = NULL;
-	guint16 num_types= 0;
 
 	source = RB_MTP_SOURCE (G_OBJECT_CLASS (rb_mtp_source_parent_class)->
 				constructor (type, n_construct_properties, construct_properties));
 
 	priv = MTP_SOURCE_GET_PRIVATE (source);
 
+	/* start the device thread */
+	priv->device_thread = rb_mtp_thread_new ();
+	rb_mtp_thread_open_device (priv->device_thread, priv->raw_device, (RBMtpOpenCallback)mtp_device_open_cb, g_object_ref (source), g_object_unref);
+
 	tracks = rb_source_get_entry_view (RB_SOURCE (source));
 	rb_entry_view_append_column (tracks, RB_ENTRY_VIEW_COL_RATING, FALSE);
 	rb_entry_view_append_column (tracks, RB_ENTRY_VIEW_COL_LAST_PLAYED, FALSE);
@@ -280,6 +267,10 @@ rb_mtp_source_constructor (GType type, guint n_construct_properties,
 				 "prepare-source",
 				 G_CALLBACK (prepare_encoder_source_cb),
 				 source, 0);
+	g_signal_connect_object (rb_encoder_factory_get (),
+				 "prepare-sink",
+				 G_CALLBACK (prepare_encoder_sink_cb),
+				 source, 0);
 
 	/* icon */
 	theme = gtk_icon_theme_get_default ();
@@ -289,82 +280,6 @@ rb_mtp_source_constructor (GType type, guint n_construct_properties,
 	rb_source_set_pixbuf (RB_SOURCE (source), pixbuf);
 	g_object_unref (pixbuf);
 
-	g_signal_connect (G_OBJECT (source), "notify::name",
-			  (GCallback)rb_mtp_source_name_changed_cb, NULL);
-	
-	/* figure out supported file types */
-	if (LIBMTP_Get_Supported_Filetypes(priv->device, &types, &num_types) == 0) {
-		int i;
-		gboolean has_mp3 = FALSE;
-		for (i = 0; i < num_types; i++) {
-			const char *mediatype;
-
-			if (i <= LIBMTP_FILETYPE_UNKNOWN) {
-				priv->supported_types[types[i]] = 1;
-			}
-
-			/* this has to work with the remapping done in 
-			 * rb-removable-media-source.c:impl_paste.
-			 */
-			switch (types[i]) {
-			case LIBMTP_FILETYPE_WAV:
-				mediatype = "audio/x-wav";
-				break;
-			case LIBMTP_FILETYPE_MP3:
-				/* special handling for mp3: always put it at the front of the list
-				 * if it's supported.
-				 */
-				has_mp3 = TRUE;
-				mediatype = NULL;
-				break;
-			case LIBMTP_FILETYPE_WMA:
-				mediatype = "audio/x-ms-wma";
-				break;
-			case LIBMTP_FILETYPE_OGG:
-				mediatype = "application/ogg";
-				break;
-			case LIBMTP_FILETYPE_MP4:
-			case LIBMTP_FILETYPE_M4A:
-			case LIBMTP_FILETYPE_AAC:
-				mediatype = "audio/aac";
-				break;
-			case LIBMTP_FILETYPE_WMV:
-				mediatype = "audio/x-ms-wmv";
-				break;
-			case LIBMTP_FILETYPE_ASF:
-				mediatype = "video/x-ms-asf";
-				break;
-			case LIBMTP_FILETYPE_FLAC:
-				mediatype = "audio/flac";
-				break;
-
-			case LIBMTP_FILETYPE_JPEG:
-				rb_debug ("JPEG (album art) supported");
-				mediatype = NULL;
-				priv->album_art_supported = TRUE;
-				break;
-
-			default:
-				rb_debug ("unknown libmtp filetype %s supported", LIBMTP_Get_Filetype_Description (types[i]));
-				mediatype = NULL;
-				break;
-			}
-
-			if (mediatype != NULL) {
-				rb_debug ("media type %s supported", mediatype);
-				priv->mediatypes = g_list_prepend (priv->mediatypes,
-								   g_strdup (mediatype));
-			}
-		}
-
-		if (has_mp3) {
-			rb_debug ("audio/mpeg supported");
-			priv->mediatypes = g_list_prepend (priv->mediatypes, g_strdup ("audio/mpeg"));
-		}
-	} else {
-		report_libmtp_errors (priv->device, FALSE);
-	}
-	
 	if (priv->album_art_supported) {
 		RhythmDB *db;
 
@@ -374,8 +289,6 @@ rb_mtp_source_constructor (GType type, guint n_construct_properties,
 		g_object_unref (db);
 	}
 
-	rb_mtp_source_load_tracks (source);
-
 	return G_OBJECT (source);
 }
 
@@ -388,8 +301,8 @@ rb_mtp_source_set_property (GObject *object,
 	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (object);
 
 	switch (prop_id) {
-	case PROP_LIBMTP_DEVICE:
-		priv->device = g_value_get_pointer (value);
+	case PROP_RAW_DEVICE:
+		priv->raw_device = g_value_get_pointer (value);
 		break;
 #if !defined(HAVE_GUDEV)
 	case PROP_UDI:
@@ -411,8 +324,8 @@ rb_mtp_source_get_property (GObject *object,
 	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (object);
 
 	switch (prop_id) {
-	case PROP_LIBMTP_DEVICE:
-		g_value_set_pointer (value, priv->device);
+	case PROP_RAW_DEVICE:
+		g_value_set_pointer (value, priv->raw_device);
 		break;
 #if !defined(HAVE_GUDEV)
 	case PROP_UDI:
@@ -433,6 +346,11 @@ rb_mtp_source_dispose (GObject *object)
 	RhythmDBEntryType entry_type;
 	RhythmDB *db;
 
+	if (priv->device_thread != NULL) {
+		g_object_unref (priv->device_thread);
+		priv->device_thread = NULL;
+	}
+
 	db = get_db_for_source (source);
 
 	g_object_get (G_OBJECT (source), "entry-type", &entry_type, NULL);
@@ -442,11 +360,6 @@ rb_mtp_source_dispose (GObject *object)
 	rhythmdb_commit (db);
 	g_object_unref (db);
 
-	if (priv->load_songs_idle_id != 0) {
-		g_source_remove (priv->load_songs_idle_id);
-		priv->load_songs_idle_id = 0;
-	}
-
 	G_OBJECT_CLASS (rb_mtp_source_parent_class)->dispose (object);
 }
 
@@ -456,15 +369,13 @@ rb_mtp_source_finalize (GObject *object)
 	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (object);
 
 	g_hash_table_destroy (priv->entry_map);
-	g_hash_table_destroy (priv->album_map);
 	g_hash_table_destroy (priv->artwork_request_map);
+	g_hash_table_destroy (priv->track_transfer_map);		/* probably need to destroy the tracks too.. */
 
 #if !defined(HAVE_GUDEV)
 	g_free (priv->udi);
 #endif
 
-	LIBMTP_Release_Device (priv->device);
-
 	G_OBJECT_CLASS (rb_mtp_source_parent_class)->finalize (object);
 }
 
@@ -483,11 +394,11 @@ impl_get_paned_key (RBBrowserSource *source)
 #if defined(HAVE_GUDEV)
 RBSource *
 rb_mtp_source_new (RBShell *shell,
-		   LIBMTP_mtpdevice_t *device)
+		   LIBMTP_raw_device_t *device)
 #else
 RBSource *
 rb_mtp_source_new (RBShell *shell,
-		   LIBMTP_mtpdevice_t *device,
+		   LIBMTP_raw_device_t *device,
 		   const char *udi)
 #endif
 {
@@ -497,7 +408,7 @@ rb_mtp_source_new (RBShell *shell,
 	char *name = NULL;
 
 	g_object_get (shell, "db", &db, NULL);
-	name = g_strdup_printf ("MTP-%s", LIBMTP_Get_Serialnumber (device));
+	name = g_strdup_printf ("MTP-%u-%d", device->bus_location, device->devnum);
 
 	entry_type = rhythmdb_entry_register_type (db, name);
 	entry_type->save_to_disk = FALSE;
@@ -512,7 +423,7 @@ rb_mtp_source_new (RBShell *shell,
 					      "visibility", TRUE,
 					      "volume", NULL,
 					      "source-group", RB_SOURCE_GROUP_DEVICES,
-					      "libmtp-device", device,
+					      "raw-device", device,
 #if !defined(HAVE_GUDEV)
 					      "udi", udi,
 #endif
@@ -540,7 +451,7 @@ entry_set_string_prop (RhythmDB *db,
 	g_value_unset (&value);
 }
 
-static void
+static RhythmDBEntry *
 add_mtp_track_to_db (RBMtpSource *source,
 		     RhythmDB *db,
 		     LIBMTP_track_t *track)
@@ -555,7 +466,7 @@ add_mtp_track_to_db (RBMtpSource *source,
 		rb_debug ("ignoring non-audio item %d (filetype %s)",
 			  track->item_id,
 			  LIBMTP_Get_Filetype_Description (track->filetype));
-		return;
+		return NULL;
 	}
 
 	/* Set URI */
@@ -568,7 +479,7 @@ add_mtp_track_to_db (RBMtpSource *source,
 	if (entry == NULL) {
 		rb_debug ("cannot create entry %i", track->item_id);
 		g_object_unref (G_OBJECT (db));
-		return;
+		return NULL;
 	}
 
 	/* Set track number */
@@ -635,109 +546,166 @@ add_mtp_track_to_db (RBMtpSource *source,
 
 	g_hash_table_insert (priv->entry_map, entry, track);
 	rhythmdb_commit (RHYTHMDB (db));
+
+	return entry;
 }
 
+typedef struct {
+	RBMtpSource *source;
+	char *name;
+	guint16 *types;
+	guint16 num_types;
+} DeviceOpenedData;
+
 static gboolean
-load_mtp_db_idle_cb (RBMtpSource* source)
+device_opened_idle (DeviceOpenedData *data)
 {
-	RhythmDB *db = NULL;
-	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (source);
-	LIBMTP_track_t *tracks = NULL;
-	LIBMTP_album_t *albums;
-	gboolean device_forgets_albums = TRUE;
-
-	db = get_db_for_source (source);
+	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (data->source);
+	int i;
+	gboolean has_mp3 = FALSE;
 
-	g_assert (db != NULL);
+	if (data->name != NULL) {
+		g_object_set (data->source, "name", data->name, NULL);
+	}
 
-	albums = LIBMTP_Get_Album_List (priv->device);
-	report_libmtp_errors (priv->device, FALSE);
-	if (albums != NULL) {
-		LIBMTP_album_t *album;
+	/* when the source name changes after this, try to update the device name */
+	g_signal_connect (G_OBJECT (data->source), "notify::name",
+			  (GCallback)rb_mtp_source_name_changed_cb, NULL);
 
-		for (album = albums; album != NULL; album = album->next) {
-			if (album->name == NULL)
-				continue;
+	for (i = 0; i < data->num_types; i++) {
+		const char *mediatype;
 
-			rb_debug ("album: %s, %d tracks", album->name, album->no_tracks);
-			g_hash_table_insert (priv->album_map, album->name, album);
-			if (album->no_tracks != 0) {
-				device_forgets_albums = FALSE;
-			}
+		if (i <= LIBMTP_FILETYPE_UNKNOWN) {
+			priv->supported_types[data->types[i]] = 1;
 		}
 
-		if (device_forgets_albums) {
-			rb_debug ("stupid mtp device detected.  will rebuild all albums.");
-		}
-	} else {
-		rb_debug ("No albums");
-		device_forgets_albums = FALSE;
-	}
+		/* this has to work with the remapping done in
+		 * rb-removable-media-source.c:impl_paste.
+		 */
+		switch (data->types[i]) {
+		case LIBMTP_FILETYPE_WAV:
+			mediatype = "audio/x-wav";
+			break;
+		case LIBMTP_FILETYPE_MP3:
+			/* special handling for mp3: always put it at the front of the list
+			 * if it's supported.
+			 */
+			has_mp3 = TRUE;
+			mediatype = NULL;
+			break;
+		case LIBMTP_FILETYPE_WMA:
+			mediatype = "audio/x-ms-wma";
+			break;
+		case LIBMTP_FILETYPE_OGG:
+			mediatype = "application/ogg";
+			break;
+		case LIBMTP_FILETYPE_MP4:
+		case LIBMTP_FILETYPE_M4A:
+		case LIBMTP_FILETYPE_AAC:
+			mediatype = "audio/aac";
+			break;
+		case LIBMTP_FILETYPE_WMV:
+			mediatype = "audio/x-ms-wmv";
+			break;
+		case LIBMTP_FILETYPE_ASF:
+			mediatype = "video/x-ms-asf";
+			break;
+		case LIBMTP_FILETYPE_FLAC:
+			mediatype = "audio/flac";
+			break;
 
-	tracks = LIBMTP_Get_Tracklisting_With_Callback (priv->device, NULL, NULL);
-	report_libmtp_errors (priv->device, FALSE);
-	if (tracks != NULL) {
-		LIBMTP_track_t *track;
-		for (track = tracks; track != NULL; track = track->next) {
-			add_mtp_track_to_db (source, db, track);
+		case LIBMTP_FILETYPE_JPEG:
+			rb_debug ("JPEG (album art) supported");
+			mediatype = NULL;
+			priv->album_art_supported = TRUE;
+			break;
 
-			if (device_forgets_albums && track->album != NULL) {
-				add_track_to_album (source, track->album, track);
-			}
+		default:
+			rb_debug ("unknown libmtp filetype %s supported", LIBMTP_Get_Filetype_Description (data->types[i]));
+			mediatype = NULL;
+			break;
 		}
-	} else {
-		rb_debug ("No tracks");
-	}
 
-	/* for stupid devices, remove any albums left with no tracks */
-	if (device_forgets_albums) {
-		GHashTableIter iter;
-		gpointer value;
-		LIBMTP_album_t *album;
-
-		g_hash_table_iter_init (&iter, priv->album_map);
-		while (g_hash_table_iter_next (&iter, NULL, &value)) {
-			int ret;
-
-			album = value;
-			if (album->no_tracks == 0) {
-				rb_debug ("pruning empty album \"%s\"", album->name); 
-				ret = LIBMTP_Delete_Object (priv->device, album->album_id);
-				if (ret != 0) {
-					report_libmtp_errors (priv->device, FALSE);
-				}
-				g_hash_table_iter_remove (&iter);
-			}
+		if (mediatype != NULL) {
+			rb_debug ("media type %s supported", mediatype);
+			priv->mediatypes = g_list_prepend (priv->mediatypes,
+							   g_strdup (mediatype));
 		}
 	}
 
+	if (has_mp3) {
+		rb_debug ("audio/mpeg supported");
+		priv->mediatypes = g_list_prepend (priv->mediatypes, g_strdup ("audio/mpeg"));
+	}
+
+	g_object_unref (data->source);
+	free (data->types);
+	g_free (data->name);
+	g_free (data);
 
-	g_object_unref (G_OBJECT (db));
 	return FALSE;
 }
 
+static gboolean
+device_open_failed_idle (RBMtpSource *source)
+{
+	rb_source_delete_thyself (RB_SOURCE (source));
+	g_object_unref (source);
+	return FALSE;
+}
+
+/* this callback runs on the device handling thread, so it can call libmtp directly */
 static void
-rb_mtp_source_load_tracks (RBMtpSource *source)
+mtp_device_open_cb (LIBMTP_mtpdevice_t *device, RBMtpSource *source)
 {
 	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (source);
-	char *name = NULL;
+	DeviceOpenedData *data;
 
-	name = LIBMTP_Get_Friendlyname (priv->device);
-	/* ignore some particular broken device names */
-	if (name == NULL || strcmp (name, "?????") == 0) {
-		g_free (name);
-		name = LIBMTP_Get_Modelname (priv->device);
+	if (device == NULL) {
+		rb_mtp_thread_report_errors (priv->device_thread, TRUE);
+
+		/* can't delete the source on this thread, so move it to the main thread */
+		g_idle_add ((GSourceFunc) device_open_failed_idle, g_object_ref (source));
+		return;
+	}
+
+	/* set the source name to match the device, ignoring some
+	 * particular broken device names.
+	 */
+	data = g_new0 (DeviceOpenedData, 1);
+	data->source = g_object_ref (source);
+	data->name = LIBMTP_Get_Friendlyname (device);
+	if (data->name == NULL || strcmp (data->name, "?????") == 0) {
+		g_free (data->name);
+		data->name = LIBMTP_Get_Modelname (device);
 	}
-	if (name == NULL) {
-		name = g_strdup (_("Digital Audio Player"));
+	if (data->name == NULL) {
+		data->name = g_strdup (_("Digital Audio Player"));
 	}
 
-	g_object_set (RB_SOURCE (source),
-		      "name", name,
-		      NULL);
+	/* figure out the set of formats supported by the device */
+	if (LIBMTP_Get_Supported_Filetypes (device, &data->types, &data->num_types) != 0) {
+		rb_mtp_thread_report_errors (priv->device_thread, FALSE);
+	}
 
-	priv->load_songs_idle_id = g_idle_add ((GSourceFunc)load_mtp_db_idle_cb, source);
-	g_free (name);
+	g_idle_add ((GSourceFunc) device_opened_idle, data);
+
+	/* now get the track list */
+	rb_mtp_thread_get_track_list (priv->device_thread, (RBMtpTrackListCallback) mtp_tracklist_cb, g_object_ref (source), g_object_unref);
+}
+
+static void
+mtp_tracklist_cb (LIBMTP_track_t *tracks, RBMtpSource *source)
+{
+	RhythmDB *db = NULL;
+	LIBMTP_track_t *track;
+
+	/* add tracks to database */
+	db = get_db_for_source (source);
+	for (track = tracks; track != NULL; track = track->next) {
+		add_mtp_track_to_db (source, db, track);
+	}
+	g_object_unref (db);
 }
 
 static char *
@@ -782,92 +750,6 @@ mimetype_to_filetype (RBMtpSource *source, const char *mimetype)
 }
 
 static void
-add_track_to_album (RBMtpSource *source, const char *album_name, LIBMTP_track_t *track)
-{
-	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (source);
-	LIBMTP_album_t *album;
-
-	album = g_hash_table_lookup (priv->album_map, album_name);
-	if (album != NULL) {
-		/* add track to album */
-
-		album->tracks = realloc (album->tracks, sizeof(uint32_t) * (album->no_tracks+1));
-		album->tracks[album->no_tracks] = track->item_id;
-		album->no_tracks++;
-		rb_debug ("adding track ID %d to album ID %d; now %d tracks",
-			  track->item_id,
-			  album->album_id,
-			  album->no_tracks);
-
-		if (LIBMTP_Update_Album (priv->device, album) != 0) {
-			rb_debug ("LIBMTP_Update_Album failed..");
-			report_libmtp_errors (priv->device, FALSE);
-		}
-	} else {
-		/* add new album */
-		album = LIBMTP_new_album_t ();
-		album->name = strdup (album_name);
-		album->no_tracks = 1;
-		album->tracks = malloc (sizeof(uint32_t));
-		album->tracks[0] = track->item_id;
-		album->storage_id = track->storage_id;
-
-		/* fill in artist and genre? */
-
-		rb_debug ("creating new album (%s) for track ID %d", album->name, track->item_id);
-
-		if (LIBMTP_Create_New_Album (priv->device, album) != 0) {
-			LIBMTP_destroy_album_t (album);
-			rb_debug ("LIBMTP_Create_New_Album failed..");
-			report_libmtp_errors (priv->device, FALSE);
-		} else {
-			g_hash_table_insert (priv->album_map, album->name, album);
-		}
-	}
-}
-
-static void
-remove_track_from_album (RBMtpSource *source, const char *album_name, LIBMTP_track_t *track)
-{
-	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (source);
-	LIBMTP_album_t *album;
-	int i;
-
-	album = g_hash_table_lookup (priv->album_map, album_name);
-	if (album == NULL) {
-		rb_debug ("Couldn't find an album for %s", album_name);
-		return;
-	}
-
-	for (i = 0; i < album->no_tracks; i++) {
-		if (album->tracks[i] == track->item_id) {
-			break;
-		}
-	}
-
-	if (i == album->no_tracks) {
-		rb_debug ("Couldn't find track %d in album %d", track->item_id, album->album_id);
-		return;
-	}
-
-	memmove (album->tracks + i, album->tracks + i + 1, album->no_tracks - (i+1));
-	album->no_tracks--;
-
-	if (album->no_tracks == 0) {
-		rb_debug ("deleting empty album %d", album->album_id);
-		if (LIBMTP_Delete_Object (priv->device, album->album_id) != 0) {
-			report_libmtp_errors (priv->device, FALSE);
-		}
-		g_hash_table_remove (priv->album_map, album_name);
-	} else {
-		rb_debug ("updating album %d: %d tracks remaining", album->album_id, album->no_tracks);
-		if (LIBMTP_Update_Album (priv->device, album) != 0) {
-			report_libmtp_errors (priv->device, FALSE);
-		}
-	}
-}
-
-static void
 impl_delete (RBSource *source)
 {
 	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (source);
@@ -875,7 +757,6 @@ impl_delete (RBSource *source)
 	GList *tem;
 	RBEntryView *tracks;
 	RhythmDB *db;
-	int ret;
 
 	db = get_db_for_source (RB_MTP_SOURCE (source));
 
@@ -895,17 +776,11 @@ impl_delete (RBSource *source)
 			continue;
 		}
 
-		ret = LIBMTP_Delete_Object (priv->device, track->item_id);
-		if (ret != 0) {
-			rb_debug ("Delete track %d failed", track->item_id);
-			report_libmtp_errors (priv->device, TRUE);
-			continue;
-		}
-
 		album_name = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM);
 		if (strcmp (album_name, _("Unknown")) != 0) {
-			remove_track_from_album (RB_MTP_SOURCE (source), album_name, track);
+			rb_mtp_thread_remove_from_album (priv->device_thread, track, album_name);
 		}
+		rb_mtp_thread_delete_track (priv->device_thread, track);
 
 		g_hash_table_remove (priv->entry_map, entry);
 		rhythmdb_entry_delete (db, entry);
@@ -946,55 +821,41 @@ get_db_for_source (RBMtpSource *source)
 	return db;
 }
 
-static LIBMTP_track_t *
-transfer_track (RBMtpSource *source,
-		LIBMTP_mtpdevice_t *device,
-		RhythmDBEntry *entry,
-		const char *filename,
-		const char *mimetype)
-{
-	LIBMTP_track_t *trackmeta = LIBMTP_new_track_t ();
-	GDate d;
-	int ret;
-
-	trackmeta->title = rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_TITLE);
-	trackmeta->album = rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_ALBUM);
-	trackmeta->artist = rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_ARTIST);
-	trackmeta->genre = rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_GENRE);
-	trackmeta->filename = g_path_get_basename (filename);
-	rb_sanitize_path_for_msdos_filesystem (trackmeta->filename);
+typedef struct {
+	RBMtpSource *source;
+	RhythmDBEntry *entry;
+} RequestAlbumArtData;
 
-	if (rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DATE) > 0) { /* Entries without a date returns 0, g_date_set_julian don't accept that */
-		g_date_set_julian (&d, rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DATE));
-		trackmeta->date	= gdate_to_char (&d);
-	}
-	trackmeta->tracknumber = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_TRACK_NUMBER);
-	trackmeta->duration = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DURATION) * 1000;
-	trackmeta->rating = rhythmdb_entry_get_double (entry, RHYTHMDB_PROP_RATING) * 20;
-	trackmeta->usecount = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_PLAY_COUNT);
-	trackmeta->filesize = rhythmdb_entry_get_uint64 (entry, RHYTHMDB_PROP_FILE_SIZE);
-	if (mimetype == NULL) {
-		mimetype = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_MIMETYPE);
-	}
-	trackmeta->filetype = mimetype_to_filetype (source, mimetype);
-	rb_debug ("using libmtp filetype %d (%s) for source media type %s",
-		  trackmeta->filetype,
-		  LIBMTP_Get_Filetype_Description (trackmeta->filetype),
-		  mimetype);
+static gboolean
+request_album_art_idle (RequestAlbumArtData *data)
+{
+	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (data->source);
+	const char *album;
 
-	ret = LIBMTP_Send_Track_From_File (device, filename, trackmeta, NULL, NULL);
-	rb_debug ("LIBMTP_Send_Track_From_File (%s) returned %d", filename, ret);
-	if (ret != 0) {
-		report_libmtp_errors (device, TRUE);
-		LIBMTP_destroy_track_t (trackmeta);
-		return NULL;
-	}
+	/* pretty sure we don't need any extra locking here - we only touch the artwork
+	 * request map on the main thread anyway.
+	 */
 
-	if (strcmp (trackmeta->album, _("Unknown")) != 0) {
-		add_track_to_album (source, trackmeta->album, trackmeta);
+	album = rhythmdb_entry_get_string (data->entry, RHYTHMDB_PROP_ALBUM);
+	if (g_hash_table_lookup (priv->artwork_request_map, album) == NULL) {
+		GValue *metadata;
+		RhythmDB *db = get_db_for_source (data->source);
+
+		rb_debug ("requesting cover art image for album %s", album);
+		g_hash_table_insert (priv->artwork_request_map, (gpointer) album, GINT_TO_POINTER (1));
+		metadata = rhythmdb_entry_request_extra_metadata (db, data->entry, "rb:coverArt");
+		if (metadata) {
+			artwork_notify_cb (db, data->entry, "rb:coverArt", metadata, data->source);
+			g_value_unset (metadata);
+			g_free (metadata);
+		}
+		g_object_unref (db);
 	}
 
-	return trackmeta;
+	g_object_unref (data->source);
+	rhythmdb_entry_unref (data->entry);
+	g_free (data);
+	return FALSE;
 }
 
 static GList *
@@ -1005,50 +866,123 @@ impl_get_mime_types (RBRemovableMediaSource *source)
 }
 
 static gboolean
-impl_track_added (RBRemovableMediaSource *isource,
+impl_track_added (RBRemovableMediaSource *source,
 		  RhythmDBEntry *entry,
 		  const char *dest,
 		  const char *mimetype)
 {
-	RBMtpSource *source = RB_MTP_SOURCE (isource);
-	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (source);
-	GFile *file;
-	char *path;
 	LIBMTP_track_t *track = NULL;
+	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (source);
+	RhythmDB *db;
+	RhythmDBEntry *mtp_entry;
 
-	file = g_file_new_for_uri (dest);
-	path = g_file_get_path (file);
-	track = transfer_track (source, priv->device, entry, path, mimetype);
-	g_free (path);
+	track = g_hash_table_lookup (priv->track_transfer_map, dest);
+	if (track == NULL) {
+		rb_debug ("track-added called, but can't find a track for dest URI %s", dest);
+		return FALSE;
+	}
+	g_hash_table_remove (priv->track_transfer_map, dest);
 
-	g_file_delete (file, NULL, NULL);
+	db = get_db_for_source (RB_MTP_SOURCE (source));
+	/* entry_map takes ownership of the track here */
+	mtp_entry = add_mtp_track_to_db (RB_MTP_SOURCE (source), db, track);
+	g_object_unref (db);
+
+	if (strcmp (track->album, _("Unknown")) != 0) {
+		rb_mtp_thread_add_to_album (priv->device_thread, track, track->album);
+	}
 
+	if (priv->album_art_supported) {
+		RequestAlbumArtData *artdata;
+		artdata = g_new0 (RequestAlbumArtData, 1);
+		artdata->source = g_object_ref (source);
+		artdata->entry = rhythmdb_entry_ref (mtp_entry);
+		g_idle_add ((GSourceFunc) request_album_art_idle, artdata);
+	}
+	return FALSE;
+}
+
+static gboolean
+impl_track_add_error (RBRemovableMediaSource *source,
+		      RhythmDBEntry *entry,
+		      const char *dest,
+		      GError *error)
+{
+	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (source);
+	/* we don't actually do anything with the error here, we just need to clean up the transfer map */
+	LIBMTP_track_t *track = g_hash_table_lookup (priv->track_transfer_map, dest);
 	if (track != NULL) {
-		RhythmDB *db = get_db_for_source (source);
-
-		if (priv->album_art_supported) {
-			const char *album;
-
-			album = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM);
-			if (g_hash_table_lookup (priv->artwork_request_map, album) == NULL) {
-				GValue *metadata;
-
-				rb_debug ("requesting cover art image for album %s", album);
-				g_hash_table_insert (priv->artwork_request_map, (gpointer) album, GINT_TO_POINTER (1));
-				metadata = rhythmdb_entry_request_extra_metadata (db, entry, "rb:coverArt");
-				if (metadata) {
-					artwork_notify_cb (db, entry, "rb:coverArt", metadata, source);
-					g_value_unset (metadata);
-					g_free (metadata);
-				}
-			}
-		}
+		LIBMTP_destroy_track_t (track);
+		g_hash_table_remove (priv->track_transfer_map, dest);
+	} else {
+		rb_debug ("track-add-error called, but can't find a track for dest URI %s", dest);
+	}
+	return TRUE;
+}
 
-		add_mtp_track_to_db (source, db, track);
-		g_object_unref (db);
+static void
+prepare_encoder_sink_cb (RBEncoderFactory *factory,
+			 const char *stream_uri,
+			 GObject *sink,
+			 RBMtpSource *source)
+{
+	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (source);
+	RhythmDBEntry *entry;
+	RhythmDB *db;
+	LIBMTP_track_t *track;
+	char **bits;
+	char *extension;
+	LIBMTP_filetype_t filetype;
+	gulong track_id;
+	GDate d;
+
+	/* make sure this stream is for a file on our device */
+	if (g_str_has_prefix (stream_uri, "xrbmtp://") == FALSE)
+		return;
+
+	/* extract the entry ID, extension, and MTP filetype from the URI */
+	bits = g_strsplit (stream_uri + strlen ("xrbmtp://"), "/", 3);
+	track_id = strtoul (bits[0], NULL, 0);
+	extension = g_strdup (bits[1]);
+	filetype = strtoul (bits[2], NULL, 0);
+	g_strfreev (bits);
+
+	db = get_db_for_source (source);
+	entry = rhythmdb_entry_lookup_by_id (db, track_id);
+	g_object_unref (db);
+	if (entry == NULL)
+		return;
+
+	track = LIBMTP_new_track_t ();
+	track->title = rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_TITLE);
+	track->album = rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_ALBUM);
+	track->artist = rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_ARTIST);
+	track->genre = rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_GENRE);
+
+	/* build up device filename; may want to reconsider if we start creating folders */
+	track->filename = g_strdup_printf ("%s - %s.%s",
+					   rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST),
+					   rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE),
+					   extension);
+	g_free (extension);
+
+	rb_sanitize_path_for_msdos_filesystem (track->filename);
+
+	if (rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DATE) > 0) {
+		g_date_set_julian (&d, rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DATE));
+		track->date = gdate_to_char (&d);
 	}
+	track->tracknumber = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_TRACK_NUMBER);
+	track->duration = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DURATION) * 1000;
+	track->rating = rhythmdb_entry_get_double (entry, RHYTHMDB_PROP_RATING) * 20;
+	track->usecount = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_PLAY_COUNT);
 
-	return FALSE;
+	track->filetype = filetype;
+
+	g_object_set (sink, "device-thread", priv->device_thread, "mtp-track", track, NULL);
+	rhythmdb_entry_unref (entry);
+
+	g_hash_table_insert (priv->track_transfer_map, g_strdup (stream_uri), track);
 }
 
 static char *
@@ -1057,12 +991,34 @@ impl_build_dest_uri (RBRemovableMediaSource *source,
 		     const char *mimetype,
 		     const char *extension)
 {
-	char* file = g_strdup_printf ("%s/%s-%s.%s", g_get_tmp_dir (),
-				      rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_ARTIST),
-				      rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_TITLE),
-				      extension);
-	char* uri = g_filename_to_uri (file, NULL, NULL);
-	g_free (file);
+	gulong id;
+	char *uri;
+	LIBMTP_filetype_t filetype;
+
+	if (mimetype == NULL) {
+		mimetype = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_MIMETYPE);
+	}
+	filetype = mimetype_to_filetype (RB_MTP_SOURCE (source), mimetype);
+	rb_debug ("using libmtp filetype %d (%s) for source media type %s",
+		  filetype,
+		  LIBMTP_Get_Filetype_Description (filetype),
+		  mimetype);
+
+	/* the prepare-sink callback needs the entry ID to set up the
+	 * upload data, and we want to use the supplied extension for
+	 * the filename on the device.
+	 *
+	 * this is pretty ugly - it'd be much nicer to have a source-defined
+	 * structure that got passed around (or was accessible from) the various
+	 * hooks and methods called during the track transfer process.  probably
+	 * something to address in my horribly stalled track transfer rewrite..
+	 *
+	 * the structure would either be created when queuing the track for transfer,
+	 * or here; passed to any prepare-source or prepare-sink callbacks for the
+	 * encoder; and then passed to whatever gets called when the transfer is complete.
+	 */
+	id = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_ENTRY_ID);
+	uri = g_strdup_printf ("xrbmtp://%lu/%s/%d", id, extension, filetype);
 	return uri;
 }
 
@@ -1075,13 +1031,7 @@ artwork_notify_cb (RhythmDB *db,
 {
 	RBMtpSourcePrivate *priv = MTP_SOURCE_GET_PRIVATE (source);
 	GdkPixbuf *pixbuf;
-	LIBMTP_album_t *album;
-	LIBMTP_filesampledata_t *albumart;
-	GError *error = NULL;
 	const char *album_name;
-	char *image_data;
-	gsize image_size;
-	int ret;
 
 	album_name = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM);
 
@@ -1094,39 +1044,9 @@ artwork_notify_cb (RhythmDB *db,
 
 	pixbuf = GDK_PIXBUF (g_value_get_object (metadata));
 
-	/* we should already have created an album object */
-	album = g_hash_table_lookup (priv->album_map, album_name);
-	if (album == NULL) {
-		rb_debug ("couldn't find an album for %s", album_name);
-		return;
-	}
-
-	/* probably should scale the image down, since some devices have a size limit and they all have
-	 * tiny displays anyway.
-	 */
-
-	if (gdk_pixbuf_save_to_buffer (pixbuf, &image_data, &image_size, "jpeg", &error, NULL) == FALSE) {
-		rb_debug ("unable to convert album art image to a JPEG buffer: %s", error->message);
-		g_error_free (error);
-		return;
-	}
-
-	albumart = LIBMTP_new_filesampledata_t ();
-	albumart->filetype = LIBMTP_FILETYPE_JPEG;
-	albumart->data = image_data;
-	albumart->size = image_size;
-
-	ret = LIBMTP_Send_Representative_Sample (priv->device, album->album_id, albumart);
-	if (ret != 0) {
-		report_libmtp_errors (priv->device, TRUE);
-	} else {
-		rb_debug ("successfully set album art for %s (%" G_GSIZE_FORMAT " bytes)", album_name, image_size);
-	}
+	rb_mtp_thread_set_album_image (priv->device_thread, album_name, pixbuf);
 
-	/* libmtp will try to free this if we don't clear the pointer */
-	albumart->data = NULL;
-	LIBMTP_destroy_filesampledata_t (albumart);
-	g_free (image_data);
+	g_object_unref (pixbuf);		/* ? */
 }
 
 static void
@@ -1151,8 +1071,8 @@ prepare_source (RBMtpSource *source, const char *stream_uri, GObject *src)
 		return;
 	}
 
-	rb_debug ("setting device %p for stream %s", priv->device, stream_uri);
-	g_object_set (src, "device", priv->device, NULL);
+	rb_debug ("setting device-thread for stream %s", stream_uri);
+	g_object_set (src, "device-thread", priv->device_thread, NULL);
 	rhythmdb_entry_unref (entry);
 }
 
diff --git a/plugins/mtpdevice/rb-mtp-source.h b/plugins/mtpdevice/rb-mtp-source.h
index 7c08ab3..f49f985 100644
--- a/plugins/mtpdevice/rb-mtp-source.h
+++ b/plugins/mtpdevice/rb-mtp-source.h
@@ -55,9 +55,9 @@ typedef struct
 } RBMtpSourceClass;
 
 #if defined(HAVE_GUDEV)
-RBSource *		rb_mtp_source_new		(RBShell *shell, LIBMTP_mtpdevice_t *device);
+RBSource *		rb_mtp_source_new		(RBShell *shell, LIBMTP_raw_device_t *device);
 #else
-RBSource *		rb_mtp_source_new		(RBShell *shell, LIBMTP_mtpdevice_t *device, const char *udi);
+RBSource *		rb_mtp_source_new		(RBShell *shell, LIBMTP_raw_device_t *device, const char *udi);
 #endif
 
 GType			rb_mtp_source_get_type		(void);
diff --git a/plugins/mtpdevice/rb-mtp-thread.c b/plugins/mtpdevice/rb-mtp-thread.c
new file mode 100644
index 0000000..61c5fde
--- /dev/null
+++ b/plugins/mtpdevice/rb-mtp-thread.c
@@ -0,0 +1,809 @@
+/*
+ *  Copyright (C) 2009 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 <glib.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "rb-mtp-thread.h"
+#include "rb-file-helpers.h"
+#include "rb-dialog.h"
+#include "rb-debug.h"
+
+G_DEFINE_TYPE(RBMtpThread, rb_mtp_thread, G_TYPE_OBJECT)
+
+
+typedef struct {
+	enum {
+		OPEN_DEVICE = 1,
+		CLOSE_DEVICE,
+		SET_DEVICE_NAME,
+
+		ADD_TO_ALBUM,
+		REMOVE_FROM_ALBUM,
+		SET_ALBUM_IMAGE,
+
+		GET_TRACK_LIST,
+		DELETE_TRACK,
+		UPLOAD_TRACK,
+		DOWNLOAD_TRACK
+	} task;
+
+	LIBMTP_raw_device_t *raw_device;
+	LIBMTP_track_t *track;
+	uint32_t track_id;
+	uint32_t storage_id;
+	char *album;
+	char *filename;
+	GdkPixbuf *image;
+	char *name;
+
+	gpointer callback;
+	gpointer user_data;
+	GDestroyNotify destroy_data;
+} RBMtpThreadTask;
+
+static char *
+task_name (RBMtpThreadTask *task)
+{
+	switch (task->task) {
+	case OPEN_DEVICE:	return g_strdup ("open device");
+	case CLOSE_DEVICE:	return g_strdup ("close device");
+	case SET_DEVICE_NAME:	return g_strdup_printf ("set device name to %s", task->name);
+
+	case ADD_TO_ALBUM:	return g_strdup_printf ("add track %u to album %s", task->track_id, task->album);
+	case REMOVE_FROM_ALBUM:	return g_strdup_printf ("remove track %u from album %s", task->track_id, task->album);
+	case SET_ALBUM_IMAGE:	return g_strdup_printf ("set image for album %s", task->album);
+
+	case GET_TRACK_LIST:	return g_strdup ("get track list");
+	case DELETE_TRACK:	return g_strdup_printf ("delete track %u", task->track_id);
+	case UPLOAD_TRACK:	return g_strdup_printf ("upload track from %s", task->filename);
+	case DOWNLOAD_TRACK:	return g_strdup_printf ("download track %u to %s",
+							task->track_id,
+							task->filename[0] ? task->filename : "<temporary>");
+	default:		return g_strdup_printf ("unknown task type %d", task->task);
+	}
+}
+
+static RBMtpThreadTask *
+create_task (int tasktype)
+{
+	RBMtpThreadTask *task = g_slice_new0 (RBMtpThreadTask);
+	task->task = tasktype;
+	return task;
+}
+
+static void
+destroy_task (RBMtpThreadTask *task)
+{
+	/* don't think we ever own the track structure here;
+	 * we only have it for uploads, and then we pass it back
+	 * to the callback.
+	 */
+
+	g_free (task->album);
+	g_free (task->filename);
+	g_free (task->name);
+
+	if (task->image) {
+		g_object_unref (task->image);
+	}
+
+	if (task->destroy_data) {
+		task->destroy_data (task->user_data);
+	}
+
+	g_slice_free (RBMtpThreadTask, task);
+}
+
+
+static void
+queue_task (RBMtpThread *thread, RBMtpThreadTask *task)
+{
+	char *name = task_name (task);
+	rb_debug ("queueing task: %s", name);
+	g_free (name);
+
+	g_async_queue_push (thread->queue, task);
+}
+
+static void
+open_device (RBMtpThread *thread, RBMtpThreadTask *task)
+{
+	RBMtpOpenCallback cb = task->callback;
+
+	/* open the device */
+	thread->device = LIBMTP_Open_Raw_Device (task->raw_device);
+	if (thread->device == NULL) {
+		/* hm. any error information we can give here? */
+		/* maybe retry a few times since apparently that can help? */
+		cb (NULL, task->user_data);
+		return;
+	}
+
+	cb (thread->device, task->user_data);
+}
+
+static LIBMTP_album_t *
+add_track_to_album (RBMtpThread *thread, const char *album_name, uint32_t track_id, uint32_t storage_id, gboolean *new_album)
+{
+	LIBMTP_album_t *album;
+
+	album = g_hash_table_lookup (thread->albums, album_name);
+	if (album != NULL) {
+		/* add track to album */
+		album->tracks = realloc (album->tracks, sizeof(uint32_t) * (album->no_tracks+1));
+		album->tracks[album->no_tracks] = track_id;
+		album->no_tracks++;
+		rb_debug ("adding track ID %d to album ID %d; now has %d tracks",
+			  track_id,
+			  album->album_id,
+			  album->no_tracks);
+
+		if (new_album != NULL) {
+			*new_album = FALSE;
+		}
+	} else {
+		/* add new album */
+		album = LIBMTP_new_album_t ();
+		album->name = strdup (album_name);
+		album->no_tracks = 1;
+		album->tracks = malloc (sizeof(uint32_t));
+		album->tracks[0] = track_id;
+		album->storage_id = storage_id;
+
+		rb_debug ("creating new album (%s) for track ID %d", album->name, track_id);
+
+		g_hash_table_insert (thread->albums, album->name, album);
+		if (new_album != NULL) {
+			*new_album = TRUE;
+		}
+	}
+
+	return album;
+}
+
+static void
+write_album_to_device (RBMtpThread *thread, LIBMTP_album_t *album, gboolean new_album)
+{
+	if (new_album) {
+		if (LIBMTP_Create_New_Album (thread->device, album) != 0) {
+			LIBMTP_destroy_album_t (album);
+			rb_debug ("LIBMTP_Create_New_Album failed..");
+			rb_mtp_thread_report_errors (thread, FALSE);
+		}
+	} else {
+		if (LIBMTP_Update_Album (thread->device, album) != 0) {
+			rb_debug ("LIBMTP_Update_Album failed..");
+			rb_mtp_thread_report_errors (thread, FALSE);
+		}
+	}
+}
+
+static void
+add_track_to_album_and_update (RBMtpThread *thread, RBMtpThreadTask *task)
+{
+	LIBMTP_album_t *album;
+	gboolean new_album = FALSE;
+
+	album = add_track_to_album (thread, task->album, task->track_id, task->storage_id, &new_album);
+	write_album_to_device (thread, album, new_album);
+}
+
+static void
+remove_track_from_album (RBMtpThread *thread, RBMtpThreadTask *task)
+{
+	LIBMTP_album_t *album;
+	int i;
+
+	album = g_hash_table_lookup (thread->albums, task->album);
+	if (album == NULL) {
+		rb_debug ("Couldn't find an album for %s", task->album);
+		return;
+	}
+
+	for (i = 0; i < album->no_tracks; i++) {
+		if (album->tracks[i] == task->track_id) {
+			break;
+		}
+	}
+
+	if (i == album->no_tracks) {
+		rb_debug ("Couldn't find track %d in album %d", task->track_id, album->album_id);
+		return;
+	}
+
+	memmove (album->tracks + i, album->tracks + i + 1, album->no_tracks - (i+1));
+	album->no_tracks--;
+
+	if (album->no_tracks == 0) {
+		rb_debug ("deleting empty album %d", album->album_id);
+		if (LIBMTP_Delete_Object (thread->device, album->album_id) != 0) {
+			rb_mtp_thread_report_errors (thread, FALSE);
+		}
+		g_hash_table_remove (thread->albums, task->album);
+	} else {
+		rb_debug ("updating album %d: %d tracks remaining", album->album_id, album->no_tracks);
+		if (LIBMTP_Update_Album (thread->device, album) != 0) {
+			rb_mtp_thread_report_errors (thread, FALSE);
+		}
+	}
+}
+
+static void
+set_album_image (RBMtpThread *thread, RBMtpThreadTask *task)
+{
+	LIBMTP_filesampledata_t *albumart;
+	LIBMTP_album_t *album;
+	GError *error = NULL;
+	char *image_data;
+	gsize image_size;
+	int ret;
+	
+	album = g_hash_table_lookup (thread->albums, task->album);
+	if (album == NULL) {
+		rb_debug ("Couldn't find an album for %s", task->album);
+		return;
+	}
+	
+	/* probably should scale the image down, since some devices have a size limit and they all have
+	 * tiny displays anyway.
+	 */
+
+	if (gdk_pixbuf_save_to_buffer (task->image, &image_data, &image_size, "jpeg", &error, NULL) == FALSE) {
+		rb_debug ("unable to convert album art image to a JPEG buffer: %s", error->message);
+		g_error_free (error);
+		return;
+	}
+
+	albumart = LIBMTP_new_filesampledata_t ();
+	albumart->filetype = LIBMTP_FILETYPE_JPEG;
+	albumart->data = image_data;
+	albumart->size = image_size;
+
+	ret = LIBMTP_Send_Representative_Sample (thread->device, album->album_id, albumart);
+	if (ret != 0) {
+		rb_mtp_thread_report_errors (thread, TRUE);
+	} else {
+		rb_debug ("successfully set album art for %s (%" G_GSIZE_FORMAT " bytes)", task->album, image_size);
+	}
+
+	/* libmtp will try to free this if we don't clear the pointer */
+	albumart->data = NULL;
+	LIBMTP_destroy_filesampledata_t (albumart);
+}
+
+static void
+get_track_list (RBMtpThread *thread, RBMtpThreadTask *task)
+{
+	RBMtpTrackListCallback cb = task->callback;
+	gboolean device_forgets_albums = TRUE;
+	GHashTable *update_albums = NULL;
+	LIBMTP_track_t *tracks = NULL;
+	LIBMTP_album_t *albums;
+	LIBMTP_album_t *album;
+
+	/* get all the albums */
+	albums = LIBMTP_Get_Album_List (thread->device);
+	rb_mtp_thread_report_errors (thread, FALSE);
+	if (albums != NULL) {
+		LIBMTP_album_t *album;
+
+		for (album = albums; album != NULL; album = album->next) {
+			if (album->name == NULL)
+				continue;
+
+			rb_debug ("album: %s, %d tracks", album->name, album->no_tracks);
+			g_hash_table_insert (thread->albums, album->name, album);
+			if (album->no_tracks != 0) {
+				device_forgets_albums = FALSE;
+			}
+		}
+
+		if (device_forgets_albums) {
+			rb_debug ("stupid mtp device detected.  will rebuild all albums.");
+		}
+	} else {
+		rb_debug ("No albums");
+		device_forgets_albums = FALSE;
+	}
+
+	tracks = LIBMTP_Get_Tracklisting_With_Callback (thread->device, NULL, NULL);
+	rb_mtp_thread_report_errors (thread, FALSE);
+	if (tracks == NULL) {
+		rb_debug ("no tracks on the device");
+	} else if (device_forgets_albums) {
+		LIBMTP_track_t *track;
+	       
+		rb_debug ("rebuilding albums");
+		update_albums = g_hash_table_new (g_direct_hash, g_direct_equal);
+		for (track = tracks; track != NULL; track = track->next) {
+			if (track->album != NULL) {
+				gboolean new_album = FALSE;
+				album = add_track_to_album (thread, track->album, track->item_id, track->storage_id, &new_album);
+				g_hash_table_insert (update_albums, album, GINT_TO_POINTER (new_album));
+			}
+		}
+		rb_debug ("finished rebuilding albums");
+	}
+
+	cb (tracks, task->user_data);
+	/* the callback owns the tracklist */
+
+	if (device_forgets_albums) {
+		GHashTableIter iter;
+		gpointer album_ptr;
+		gpointer new_album_ptr;
+
+		rb_debug ("writing rebuilt albums back to the device");
+		g_hash_table_iter_init (&iter, update_albums);
+		while (g_hash_table_iter_next (&iter, &album_ptr, &new_album_ptr)) {
+			album = album_ptr;
+			rb_debug ("writing album \"%s\"", album->name);
+			write_album_to_device (thread, album, GPOINTER_TO_INT (new_album_ptr));
+		}
+		g_hash_table_destroy (update_albums);
+
+		rb_debug ("removing remaining empty albums");
+		g_hash_table_iter_init (&iter, thread->albums);
+		while (g_hash_table_iter_next (&iter, NULL, &album_ptr)) {
+			int ret;
+
+			album = album_ptr;
+			if (album->no_tracks == 0) {
+				rb_debug ("pruning empty album \"%s\"", album->name); 
+				ret = LIBMTP_Delete_Object (thread->device, album->album_id);
+				if (ret != 0) {
+					rb_mtp_thread_report_errors (thread, FALSE);
+				}
+				g_hash_table_iter_remove (&iter);
+			}
+		}
+
+		rb_debug ("finished updating albums on the device");
+	}
+}
+
+static void
+download_track (RBMtpThread *thread, RBMtpThreadTask *task)
+{
+	LIBMTP_file_t *fileinfo;
+	LIBMTP_error_t *stack;
+	GError *error = NULL;
+	GFile *dir;
+	RBMtpDownloadCallback cb = (RBMtpDownloadCallback) task->callback;
+
+	/* first, check there's enough space to copy it */
+	fileinfo = LIBMTP_Get_Filemetadata (thread->device, task->track_id);
+	if (fileinfo == NULL) {
+		stack = LIBMTP_Get_Errorstack (thread->device);
+		rb_debug ("unable to get track metadata for %u: %s", task->track_id, stack->error_text);
+		error = g_error_new (RB_MTP_THREAD_ERROR,
+				     RB_MTP_THREAD_ERROR_GET_TRACK,
+				     _("Unable to copy file from MTP device: %s"),
+				     stack->error_text);
+		LIBMTP_Clear_Errorstack (thread->device);
+
+		cb (task->track_id, NULL, error, task->user_data);
+		g_error_free (error);
+		return;
+	}
+
+	if (task->filename[0] == '\0') {
+		dir = g_file_new_for_path (g_get_tmp_dir ());
+	} else {
+		GFile *file = g_file_new_for_path (task->filename);
+		dir = g_file_get_parent (file);
+		g_object_unref (file);
+	}
+	rb_debug ("checking for %" G_GINT64_FORMAT " bytes available", fileinfo->filesize);
+	if (rb_check_dir_has_space (dir, fileinfo->filesize) == FALSE) {
+		char *dpath = g_file_get_path (dir);
+		rb_debug ("not enough space in %s", dpath);
+		error = g_error_new (RB_MTP_THREAD_ERROR, RB_MTP_THREAD_ERROR_NO_SPACE,
+				     _("Not enough space in %s"), dpath);
+		g_free (dpath);
+	}
+	LIBMTP_destroy_file_t (fileinfo);
+	g_object_unref (dir);
+
+	if (error != NULL) {
+		rb_debug ("bailing out due to error: %s", error->message);
+		cb (task->track_id, NULL, error, task->user_data);
+		g_error_free (error);
+		return;
+	}
+
+	if (task->filename[0] == '\0') {
+		/* download to a temporary file */
+		int fd;
+		GError *tmperror = NULL;
+
+		g_free (task->filename);
+		fd = g_file_open_tmp ("rb-mtp-temp-XXXXXX", &task->filename, &tmperror);
+		if (fd == -1) {
+			rb_debug ("unable to open temporary file: %s", tmperror->message);
+			error = g_error_new (RB_MTP_THREAD_ERROR,
+					     RB_MTP_THREAD_ERROR_TEMPFILE,
+					     _("Unable to open temporary file: %s"),
+					     tmperror->message);
+			g_error_free (tmperror);
+
+			cb (task->track_id, NULL, error, task->user_data);
+			g_error_free (error);
+			return;
+		} else {
+			rb_debug ("downloading track %u to file descriptor %d", task->track_id, fd);
+			if (LIBMTP_Get_Track_To_File_Descriptor (thread->device, task->track_id, fd, NULL, NULL)) {
+				stack = LIBMTP_Get_Errorstack (thread->device);
+				rb_debug ("unable to retrieve track %u: %s", task->track_id, stack->error_text);
+				error = g_error_new (RB_MTP_THREAD_ERROR, RB_MTP_THREAD_ERROR_GET_TRACK,
+						     _("Unable to copy file from MTP device: %s"),
+						     stack->error_text);
+				LIBMTP_Clear_Errorstack (thread->device);
+
+				cb (task->track_id, NULL, error, task->user_data);
+				g_error_free (error);
+				close (fd);
+				remove (task->filename);
+				return;
+			}
+			rb_debug ("done downloading track");
+
+			close (fd);
+		}
+	} else {
+		if (LIBMTP_Get_Track_To_File (thread->device, task->track_id, task->filename, NULL, NULL)) {
+			stack = LIBMTP_Get_Errorstack (thread->device);
+			error = g_error_new (RB_MTP_THREAD_ERROR, RB_MTP_THREAD_ERROR_GET_TRACK,
+					     _("Unable to copy file from MTP device: %s"),
+					     stack->error_text);
+			LIBMTP_Clear_Errorstack (thread->device);
+
+			cb (task->track_id, NULL, error, task->user_data);
+			g_error_free (error);
+			return;
+		}
+	}
+
+	cb (task->track_id, task->filename, NULL, task->user_data);
+}
+
+static void
+upload_track (RBMtpThread *thread, RBMtpThreadTask *task)
+{
+	RBMtpUploadCallback cb = (RBMtpUploadCallback) task->callback;
+	LIBMTP_error_t *stack;
+	GError *error = NULL;
+
+	if (LIBMTP_Send_Track_From_File (thread->device, task->filename, task->track, NULL, NULL)) {
+		stack = LIBMTP_Get_Errorstack (thread->device);
+		rb_debug ("unable to send track: %s", stack->error_text);
+		error = g_error_new (RB_MTP_THREAD_ERROR, RB_MTP_THREAD_ERROR_SEND_TRACK,
+				     _("Unable to send file to MTP device: %s"),
+				     stack->error_text);
+		LIBMTP_Clear_Errorstack (thread->device);
+		task->track->item_id = 0;		/* is this actually an invalid item ID? */
+	}
+	cb (task->track, error, task->user_data);
+	g_clear_error (&error);
+}
+
+static gboolean 
+run_task (RBMtpThread *thread, RBMtpThreadTask *task)
+{
+	char *name = task_name (task);
+	rb_debug ("running task: %s", name);
+	g_free (name);
+
+	switch (task->task) {
+	case OPEN_DEVICE:
+		open_device (thread, task);
+		break;
+
+	case CLOSE_DEVICE:
+		return TRUE;
+
+	case SET_DEVICE_NAME:
+		if (LIBMTP_Set_Friendlyname (thread->device, task->name)) {
+			rb_mtp_thread_report_errors (thread, TRUE);
+		}
+		break;
+
+	case ADD_TO_ALBUM:
+		add_track_to_album_and_update (thread, task);
+		break;
+
+	case REMOVE_FROM_ALBUM:
+		remove_track_from_album (thread, task);
+		break;
+
+	case SET_ALBUM_IMAGE:
+		set_album_image (thread, task);
+		break;
+
+	case GET_TRACK_LIST:
+		get_track_list (thread, task);
+		break;
+
+	case DELETE_TRACK:
+		if (LIBMTP_Delete_Object (thread->device, task->track_id)) {
+			rb_mtp_thread_report_errors (thread, TRUE);
+		}
+		break;
+
+	case UPLOAD_TRACK:
+		upload_track (thread, task);
+		break;
+
+	case DOWNLOAD_TRACK:
+		download_track (thread, task);
+		break;
+
+	default:
+		g_assert_not_reached ();
+	}
+
+	return FALSE;
+}
+
+static gpointer
+task_thread (RBMtpThread *thread)
+{
+	RBMtpThreadTask *task;
+	gboolean quit = FALSE;
+
+	rb_debug ("MTP device worker thread starting");
+	while (quit == FALSE) {
+
+		task = g_async_queue_pop (thread->queue);
+		quit = run_task (thread, task);
+		destroy_task (task);
+	}
+
+	rb_debug ("MTP device worker thread exiting");
+	
+	/* clean up any queued tasks */
+	while ((task = g_async_queue_try_pop (thread->queue)) != NULL)
+		destroy_task (task);
+
+	return NULL;
+}
+
+/* callable interface */
+
+void
+rb_mtp_thread_open_device (RBMtpThread *thread,
+			   LIBMTP_raw_device_t *raw_device,
+			   RBMtpOpenCallback callback,
+			   gpointer data,
+			   GDestroyNotify destroy_data)
+{
+	RBMtpThreadTask *task = create_task (OPEN_DEVICE);
+	task->raw_device = raw_device;
+	task->callback = callback;
+	task->user_data = data;
+	task->destroy_data = destroy_data;
+	queue_task (thread, task);
+}
+
+void
+rb_mtp_thread_set_device_name (RBMtpThread *thread, const char *name)
+{
+	RBMtpThreadTask *task = create_task (SET_DEVICE_NAME);
+	task->name = g_strdup (name);
+	queue_task (thread, task);
+}
+
+void
+rb_mtp_thread_add_to_album (RBMtpThread *thread, LIBMTP_track_t *track, const char *album)
+{
+	RBMtpThreadTask *task = create_task (ADD_TO_ALBUM);
+	task->track_id = track->item_id;
+	task->storage_id = track->storage_id;
+	task->album = g_strdup (album);
+	queue_task (thread, task);
+}
+
+void
+rb_mtp_thread_remove_from_album (RBMtpThread *thread, LIBMTP_track_t *track, const char *album)
+{
+	RBMtpThreadTask *task = create_task (REMOVE_FROM_ALBUM);
+	task->track_id = track->item_id;
+	task->storage_id = track->storage_id;
+	task->album = g_strdup (album);
+	queue_task (thread, task);
+}
+
+void
+rb_mtp_thread_set_album_image (RBMtpThread *thread, const char *album, GdkPixbuf *image)
+{
+	RBMtpThreadTask *task = create_task (SET_ALBUM_IMAGE);
+	task->album = g_strdup (album);
+	task->image = g_object_ref (image);
+	queue_task (thread, task);
+}
+
+void
+rb_mtp_thread_get_track_list (RBMtpThread *thread,
+			      RBMtpTrackListCallback callback,
+			      gpointer data,
+			      GDestroyNotify destroy_data)
+{
+	RBMtpThreadTask *task = create_task (GET_TRACK_LIST);
+	task->callback = callback;
+	task->user_data = data;
+	task->destroy_data = destroy_data;
+	queue_task (thread, task);
+}
+
+void
+rb_mtp_thread_delete_track (RBMtpThread *thread, LIBMTP_track_t *track)
+{
+	RBMtpThreadTask *task = create_task (DELETE_TRACK);
+	task->track_id = track->item_id;
+	task->storage_id = track->storage_id;
+	queue_task (thread, task);
+}
+
+void
+rb_mtp_thread_upload_track (RBMtpThread *thread,
+			    LIBMTP_track_t *track,
+			    const char *filename,
+			    RBMtpUploadCallback func,
+			    gpointer data,
+			    GDestroyNotify destroy_data)
+{
+	RBMtpThreadTask *task = create_task (UPLOAD_TRACK);
+	task->track = track;
+	task->filename = g_strdup (filename);
+	task->callback = func;
+	task->user_data = data;
+	task->destroy_data = destroy_data;
+	queue_task (thread, task);
+}
+
+void
+rb_mtp_thread_download_track (RBMtpThread *thread,
+			      uint32_t track_id,
+			      const char *filename,
+			      RBMtpDownloadCallback func,
+			      gpointer data,
+			      GDestroyNotify destroy_data)
+{
+	RBMtpThreadTask *task = create_task (DOWNLOAD_TRACK);
+	task->track_id = track_id;
+	task->filename = g_strdup (filename);
+	task->callback = func;
+	task->user_data = data;
+	task->destroy_data = destroy_data;
+	queue_task (thread, task);
+}
+
+void
+rb_mtp_thread_report_errors (RBMtpThread *thread, gboolean use_dialog)
+{
+	LIBMTP_error_t *stack;
+
+	for (stack = LIBMTP_Get_Errorstack (thread->device); stack != NULL; stack = stack->next) {
+		if (use_dialog) {
+			GDK_THREADS_ENTER ();
+			rb_error_dialog (NULL, _("Media player device error"), "%s", stack->error_text);
+			GDK_THREADS_LEAVE ();
+
+			/* only display one dialog box per error */
+			use_dialog = FALSE;
+		} else {
+			g_warning ("libmtp error: %s", stack->error_text);
+		}
+	}
+
+	LIBMTP_Clear_Errorstack (thread->device);
+}
+
+/* GObject things */
+
+static void
+impl_finalize (GObject *object)
+{
+	RBMtpThread *thread = RB_MTP_THREAD (object);
+	RBMtpThreadTask *task;
+
+	rb_debug ("joining MTP worker thread");
+	task = create_task (CLOSE_DEVICE);
+	queue_task (thread, task);
+	g_thread_join (thread->thread);
+	rb_debug ("MTP worker thread exited");
+
+	g_async_queue_unref (thread->queue);
+
+	g_hash_table_destroy (thread->albums);
+
+	LIBMTP_Release_Device (thread->device);
+
+	G_OBJECT_CLASS (rb_mtp_thread_parent_class)->finalize (object);
+}
+
+static void
+rb_mtp_thread_init (RBMtpThread *thread)
+{
+	thread->queue = g_async_queue_new ();
+	
+	thread->albums = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify) LIBMTP_destroy_album_t);
+
+	thread->thread = g_thread_create ((GThreadFunc) task_thread, thread, TRUE, NULL);		/* XXX should handle errors i guess */
+}
+
+static void
+rb_mtp_thread_class_init (RBMtpThreadClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+	object_class->finalize = impl_finalize;
+}
+
+RBMtpThread *
+rb_mtp_thread_new (void)
+{
+	return RB_MTP_THREAD (g_object_new (RB_TYPE_MTP_THREAD, NULL));
+}
+
+GQuark
+rb_mtp_thread_error_quark (void)
+{
+	static GQuark quark = 0;
+	if (!quark)
+		quark = g_quark_from_static_string ("rb_mtp_thread_error");
+
+	return quark;
+}
+
+#define ENUM_ENTRY(NAME, DESC) { NAME, "" #NAME "", DESC }
+
+GType
+rb_mtp_thread_error_get_type (void)
+{
+	static GType etype = 0;
+
+	if (etype == 0)	{
+		static const GEnumValue values[] = {
+			ENUM_ENTRY (RB_MTP_THREAD_ERROR_NO_SPACE, "Not enough space to download track"),
+			ENUM_ENTRY (RB_MTP_THREAD_ERROR_TEMPFILE, "Unable to create temporary file"),
+			ENUM_ENTRY (RB_MTP_THREAD_ERROR_GET_TRACK, "Unable to retrieve track"),
+			{ 0, 0, 0 }
+		};
+
+		etype = g_enum_register_static ("RBMTPThreadError", values);
+	}
+
+	return etype;
+}
+
diff --git a/plugins/mtpdevice/rb-mtp-thread.h b/plugins/mtpdevice/rb-mtp-thread.h
new file mode 100644
index 0000000..c03597a
--- /dev/null
+++ b/plugins/mtpdevice/rb-mtp-thread.h
@@ -0,0 +1,125 @@
+/*
+ *  Copyright (C) 2009 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 <libmtp.h>
+
+#include <gio/gio.h>
+#include <glib.h>
+#include <glib-object.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+/*
+ * Worker thread for dealing with MTP devices.
+ * libmtp isn't thread-aware, and some operations are pretty slow
+ * (getting track listings, sending and retrieving tracks), so
+ * we do everything except the initial setup in a dedicated thread.
+ */
+
+#ifndef __RB_MTP_THREAD_H
+#define __RB_MTP_THREAD_H
+
+typedef enum
+{
+	RB_MTP_THREAD_ERROR_NO_SPACE,
+	RB_MTP_THREAD_ERROR_TEMPFILE,
+	RB_MTP_THREAD_ERROR_GET_TRACK,
+	RB_MTP_THREAD_ERROR_SEND_TRACK
+} RBMtpThreadError;
+
+GType rb_mtp_thread_error_get_type (void);
+#define RB_TYPE_MTP_THREAD_ERROR (rb_mtp_thread_error_get_type ())
+GQuark rb_mtp_thread_error_quark (void);
+#define RB_MTP_THREAD_ERROR rb_mtp_thread_error_quark ()
+
+#define RB_TYPE_MTP_THREAD         (rb_mtp_thread_get_type ())
+#define RB_MTP_THREAD(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_MTP_THREAD, RBMtpThread))
+#define RB_MTP_THREAD_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_MTP_THREAD, RBMtpThreadClass))
+#define RB_IS_MTP_THREAD(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_MTP_THREAD))
+#define RB_IS_MTP_THREAD_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_MTP_THREAD))
+#define RB_MTP_THREAD_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_MTP_THREAD, RBMtpThreadClass))
+
+typedef struct
+{
+	GObject parent;
+	LIBMTP_mtpdevice_t *device;
+	GHashTable *albums;
+
+	GThread *thread;
+	GAsyncQueue *queue;
+} RBMtpThread;
+
+typedef struct
+{
+	GObjectClass parent;
+} RBMtpThreadClass;
+
+/* callback types */
+typedef void (*RBMtpOpenCallback) (LIBMTP_mtpdevice_t *device, gpointer user_data);
+typedef void (*RBMtpTrackListCallback) (LIBMTP_track_t *tracklist, gpointer user_data);
+typedef void (*RBMtpUploadCallback) (LIBMTP_track_t *track, GError *error, gpointer user_data);
+typedef void (*RBMtpDownloadCallback) (uint32_t track_id, const char *filename, GError *error, gpointer user_data);
+typedef void (*RBMtpCreatePlaylistCallback) (LIBMTP_playlist_t *playlist, gpointer user_data);
+
+GType		rb_mtp_thread_get_type (void);
+RBMtpThread *	rb_mtp_thread_new (void);
+
+void		rb_mtp_thread_report_errors (RBMtpThread *thread, gboolean use_dialog);
+
+void		rb_mtp_thread_open_device (RBMtpThread *thread,
+					   LIBMTP_raw_device_t *raw_device,
+					   RBMtpOpenCallback func,
+					   gpointer data,
+					   GDestroyNotify destroy_data);
+void		rb_mtp_thread_get_track_list (RBMtpThread *thread,
+					      RBMtpTrackListCallback func,
+					      gpointer data,
+					      GDestroyNotify destroy_data);
+
+void		rb_mtp_thread_set_device_name (RBMtpThread *thread, const char *name);
+
+/* albums */
+void		rb_mtp_thread_add_to_album (RBMtpThread *thread, LIBMTP_track_t *track, const char *album);
+void		rb_mtp_thread_remove_from_album (RBMtpThread *thread, LIBMTP_track_t *track, const char *album);
+void		rb_mtp_thread_set_album_image (RBMtpThread *thread, const char *album, GdkPixbuf *image);
+
+/* tracks */
+void		rb_mtp_thread_delete_track (RBMtpThread *thread, LIBMTP_track_t *track);
+void		rb_mtp_thread_upload_track (RBMtpThread *thread,
+					    LIBMTP_track_t *track,
+					    const char *filename,
+					    RBMtpUploadCallback func,
+					    gpointer data,
+					    GDestroyNotify destroy_data);
+void		rb_mtp_thread_download_track (RBMtpThread *thread,
+					      uint32_t track_id,
+					      const char *filename,
+					      RBMtpDownloadCallback func,
+					      gpointer data,
+					      GDestroyNotify destroy_data);
+
+#endif
+
diff --git a/po/POTFILES.in b/po/POTFILES.in
index af7e440..7b1a755 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -112,6 +112,7 @@ plugins/magnatune/magnatune/__init__.py
 plugins/mtpdevice/rb-mtp-gst-src.c
 plugins/mtpdevice/rb-mtp-plugin.c
 plugins/mtpdevice/rb-mtp-source.c
+plugins/mtpdevice/rb-mtp-thread.c
 [type: gettext/ini]plugins/power-manager/power-manager.rb-plugin.in
 plugins/power-manager/rb-power-manager-plugin.c
 plugins/pythonconsole/pythonconsole.py



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