[rhythmbox/media-player-sync: 1/3] media-player: implement syncing of music, podcasts, and playlists



commit d0288a3ffba08fe6a67fa2f45d2ae156f2bc43f3
Author: Jonathan Matthew <jonathan d14n org>
Date:   Sun Dec 27 19:21:12 2009 +1000

    media-player: implement syncing of music, podcasts, and playlists
    
    The sync UI presents options for syncing all music, all podcasts,
    or individual playlists and podcast feeds.  This is displayed on a
    new 'sync' tab in the media player source properties dialog.
    
    The sync process works by generating track identifiers (based on
    several track metadata fields) for all tracks on the device, and
    all tracks from the selected local playlists and podcasts.  It
    compares the sets of track identifiers to determine which tracks
    need to be transferred to the device, and which need to be deleted
    from it.  After updating the files on the device, it then
    reconstructs playlists on the device to match the local playlists
    selected for syncing.
    
    Sync settings are stored in a keyfile named after the device serial
    number.  The settings consist of sync categories (such as playlists
    and podcasts) and sync groups within categories (such as individual
    playlists or podcast feeds).
    
    This is based on Paul Bellamy's summer of code project.

 data/ui/media-player-properties.ui      |  127 ++++
 sources/Makefile.am                     |    2 +
 sources/rb-media-player-source.c        |  994 +++++++++++++++++++++++++++++++
 sources/rb-media-player-source.h        |   20 +
 sources/rb-media-player-sync-settings.c |  392 ++++++++++++
 sources/rb-media-player-sync-settings.h |   94 +++
 6 files changed, 1629 insertions(+), 0 deletions(-)
---
diff --git a/data/ui/media-player-properties.ui b/data/ui/media-player-properties.ui
index 5194cd8..69a827c 100644
--- a/data/ui/media-player-properties.ui
+++ b/data/ui/media-player-properties.ui
@@ -98,6 +98,133 @@
                 <property name="tab_fill">False</property>
               </packing>
             </child>
+            <child>
+              <object class="GtkVBox" id="vbox2">
+                <property name="visible">True</property>
+                <property name="border_width">12</property>
+                <property name="orientation">vertical</property>
+                <child>
+                  <object class="GtkFrame" id="frame4">
+                    <property name="visible">True</property>
+                    <property name="label_xalign">0</property>
+                    <property name="shadow_type">none</property>
+                    <child>
+                      <object class="GtkTable" id="table-content3">
+                        <property name="visible">True</property>
+                        <property name="border_width">12</property>
+                        <property name="n_rows">4</property>
+                        <property name="column_spacing">12</property>
+                        <property name="row_spacing">6</property>
+                        <child>
+                          <object class="GtkScrolledWindow" id="scrolledwindow1">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="hscrollbar_policy">automatic</property>
+                            <property name="vscrollbar_policy">automatic</property>
+                            <child>
+                              <object class="GtkTreeView" id="treeview-sync">
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="headers_visible">False</property>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="top_attach">3</property>
+                            <property name="bottom_attach">4</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkCheckButton" id="checkbutton-sync-music-all">
+                            <property name="label" translatable="yes">Sync All Music</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="draw_indicator">True</property>
+                          </object>
+                          <packing>
+                            <property name="top_attach">1</property>
+                            <property name="bottom_attach">2</property>
+                            <property name="x_options">GTK_FILL</property>
+                            <property name="y_options"></property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkCheckButton" id="checkbutton-sync-podcasts-all">
+                            <property name="label" translatable="yes">Sync All Podcasts</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="draw_indicator">True</property>
+                          </object>
+                          <packing>
+                            <property name="top_attach">2</property>
+                            <property name="bottom_attach">3</property>
+                            <property name="x_options">GTK_FILL</property>
+                            <property name="y_options"></property>
+                          </packing>
+                        </child>
+                        <child>
+                          <placeholder/>
+                        </child>
+                      </object>
+                    </child>
+                    <child type="label">
+                      <object class="GtkLabel" id="label-frame-sync">
+                        <property name="visible">True</property>
+                        <property name="label" translatable="yes">&lt;b&gt;Sync Preferences&lt;/b&gt;</property>
+                        <property name="use_markup">True</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkFrame" id="frame7">
+                    <property name="visible">True</property>
+                    <property name="label_xalign">0</property>
+                    <property name="shadow_type">none</property>
+                    <child>
+                      <object class="GtkAlignment" id="alignment1">
+                        <property name="visible">True</property>
+                        <property name="left_padding">12</property>
+                        <child>
+                          <object class="GtkProgressBar" id="progressbar-sync-preview">
+                            <property name="visible">True</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child type="label">
+                      <object class="GtkLabel" id="label1">
+                        <property name="visible">True</property>
+                        <property name="label" translatable="yes">&lt;b&gt;Sync Preview&lt;/b&gt;</property>
+                        <property name="use_markup">True</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child type="tab">
+              <object class="GtkLabel" id="label-notebook-sync">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Sync</property>
+              </object>
+              <packing>
+                <property name="position">1</property>
+                <property name="tab_fill">False</property>
+              </packing>
+            </child>
           </object>
           <packing>
             <property name="position">1</property>
diff --git a/sources/Makefile.am b/sources/Makefile.am
index 1a316b5..413ab76 100644
--- a/sources/Makefile.am
+++ b/sources/Makefile.am
@@ -28,6 +28,8 @@ libsourcesimpl_la_SOURCES =		\
 	rb-removable-media-source.h	\
 	rb-media-player-source.c	\
 	rb-media-player-source.h	\
+	rb-media-player-sync-settings.c	\
+	rb-media-player-sync-settings.h	\
 	rb-playlist-source.c            \
 	rb-playlist-source.h		\
 	rb-playlist-xml.h		\
diff --git a/sources/rb-media-player-source.c b/sources/rb-media-player-source.c
index 9e27d3a..a2c953d 100644
--- a/sources/rb-media-player-source.c
+++ b/sources/rb-media-player-source.c
@@ -36,15 +36,31 @@
 
 #include "rb-shell.h"
 #include "rb-media-player-source.h"
+#include "rb-media-player-sync-settings.h"
 #include "rb-dialog.h"
 #include "rb-debug.h"
 #include "rb-file-helpers.h"
 #include "rb-builder-helpers.h"
+#include "rb-playlist-manager.h"
+#include "rb-podcast-manager.h"
 #include "rb-util.h"
 
 typedef struct {
+	RBMediaPlayerSyncSettings *sync_settings;
+
+	GtkActionGroup *action_group;
+	GtkAction *sync_action;
+
 	/* properties dialog bits */
 	GtkDialog *properties_dialog;
+	GtkTreeStore *sync_tree_store;
+	GtkWidget *preview_bar;
+
+	/* sync state */
+	gboolean sync_needs_update;
+	guint64 sync_space_needed;
+	GList *sync_to_add;
+	GList *sync_to_remove;
 
 } RBMediaPlayerSourcePrivate;
 
@@ -54,6 +70,7 @@ G_DEFINE_TYPE (RBMediaPlayerSource, rb_media_player_source, RB_TYPE_REMOVABLE_ME
 
 static void rb_media_player_source_class_init (RBMediaPlayerSourceClass *klass);
 static void rb_media_player_source_init (RBMediaPlayerSource *source);
+static void rb_media_player_source_dispose (GObject *object);
 
 static void rb_media_player_source_set_property (GObject *object,
 					 guint prop_id,
@@ -63,6 +80,28 @@ static void rb_media_player_source_get_property (GObject *object,
 					 guint prop_id,
 					 GValue *value,
 					 GParamSpec *pspec);
+static void rb_media_player_source_constructed (GObject *object);
+
+static gboolean rb_media_player_source_track_added (RBRemovableMediaSource *source,
+						    RhythmDBEntry *entry,
+						    const char *uri,
+						    guint64 dest_size,
+						    const char *mimetype);
+static gboolean rb_media_player_source_track_add_error (RBRemovableMediaSource *source,
+							RhythmDBEntry *entry,
+							const char *uri,
+							GError *error);
+
+static void track_add_done (RBMediaPlayerSource *source, RhythmDBEntry *entry);
+static void update_sync (RBMediaPlayerSource *source);
+static void sync_cmd (GtkAction *action, RBSource *source);
+static char *make_track_uuid  (RhythmDBEntry *entry);
+
+static GtkActionEntry rb_media_player_source_actions[] = {
+	{ "MediaPlayerSourceSync", GTK_STOCK_REFRESH, N_("Sync"), NULL,
+	  N_("Synchronize media player with the library"),
+	  G_CALLBACK (sync_cmd) },
+};
 
 enum
 {
@@ -74,12 +113,22 @@ static void
 rb_media_player_source_class_init (RBMediaPlayerSourceClass *klass)
 {
 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+	RBRemovableMediaSourceClass *rms_class = RB_REMOVABLE_MEDIA_SOURCE_CLASS (klass);
+
+	object_class->dispose = rb_media_player_source_dispose;
 
 	object_class->set_property = rb_media_player_source_set_property;
 	object_class->get_property = rb_media_player_source_get_property;
+	object_class->constructed = rb_media_player_source_constructed;
 
+	rms_class->impl_track_added = rb_media_player_source_track_added;
+	rms_class->impl_track_add_error = rb_media_player_source_track_add_error;
+
+	klass->impl_get_entries = NULL;
 	klass->impl_get_capacity = NULL;
 	klass->impl_get_free_space = NULL;
+	klass->impl_add_playlist = NULL;
+	klass->impl_remove_playlists = NULL;
 	klass->impl_show_properties = NULL;
 
 	g_object_class_install_property (object_class,
@@ -94,6 +143,19 @@ rb_media_player_source_class_init (RBMediaPlayerSourceClass *klass)
 }
 
 static void
+rb_media_player_source_dispose (GObject *object)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (object);
+
+	if (priv->sync_settings) {
+		g_object_unref (priv->sync_settings);
+		priv->sync_settings = NULL;
+	}
+
+	G_OBJECT_CLASS (rb_media_player_source_parent_class)->dispose (object);
+}
+
+static void
 rb_media_player_source_init (RBMediaPlayerSource *source)
 {
 }
@@ -129,7 +191,75 @@ rb_media_player_source_get_property (GObject *object,
 	}
 }
 
+static void
+rb_media_player_source_constructed (GObject *object)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (object);
+	RBShell *shell;
 
+	RB_CHAIN_GOBJECT_METHOD (rb_media_player_source_parent_class, constructed, object);
+
+	g_object_get (object, "shell", &shell, NULL);
+	priv->action_group = _rb_source_register_action_group (RB_SOURCE (object),
+							       "MediaPlayerActions",
+							       NULL, 0,
+							       NULL);
+	_rb_action_group_add_source_actions (priv->action_group,
+					     G_OBJECT (shell),
+					     rb_media_player_source_actions,
+					     G_N_ELEMENTS (rb_media_player_source_actions));
+	priv->sync_action = gtk_action_group_get_action (priv->action_group, "MediaPlayerSync");
+}
+
+static gboolean
+rb_media_player_source_track_added (RBRemovableMediaSource *source,
+				    RhythmDBEntry *entry,
+				    const char *uri,
+				    guint64 dest_size,
+				    const char *mimetype)
+{
+	track_add_done (RB_MEDIA_PLAYER_SOURCE (source), entry);
+	return TRUE;
+}
+
+static gboolean
+rb_media_player_source_track_add_error (RBRemovableMediaSource *source,
+					RhythmDBEntry *entry,
+					const char *uri,
+					GError *error)
+{
+	track_add_done (RB_MEDIA_PLAYER_SOURCE (source), entry);
+	return TRUE;
+}
+
+/* must be called once device information is available via source properties */
+void
+rb_media_player_source_load		(RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	char *device_id;
+	char *sync_filename;
+	char *sync_path;
+	char *sync_dir;
+
+	/* make sure the sync settings dir exists */
+	sync_dir = g_build_filename (rb_user_data_dir (), "sync", NULL);
+	g_mkdir (sync_dir, 0700);
+
+	/* construct path to sync settings file */
+	g_object_get (source, "serial", &device_id, NULL);
+	if (device_id == NULL) {
+		g_object_get (source, "name", &device_id, NULL);
+	}
+	sync_filename = g_strdup_printf ("device-%s.conf", device_id);
+	sync_path = g_build_filename (sync_dir, sync_filename, NULL);
+	g_free (sync_filename);
+	g_free (device_id);
+	g_free (sync_dir);
+
+	priv->sync_settings = rb_media_player_sync_settings_new (sync_path);
+	g_free (sync_path);
+}
 
 static guint64
 get_capacity (RBMediaPlayerSource *source)
@@ -147,6 +277,18 @@ get_free_space (RBMediaPlayerSource *source)
 	return klass->impl_get_free_space (source);
 }
 
+void
+rb_media_player_source_delete_entries	(RBMediaPlayerSource *source,
+					 GList *entries,
+					 RBMediaPlayerSourceDeleteCallback callback,
+					 gpointer callback_data,
+					 GDestroyNotify destroy_data)
+{
+	RBMediaPlayerSourceClass *klass = RB_MEDIA_PLAYER_SOURCE_GET_CLASS (source);
+
+	return klass->impl_delete_entries (source, entries, callback, callback_data, destroy_data);
+}
+
 static void
 properties_dialog_response_cb (GtkDialog *dialog,
 			       int response_id,
@@ -159,19 +301,190 @@ properties_dialog_response_cb (GtkDialog *dialog,
 	priv->properties_dialog = NULL;
 }
 
+static void
+update_sync_preview_bar (RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	char *text;
+	char *used;
+	char *capacity;
+	double frac;
+
+	/* TODO use segmented bar widget here */
+
+	update_sync (source);
+
+	frac = (priv->sync_space_needed/(double) get_capacity (source));
+	frac = (frac > 1.0 ? 1.0 : frac);
+	frac = (frac < 0.0 ? 0.0 : frac);
+	used = g_format_size_for_display (priv->sync_space_needed);
+	capacity = g_format_size_for_display (get_capacity (source));
+	gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (priv->preview_bar), frac);
+
+	/* Translators: this is used to display the amount of storage space which will be
+	 * used and the total storage space on a device after it is synced.
+	 */
+	text = g_strdup_printf (_("%s of %s"), used, capacity);
+	gtk_progress_bar_set_text (GTK_PROGRESS_BAR (priv->preview_bar), text);
+	g_free (text);
+	g_free (capacity);
+	g_free (used);
+}
+
+static void
+set_treeview_children (RBMediaPlayerSource *source,
+		       GtkTreeIter *parent,
+		       const char *category,
+		       gboolean value)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	GtkTreeIter iter;
+	gchar *name;
+	gboolean valid;
+
+	valid = gtk_tree_model_iter_children (GTK_TREE_MODEL (priv->sync_tree_store), &iter, parent);
+
+	while (valid) {
+		gtk_tree_model_get (GTK_TREE_MODEL (priv->sync_tree_store), &iter,
+				    1, &name,
+				    -1);
+
+		gtk_tree_store_set (priv->sync_tree_store, &iter,
+		/* Active */	    0, rb_media_player_sync_settings_sync_group (priv->sync_settings, category, name),
+		/* Activatable */   2, value,
+				    -1);
+
+		g_free (name);
+		valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (priv->sync_tree_store), &iter);
+	}
+}
+
+static void
+sync_entries_changed_cb (GtkCellRendererToggle *cell_renderer,
+			 gchar *path,
+			 RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	GtkTreeIter iter;
+	if (gtk_tree_model_get_iter_from_string (GTK_TREE_MODEL (priv->sync_tree_store), &iter, path) == TRUE) {
+		char *name;
+		char *category_name;
+		gboolean is_category;
+		gboolean value;
+
+		gtk_tree_model_get (GTK_TREE_MODEL (priv->sync_tree_store),
+				    &iter,
+				    1, &name,
+				    3, &is_category,
+				    4, &category_name,
+				    -1);
+		value = !gtk_cell_renderer_toggle_get_active (cell_renderer);
+
+		gtk_tree_store_set (priv->sync_tree_store,
+				    &iter,
+				    0, value,
+				    -1 );
+
+		if (is_category) {
+			rb_media_player_sync_settings_set_category (priv->sync_settings, category_name, value);
+			set_treeview_children (source, &iter, category_name, value);
+		} else {
+			rb_media_player_sync_settings_set_group (priv->sync_settings, category_name, name, value);
+		}
+		g_free (category_name);
+	}
+
+	update_sync_preview_bar (source);
+}
+
+static void
+sync_music_all_changed_cb (GtkWidget *togglebutton,
+			   RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	GtkTreeIter iter;
+	gboolean value;
+	gboolean sync_all_playlists;
+
+	value = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (togglebutton));
+	rb_media_player_sync_settings_set_category (priv->sync_settings, SYNC_CATEGORY_MUSIC, value);
+	sync_all_playlists = rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_PLAYLIST);
+
+	if (gtk_tree_model_get_iter_from_string (GTK_TREE_MODEL (priv->sync_tree_store), &iter, "0") == TRUE) {
+		gtk_tree_store_set (priv->sync_tree_store, &iter,
+		/* Active */	    0, sync_all_playlists || value,
+		/* Activatable */   2, !value,
+				    -1);
+
+		set_treeview_children (source,
+				       &iter,
+				       SYNC_CATEGORY_PLAYLIST,
+				       !value && sync_all_playlists);
+	}
+
+	update_sync_preview_bar (source);
+}
+
+/* i'm not really sure why we have 'sync all podcasts'; selecting the
+ * podcast container does the same thing..
+ */
+static void
+sync_podcasts_all_changed_cb (GtkWidget *togglebutton,
+			      RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	GtkTreeIter iter;
+	gboolean value;
+	gboolean sync_podcasts;
+
+	value = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (togglebutton));
+	rb_media_player_sync_settings_set_category (priv->sync_settings, SYNC_CATEGORY_ALL_PODCASTS, value);
+	sync_podcasts = rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_PODCAST);
+
+	if (gtk_tree_model_get_iter_from_string (GTK_TREE_MODEL (priv->sync_tree_store), &iter, "1") == TRUE) {
+		gtk_tree_store_set (priv->sync_tree_store, &iter,
+		/* Active */	    0, sync_podcasts || value,
+		/* Activatable */   2, !value,
+				    -1);
+
+		set_treeview_children (source,
+				       &iter,
+				       SYNC_CATEGORY_PODCAST,
+				       !value && sync_podcasts);
+	}
+
+	update_sync_preview_bar (source);
+}
+
+
+
 void
 rb_media_player_source_show_properties (RBMediaPlayerSource *source)
 {
 	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
 	RBMediaPlayerSourceClass *klass = RB_MEDIA_PLAYER_SOURCE_GET_CLASS (source);
 	GtkBuilder *builder;
+	GtkTreeIter tree_iter;
+	GtkTreeIter parent_iter;
+	GtkTreeModel *query_model;
+	GtkCellRenderer *renderer;
+	GtkTreeViewColumn *col;
+	GtkWidget *tree_view;
 	GtkWidget *widget;
 	const char *ui_file;
 	char *used_str;
 	char *capacity_str;
 	char *text;
+	GList *l;
+	GList *playlists;
 	guint64 capacity;
 	guint64 free_space;
+	gboolean valid;
+	gboolean sync_category;
+	gboolean sync_all;
+	RBShell *shell;
+	RhythmDB *db;
+	RBPlaylistManager *playlist_manager;
 
 	if (priv->properties_dialog != NULL) {
 		gtk_window_present (GTK_WINDOW (priv->properties_dialog));
@@ -191,6 +504,9 @@ rb_media_player_source_show_properties (RBMediaPlayerSource *source)
 		return;
 	}
 
+	g_object_get (source, "shell", &shell, NULL);
+	g_object_get (shell, "db", &db, "playlist-manager", &playlist_manager, NULL);
+
 	priv->properties_dialog = GTK_DIALOG (gtk_builder_get_object (builder, "media-player-properties"));
 	g_signal_connect_object (priv->properties_dialog,
 				 "response",
@@ -227,5 +543,683 @@ rb_media_player_source_show_properties (RBMediaPlayerSource *source)
 					     GTK_WIDGET (gtk_builder_get_object (builder, "media-player-notebook")));
 	}
 
+	/* set up sync widgetry.
+	 * tree_store columns are: Active, Name, Activatable, is-category, category name
+	 */
+	priv->preview_bar = GTK_WIDGET (gtk_builder_get_object (builder, "progressbar-sync-preview"));
+	priv->sync_tree_store = gtk_tree_store_new (5, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_STRING);
+
+	/* music library parent */
+	sync_all = rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_MUSIC);
+	sync_category = rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_PLAYLIST);
+	gtk_tree_store_append (priv->sync_tree_store, &parent_iter, NULL);
+	gtk_tree_store_set (priv->sync_tree_store, &parent_iter,
+			    0, sync_all || sync_category,
+			    1, _("Music Playlists"),
+			    2, sync_all == FALSE,
+			    3, TRUE,
+			    4, SYNC_CATEGORY_PLAYLIST,
+			    -1);
+
+	/* playlist entries */
+	playlists = rb_playlist_manager_get_playlists (playlist_manager);
+	for (l = playlists; l != NULL; l=l->next) {
+		char *name;
+
+		gtk_tree_store_append (priv->sync_tree_store, &tree_iter, &parent_iter);
+		/* set playlists data here */
+		g_object_get (l->data, "name", &name, NULL);
+
+		/* set this row's data */
+		gtk_tree_store_set (priv->sync_tree_store, &tree_iter,
+				    0, rb_media_player_sync_settings_sync_group (priv->sync_settings, SYNC_CATEGORY_PLAYLIST, name),
+				    1, name,
+				    2, sync_category && !sync_all,
+				    3, FALSE,
+				    4, SYNC_CATEGORY_PLAYLIST,
+				    -1);
+
+		g_free (name);
+	}
+
+	/* Append the Podcasts parent */
+	gtk_tree_store_append (priv->sync_tree_store,
+			       &parent_iter,
+			       NULL);
+	sync_all = rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_ALL_PODCASTS);
+	sync_category = rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_PODCAST);
+	gtk_tree_store_set (priv->sync_tree_store, &parent_iter,
+			    0, sync_category || sync_all,
+			    1, _("Podcasts"),
+			    2, sync_all == FALSE,
+			    3, TRUE,
+			    4, SYNC_CATEGORY_PODCAST,
+			    -1);
+
+	/* this really needs to use a live query model */
+	query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
+	rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
+				RHYTHMDB_QUERY_PROP_EQUALS,
+				RHYTHMDB_PROP_TYPE, RHYTHMDB_ENTRY_TYPE_PODCAST_FEED,
+				RHYTHMDB_QUERY_END);
+	valid = gtk_tree_model_get_iter_first (query_model, &tree_iter);
+	while (valid) {
+		RhythmDBEntry *entry;
+		GtkTreeIter tree_iter2;
+		const char *name;
+
+		entry = rhythmdb_query_model_iter_to_entry (RHYTHMDB_QUERY_MODEL (query_model), &tree_iter);
+		gtk_tree_store_append (priv->sync_tree_store, &tree_iter2, &parent_iter);
+
+		/* set up this row */
+		name = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE);
+		gtk_tree_store_set (priv->sync_tree_store, &tree_iter2,
+				    0, rb_media_player_sync_settings_sync_group (priv->sync_settings, SYNC_CATEGORY_PODCAST, name),
+				    1, name,
+				    2, sync_category && !sync_all,
+				    3, FALSE,
+				    4, SYNC_CATEGORY_PODCAST,
+				    -1);
+
+		valid = gtk_tree_model_iter_next (query_model, &tree_iter);
+	}
+
+	/* Set up the treeview */
+	tree_view = GTK_WIDGET (gtk_builder_get_object (builder, "treeview-sync"));
+
+	/* First column */
+	renderer = gtk_cell_renderer_toggle_new ();
+	col = gtk_tree_view_column_new_with_attributes (NULL,
+							renderer,
+							"active", 0,
+							"sensitive", 2,
+							"activatable", 2,
+							NULL);
+	g_signal_connect (G_OBJECT (renderer),
+			  "toggled", G_CALLBACK (sync_entries_changed_cb),
+			  source);
+	gtk_tree_view_append_column(GTK_TREE_VIEW (tree_view), col);
+
+	/* Second column */
+	renderer = gtk_cell_renderer_text_new ();
+	col = gtk_tree_view_column_new_with_attributes (NULL,
+							renderer,
+							"text", 1,
+							"sensitive", 2,
+							NULL);
+	gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), col);
+	gtk_tree_view_set_model (GTK_TREE_VIEW (tree_view),
+				 GTK_TREE_MODEL (priv->sync_tree_store));
+	gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)),
+				    GTK_SELECTION_NONE);
+
+	widget = GTK_WIDGET (gtk_builder_get_object (builder, "checkbutton-sync-music-all"));
+	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget),
+				      rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_MUSIC));
+	g_signal_connect (widget, "toggled",
+			  (GCallback)sync_music_all_changed_cb,
+			  source);
+
+	widget = GTK_WIDGET (gtk_builder_get_object (builder, "checkbutton-sync-podcasts-all"));
+	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget),
+				      rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_ALL_PODCASTS));
+	g_signal_connect (widget, "toggled",
+			  (GCallback)sync_podcasts_all_changed_cb,
+			  source);
+
+	update_sync_preview_bar (source);
+
 	gtk_widget_show (GTK_WIDGET (priv->properties_dialog));
+
+	g_object_unref (builder);
+	g_object_unref (playlist_manager);
+	g_object_unref (shell);
+	g_object_unref (db);
+}
+
+typedef struct {
+	GHashTable *target;
+	GList *result;
+} BuildSyncListData;
+
+static void
+build_sync_list_cb (char *uuid, RhythmDBEntry *entry, BuildSyncListData *data)
+{
+	if (!g_hash_table_lookup (data->target, uuid)) {
+		rb_debug ("adding %s (%" G_GINT64_FORMAT " bytes); id %s to sync list",
+			  rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION),
+			  rhythmdb_entry_get_uint64 (entry, RHYTHMDB_PROP_FILE_SIZE),
+			  uuid);
+		data->result = g_list_prepend (data->result, rhythmdb_entry_ref (entry));
+	}
+}
+
+
+static gboolean
+entry_is_undownloaded_podcast (RhythmDBEntry *entry)
+{
+	if (rhythmdb_entry_get_entry_type (entry) == RHYTHMDB_ENTRY_TYPE_PODCAST_POST) {
+		return (!rb_podcast_manager_entry_downloaded (entry));
+	}
+
+	return FALSE;
+}
+
+
+static void
+update_sync_space_needed (RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	GList *list_iter;
+	gint64 add_size = 0;
+	gint64 remove_size = 0;
+
+	for (list_iter = priv->sync_to_add; list_iter; list_iter = list_iter->next) {
+		add_size += rhythmdb_entry_get_uint64 (list_iter->data, RHYTHMDB_PROP_FILE_SIZE);
+	}
+
+	for (list_iter = priv->sync_to_remove; list_iter; list_iter = list_iter->next) {
+		remove_size += rhythmdb_entry_get_uint64 (list_iter->data, RHYTHMDB_PROP_FILE_SIZE);
+	}
+
+	priv->sync_space_needed = get_capacity (source) - get_free_space (source);
+	rb_debug ("current space used: %" G_GINT64_FORMAT " bytes; adding %" G_GINT64_FORMAT ", removing %" G_GINT64_FORMAT,
+		  priv->sync_space_needed,
+		  add_size,
+		  remove_size);
+	priv->sync_space_needed = priv->sync_space_needed + add_size - remove_size;
+	rb_debug ("space used after sync: %" G_GINT64_FORMAT " bytes", priv->sync_space_needed);
+}
+
+static gboolean
+hash_table_insert_from_tree_model_cb (GtkTreeModel *query_model,
+				      GtkTreePath  *path,
+				      GtkTreeIter  *iter,
+				      GHashTable   *target)
+{
+	RhythmDBEntry *entry;
+
+	entry = rhythmdb_query_model_iter_to_entry (RHYTHMDB_QUERY_MODEL (query_model), iter);
+	if (!entry_is_undownloaded_podcast (entry)) {
+		g_hash_table_insert (target,
+				     make_track_uuid (entry),
+				     rhythmdb_entry_ref (entry));
+	}
+
+	return FALSE;
+}
+static void
+itinerary_insert_all_of_type (RhythmDB *db,
+			      RhythmDBEntryType entry_type,
+			      GHashTable *target)
+{
+	GtkTreeModel *query_model;
+
+	query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
+	rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
+				RHYTHMDB_QUERY_PROP_EQUALS,
+				RHYTHMDB_PROP_TYPE, entry_type,
+				RHYTHMDB_QUERY_END);
+
+	gtk_tree_model_foreach (query_model,
+				(GtkTreeModelForeachFunc) hash_table_insert_from_tree_model_cb,
+				target);
+}
+
+static void
+itinerary_insert_some_playlists (RBMediaPlayerSource *source,
+				 GHashTable *target)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	GList *list_iter;
+	GList *playlists;
+	RBShell *shell;
+
+	g_object_get (source, "shell", &shell, NULL);
+	playlists = rb_playlist_manager_get_playlists ((RBPlaylistManager *) rb_shell_get_playlist_manager (shell));
+	g_object_unref (shell);
+
+	for (list_iter = playlists; list_iter; list_iter = list_iter->next) {
+		gchar *name;
+
+		g_object_get (G_OBJECT (list_iter->data), "name", &name, NULL);
+
+		/* See if we should sync it */
+		if (rb_media_player_sync_settings_sync_group (priv->sync_settings, SYNC_CATEGORY_PLAYLIST, name)) {
+			GtkTreeModel *query_model;
+
+			rb_debug ("adding entries from playlist %s to itinerary", name);
+			g_object_get (RB_SOURCE (list_iter->data), "base-query-model", &query_model, NULL);
+			gtk_tree_model_foreach (query_model,
+						(GtkTreeModelForeachFunc) hash_table_insert_from_tree_model_cb,
+						target);
+			g_object_unref (query_model);
+		} else {
+			rb_debug ("not adding playlist %s to itinerary", name);
+		}
+
+		g_free (name);
+	}
+
+	g_list_free (playlists);
+}
+
+static void
+itinerary_insert_some_podcasts (RBMediaPlayerSource *source,
+				RhythmDB *db,
+				GHashTable *target)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	GList *podcasts;
+	GList *i;
+
+	podcasts = rb_media_player_sync_settings_get_enabled_groups (priv->sync_settings, SYNC_CATEGORY_PODCAST);
+	for (i = podcasts; i != NULL; i = i->next) {
+		GtkTreeModel *query_model;
+		rb_debug ("adding entries from podcast  %s to itinerary", (char *)i->data);
+		query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
+		rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
+					RHYTHMDB_QUERY_PROP_EQUALS,
+					RHYTHMDB_PROP_TYPE, RHYTHMDB_ENTRY_TYPE_PODCAST_POST,
+					RHYTHMDB_QUERY_PROP_EQUALS,
+					RHYTHMDB_PROP_ALBUM, i->data,	/* album? */
+					RHYTHMDB_QUERY_END);
+
+		/* TODO: exclude undownloaded episodes, sort by post date, set limit, optionally exclude things with play count > 0
+		 * RHYTHMDB_QUERY_PROP_NOT_EQUAL, RHYTHMDB_PROP_MOUNTPOINT, NULL,	(will this work?)
+		 * RHYTHMDB_QUERY_PROP_NOT_EQUAL, RHYTHMDB_PROP_STATUS, RHYTHMDB_PODCAST_STATUS_ERROR,
+		 *
+		 * RHYTHMDB_QUERY_PROP_EQUALS, RHYTHMDB_PROP_PLAYCOUNT, 0
+		 */
+
+		gtk_tree_model_foreach (query_model,
+					(GtkTreeModelForeachFunc) hash_table_insert_from_tree_model_cb,
+					target);
+		g_object_unref (query_model);
+	}
+}
+
+static GHashTable *
+build_sync_itinerary (RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	RBShell *shell;
+	RhythmDB *db;
+	GHashTable *itinerary;
+
+	rb_debug ("building itinerary hash");
+
+	g_object_get (source, "shell", &shell, NULL);
+	g_object_get (shell, "db", &db, NULL);
+
+	itinerary = g_hash_table_new_full (g_str_hash,
+					   g_str_equal,
+					   g_free,
+					   (GDestroyNotify)rhythmdb_entry_unref);
+
+	/* duplicating the original logic here; doesn't seem right to me though */
+	if (rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_PLAYLIST)) {
+		if (rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_MUSIC)) {
+			rb_debug ("adding all music to the itinerary");
+			itinerary_insert_all_of_type (db, RHYTHMDB_ENTRY_TYPE_SONG, itinerary);
+		} else {
+			rb_debug ("adding selected playlists to the itinerary");
+			itinerary_insert_some_playlists (source, itinerary);
+		}
+	}
+
+	if (rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_PODCAST)) {
+		if (rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_ALL_PODCASTS)) {
+			rb_debug ("adding all podcasts to the itinerary");
+			/* TODO: when we get #episodes/not-if-played settings, use
+			 * equivalent of insert_some_podcasts, iterating through all feeds
+			 * (use a query for all entries of type PODCAST_FEED to find them)
+			 */
+			itinerary_insert_all_of_type (db, RHYTHMDB_ENTRY_TYPE_PODCAST_POST, itinerary);
+		} else {
+			rb_debug ("adding selected podcasts to the itinerary");
+			itinerary_insert_some_podcasts (source, db, itinerary);
+		}
+	}
+
+	g_object_unref (shell);
+	g_object_unref (db);
+
+	rb_debug ("finished building itinerary hash; has %d entries", g_hash_table_size (itinerary));
+	return itinerary;
+}
+
+static void
+_g_hash_table_transfer_all (GHashTable *target, GHashTable *source)
+{
+	GHashTableIter iter;
+	gpointer key, value;
+
+	g_hash_table_iter_init (&iter, source);
+	while (g_hash_table_iter_next (&iter, &key, &value)) {
+		g_hash_table_insert (target, key, value);
+		g_hash_table_iter_steal (&iter);
+	}
+}
+
+static GHashTable *
+build_device_state (RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	RBMediaPlayerSourceClass *klass = RB_MEDIA_PLAYER_SOURCE_GET_CLASS (source);
+	GHashTable *device;
+
+	rb_debug ("building device contents hash");
+
+	device = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)rhythmdb_entry_unref);
+
+	/* TODO: could do this by retrieving the list of enabled sync categories, probably.
+	 * would probably require the 'sync all music' option to work differently though.
+	 */
+
+	if (rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_PLAYLIST)) {
+		GHashTable *entries;
+		rb_debug ("getting music entries from device");
+		entries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) rhythmdb_entry_unref);
+		klass->impl_get_entries (source, SYNC_CATEGORY_MUSIC, entries);
+		_g_hash_table_transfer_all (device, entries);
+		g_hash_table_destroy (entries);
+		rb_debug ("done getting music entries from device");
+	}
+
+	if (rb_media_player_sync_settings_sync_category (priv->sync_settings, SYNC_CATEGORY_PODCAST)) {
+		GHashTable *podcasts;
+		rb_debug ("getting podcast entries from device");
+		podcasts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) rhythmdb_entry_unref);
+		klass->impl_get_entries (source, SYNC_CATEGORY_PODCAST, podcasts);
+		_g_hash_table_transfer_all (device, podcasts);
+		g_hash_table_destroy (podcasts);
+		rb_debug ("done getting podcast entries from device");
+	}
+
+	rb_debug ("done building device contents hash; has %d entries", g_hash_table_size (device));
+	return device;
+}
+
+static void
+update_sync (RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	GHashTable *device;
+	GHashTable *itinerary;
+	BuildSyncListData data;
+
+	/* destroy existing sync lists */
+	rb_list_destroy_free (priv->sync_to_add, (GDestroyNotify) rhythmdb_entry_unref);
+	rb_list_destroy_free (priv->sync_to_remove, (GDestroyNotify) rhythmdb_entry_unref);
+	priv->sync_to_add = NULL;
+	priv->sync_to_remove = NULL;
+
+	/* figure out what we want on the device and what's already there */
+	itinerary = build_sync_itinerary (source);
+	device = build_device_state (source);
+
+	/* figure out what to add to the device */
+	rb_debug ("building list of files to transfer to device");
+	data.target = device;
+	data.result = NULL;
+	g_hash_table_foreach (itinerary, (GHFunc)build_sync_list_cb, &data);
+	priv->sync_to_add = data.result;
+	rb_debug ("decided to transfer %d files to the device", g_list_length (priv->sync_to_add));
+
+	/* and what to remove */
+	rb_debug ("building list of files to remove from device");
+	data.target = itinerary;
+	data.result = NULL;
+	g_hash_table_foreach (device, (GHFunc)build_sync_list_cb, &data);
+	priv->sync_to_remove = data.result;
+	rb_debug ("decided to remove %d files from the device", g_list_length (priv->sync_to_remove));
+
+	g_hash_table_destroy (device);
+	g_hash_table_destroy (itinerary);
+
+	update_sync_space_needed (source);
+	priv->sync_needs_update = FALSE;
+}
+
+static void
+sync_playlists (RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	RBMediaPlayerSourceClass *klass = RB_MEDIA_PLAYER_SOURCE_GET_CLASS (source);
+	RBPlaylistManager *playlist_manager;
+	RBShell *shell;
+	GHashTable *device;
+	GList *all_playlists;
+	GList *l;
+
+	if (klass->impl_add_playlist == NULL || klass->impl_remove_playlists == NULL) {
+		rb_debug ("source class doesn't support playlists");
+		return;
+	}
+
+	/* build an updated device contents map, so we can find the device entries
+	 * corresponding to the entries in the local playlists.
+	 */
+	device = build_device_state (source);
+
+	/* remove all playlists from the device, then add the synced playlists. */
+	klass->impl_remove_playlists (source);
+
+	/* get all local playlists */
+	g_object_get (source, "shell", &shell, NULL);
+	g_object_get (shell, "playlist-manager", &playlist_manager, NULL);
+	all_playlists = rb_playlist_manager_get_playlists (playlist_manager);
+	g_object_unref (playlist_manager);
+	g_object_unref (shell);
+
+	for (l = all_playlists; l != NULL; l = l->next) {
+		char *name;
+		RBSource *playlist_source = RB_SOURCE (l->data);
+		RhythmDBQueryModel *model;
+		GList *tracks = NULL;
+		GtkTreeIter iter;
+
+		/* is this playlist selected for syncing? */
+		g_object_get (playlist_source, "name", &name, NULL);
+		if (rb_media_player_sync_settings_group_enabled (priv->sync_settings, SYNC_CATEGORY_PLAYLIST, name) == FALSE) {
+			rb_debug ("not syncing playlist %s", name);
+			g_free (name);
+			continue;
+		}
+
+		/* match playlist entries to entries on the device */
+		g_object_get (playlist_source, "base-query-model", &model, NULL);
+		if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (model), &iter) == FALSE) {
+			rb_debug ("not syncing empty playlist %s", name);
+			g_free (name);
+			g_object_unref (model);
+			continue;
+		}
+
+		do {
+			char *trackid;
+			RhythmDBEntry *entry;
+			RhythmDBEntry *device_entry;
+
+			entry = rhythmdb_query_model_iter_to_entry (model, &iter);
+			trackid = make_track_uuid (entry);
+
+			device_entry = g_hash_table_lookup (device, trackid);
+			if (device_entry != NULL) {
+				tracks = g_list_prepend (tracks, device_entry);
+			} else {
+				rb_debug ("unable to find entry on device for track %s (id %s)",
+					  rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION),
+					  trackid);
+			}
+			g_free (trackid);
+
+		} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (model), &iter));
+
+		tracks = g_list_reverse (tracks);
+
+		/* transfer the playlist to the device */
+		rb_debug ("syncing playlist %s", name);
+		klass->impl_add_playlist (source, name, tracks);
+
+		g_free (name);
+		g_list_free (tracks);
+		g_object_unref (model);
+	}
+}
+
+static gboolean
+sync_idle_cb_cleanup (RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+
+	rb_debug ("cleaning up after sync process");
+	priv->sync_needs_update = TRUE;
+
+	gtk_action_set_sensitive (priv->sync_action, TRUE);
+
+	return FALSE;
+}
+
+static gboolean
+sync_idle_cb_playlists (RBMediaPlayerSource *source)
+{
+	/* Transfer the playlists */
+	rb_debug ("transferring playlists to the device");
+	sync_playlists (source);
+	g_idle_add ((GSourceFunc)sync_idle_cb_cleanup, source);
+	return FALSE;
+}
+
+static void
+track_add_done (RBMediaPlayerSource *source, RhythmDBEntry *entry)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	GList *l;
+	/* remove the entry from the set of transfers we're waiting for;
+	 * if the set is now empty, trigger the next sync stage.
+	 */
+
+	l = g_list_find (priv->sync_to_add, entry);
+	if (l != NULL) {
+		priv->sync_to_add = g_list_remove_link (priv->sync_to_add, l);
+		if (priv->sync_to_add == NULL) {
+			rb_debug ("finished transferring files to the device");
+			g_idle_add ((GSourceFunc) sync_idle_cb_playlists, source);
+		}
+		rhythmdb_entry_unref (entry);
+	}
+}
+
+static void
+sync_delete_done_cb (RBMediaPlayerSource *source, gpointer dontcare)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	rb_debug ("finished deleting %d files from media player", g_list_length (priv->sync_to_remove));
+	rb_list_destroy_free (priv->sync_to_remove, (GDestroyNotify) rhythmdb_entry_unref);
+	priv->sync_to_remove = NULL;
+
+	/* Transfer needed tracks and podcasts from itinerary to device */
+	if (priv->sync_to_add != NULL) {
+		rb_debug ("transferring %d files from media player", g_list_length (priv->sync_to_add));
+		rb_source_paste (RB_SOURCE (source), priv->sync_to_add);
+	} else {
+		rb_debug ("no files to transfer to the device");
+		g_idle_add ((GSourceFunc) sync_idle_cb_playlists, source);
+	}
+}
+
+static gboolean
+sync_idle_cb_delete_entries (RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+	rb_debug ("deleting %d files from media player", g_list_length (priv->sync_to_remove));
+	rb_media_player_source_delete_entries (source,
+					       priv->sync_to_remove,
+					       (RBMediaPlayerSourceDeleteCallback) sync_delete_done_cb,
+					       NULL,
+					       NULL);
+	return FALSE;
+}
+
+static gboolean
+sync_idle_cb_update_sync (RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+
+	if (priv->sync_needs_update) {
+		update_sync (source);
+	}
+
+	/* Check we have enough space on the device. */
+	if (priv->sync_space_needed > get_capacity (source)) {
+		rb_debug ("not enough free space on device; need %" G_GINT64_FORMAT ", capacity is %" G_GINT64_FORMAT,
+			  priv->sync_space_needed,
+			  get_capacity (source));
+		rb_error_dialog (NULL,
+				 _("Not enough free space to sync"),
+				 _("There is not enough free space on this device to transfer the selected music, playlists, and podcasts."));
+		g_idle_add ((GSourceFunc)sync_idle_cb_cleanup, source);
+		return FALSE;
+	}
+
+	g_idle_add ((GSourceFunc)sync_idle_cb_delete_entries, source);
+	return FALSE;
+}
+
+void
+rb_media_player_source_sync (RBMediaPlayerSource *source)
+{
+	RBMediaPlayerSourcePrivate *priv = MEDIA_PLAYER_SOURCE_GET_PRIVATE (source);
+
+	gtk_action_set_sensitive (priv->sync_action, FALSE);
+
+	g_idle_add ((GSourceFunc)sync_idle_cb_update_sync, source);
+}
+
+static char *
+make_track_uuid  (RhythmDBEntry *entry)
+{
+	/* This function is for hashing the two databases for syncing. */
+	GString *str = g_string_new ("");
+	char *result;
+
+	/*
+	 * possible improvements here:
+	 * - use musicbrainz track ID if known (maybe not a great idea?)
+	 * - fuzz the duration a bit (round to nearest 5 seconds?) to catch slightly
+	 *   different encodings of the same track
+	 * - maybe don't include genre, since there's no canonical genre for anything
+	 */
+
+	g_string_printf (str, "%s%s%s%s%lu%lu%lu",
+			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE),
+			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST),
+			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_GENRE),
+			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM),
+			 rhythmdb_entry_get_ulong  (entry, RHYTHMDB_PROP_DURATION),
+			 rhythmdb_entry_get_ulong  (entry, RHYTHMDB_PROP_TRACK_NUMBER),
+			 rhythmdb_entry_get_ulong  (entry, RHYTHMDB_PROP_DISC_NUMBER));
+
+	/* not sure why we md5 this.  how does it help? */
+	result = g_compute_checksum_for_string (G_CHECKSUM_MD5, str->str, str->len);
+
+	g_string_free (str, TRUE);
+
+	return result;
+}
+
+void
+_rb_media_player_source_add_to_map (GHashTable *map, RhythmDBEntry *entry)
+{
+	g_hash_table_insert (map,
+			     make_track_uuid (entry),
+			     rhythmdb_entry_ref (entry));
+}
+
+static void
+sync_cmd (GtkAction *action, RBSource *source)
+{
+	rb_media_player_source_sync (RB_MEDIA_PLAYER_SOURCE (source));
 }
diff --git a/sources/rb-media-player-source.h b/sources/rb-media-player-source.h
index 980a442..84d3c61 100644
--- a/sources/rb-media-player-source.h
+++ b/sources/rb-media-player-source.h
@@ -60,15 +60,35 @@ struct _RBMediaPlayerSourceClass
 	RBRemovableMediaSourceClass parent_class;
 
 	/* class members */
+	void		(*impl_get_entries)	(RBMediaPlayerSource *source, const char *category, GHashTable *map);
 	guint64		(*impl_get_capacity)	(RBMediaPlayerSource *source);
 	guint64		(*impl_get_free_space)	(RBMediaPlayerSource *source);
+	void		(*impl_delete_entries)	(RBMediaPlayerSource *source,
+						 GList *entries,
+						 RBMediaPlayerSourceDeleteCallback callback,
+						 gpointer data,
+						 GDestroyNotify destroy_data);
+	void		(*impl_add_playlist)	(RBMediaPlayerSource *source, gchar *name, GList *entries);
+	void		(*impl_remove_playlists) (RBMediaPlayerSource *source);
 	void		(*impl_show_properties)	(RBMediaPlayerSource *source, GtkWidget *info_box, GtkWidget *notebook);
 };
 
 GType	rb_media_player_source_get_type	(void);
 
+void	rb_media_player_source_load		(RBMediaPlayerSource *source);
+
+void	rb_media_player_source_delete_entries	(RBMediaPlayerSource *source,
+						 GList *entries,
+						 RBMediaPlayerSourceDeleteCallback callback,
+						 gpointer user_data,
+						 GDestroyNotify destroy_data);
+
 void	rb_media_player_source_show_properties (RBMediaPlayerSource *source);
 
+void	rb_media_player_source_sync (RBMediaPlayerSource *source);
+
+void	_rb_media_player_source_add_to_map 	(GHashTable *device_map, RhythmDBEntry *entry);
+
 G_END_DECLS
 
 #endif
diff --git a/sources/rb-media-player-sync-settings.c b/sources/rb-media-player-sync-settings.c
new file mode 100644
index 0000000..a23ecb3
--- /dev/null
+++ b/sources/rb-media-player-sync-settings.c
@@ -0,0 +1,392 @@
+/*
+ *  Copyright (C) 2009 Paul Bellamy  <paul a bellamy gmail com>
+ *  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.
+ *
+ */
+
+/*
+ * sync settings consist of categories and groups within those categories.
+ * categories are things like music and podcasts, groups are things like
+ * playlists and individual podcast feeds.  if sync for a category is enabled,
+ * then all groups within the category will be synced.
+ *
+ * at some point we probably need to have more than just enabled/disabled settings
+ * at all levels.. things like the number of episodes for a podcast feed to keep
+ * on the device.
+ *
+ * categories are stored as groups in the keyfile, where the keyfile group name
+ * matches the category name.  if the category as a whole is enabled, the
+ * 'enabled' key will be set in the keyfile group.  the list of groups enabled
+ * within the category is stored in the 'groups' key in the keyfile group.
+ *
+ * if any settings exist for a sync group, they will be stored in a keyfile group
+ * named <category>:<sync-group>.  there is no way to set any such settings at
+ * present.
+ */
+
+#include "config.h"
+
+#include <gio/gio.h>
+#include <string.h>
+
+#include "rb-media-player-sync-settings.h"
+#include "rb-debug.h"
+#include "rb-util.h"
+
+#define CATEGORY_ENABLED_KEY	"enabled"
+#define CATEGORY_GROUPS_KEY	"groups"
+
+typedef struct {
+	GKeyFile *key_file;
+	char *key_file_path;
+
+	guint save_key_file_id;
+} RBMediaPlayerSyncSettingsPrivate;
+
+enum {
+	PROP_0,
+	PROP_KEYFILE_PATH
+};
+
+G_DEFINE_TYPE (RBMediaPlayerSyncSettings, rb_media_player_sync_settings, G_TYPE_OBJECT)
+
+#define GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_MEDIA_PLAYER_SYNC_SETTINGS, RBMediaPlayerSyncSettingsPrivate))
+
+RBMediaPlayerSyncSettings *
+rb_media_player_sync_settings_new (const char *keyfile)
+{
+	GObject *settings;
+	settings = g_object_new (RB_TYPE_MEDIA_PLAYER_SYNC_SETTINGS,
+				 "keyfile-path", keyfile,
+				 NULL);
+	return RB_MEDIA_PLAYER_SYNC_SETTINGS (settings);
+}
+
+gboolean
+rb_media_player_sync_settings_save (RBMediaPlayerSyncSettings *settings)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	char *data;
+	gsize length;
+	GError *error = NULL;
+	GFile *file;
+
+	data = g_key_file_to_data (priv->key_file, &length, &error);
+	if (error != NULL) {
+		rb_debug ("unable to save sync settings: %s", error->message);
+		g_error_free (error);
+		return FALSE;
+	}
+
+	file = g_file_new_for_path (priv->key_file_path);
+	g_file_replace_contents (file, data, length, NULL, FALSE, G_FILE_CREATE_NONE, NULL, NULL, &error);
+	if (error != NULL) {
+		rb_debug ("unable to save sync settings: %s", error->message);
+		g_error_free (error);
+	}
+	g_object_unref (file);
+	g_free (data);
+	return (error == NULL);
+}
+
+static gboolean
+_save_idle_cb (RBMediaPlayerSyncSettings *settings)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	priv->save_key_file_id = 0;
+	rb_media_player_sync_settings_save (settings);
+	return FALSE;
+}
+
+static void
+_save_idle (RBMediaPlayerSyncSettings *settings)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	if (priv->save_key_file_id == 0) {
+		priv->save_key_file_id = g_idle_add ((GSourceFunc) _save_idle_cb, settings);
+	}
+}
+
+static gboolean
+_get_boolean_with_default (GKeyFile *keyfile, const char *group, const char *key, gboolean default_value)
+{
+	GError *error = NULL;
+	gboolean v;
+	v = g_key_file_get_boolean (keyfile, group, key, &error);
+	if (error != NULL) {
+		g_error_free (error);
+		return default_value;
+	}
+	return v;
+}
+
+void
+rb_media_player_sync_settings_set_category (RBMediaPlayerSyncSettings *settings,
+					    const char *category,
+					    gboolean enabled)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	g_key_file_set_boolean (priv->key_file, category, CATEGORY_ENABLED_KEY, enabled);
+	_save_idle (settings);
+}
+
+gboolean
+rb_media_player_sync_settings_sync_category (RBMediaPlayerSyncSettings *settings,
+					     const char *category)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	/* hrm, default? */
+	return _get_boolean_with_default (priv->key_file, category, CATEGORY_ENABLED_KEY, FALSE);
+}
+
+GList *
+rb_media_player_sync_settings_get_enabled_categories (RBMediaPlayerSyncSettings *settings)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	char **groups;
+	GList *categories;
+	int i;
+
+	categories = NULL;
+	groups = g_key_file_get_groups (priv->key_file, NULL);
+	for (i = 0; groups[i] != NULL; i++) {
+		/* filter out group entries */
+		if (g_utf8_strchr (groups[i], -1, ':') != NULL) {
+			continue;
+		}
+
+		categories = g_list_prepend (categories, g_strdup (groups[i]));
+	}
+	g_strfreev (groups);
+	return g_list_reverse (categories);
+}
+
+void
+rb_media_player_sync_settings_set_group (RBMediaPlayerSyncSettings *settings,
+					 const char *category,
+					 const char *group,
+					 gboolean enabled)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	char **groups;
+	int ngroups;
+
+	ngroups = 0;
+	groups = g_key_file_get_string_list (priv->key_file, category, CATEGORY_GROUPS_KEY, NULL, NULL);
+	if (groups != NULL) {
+		int i;
+		ngroups = g_strv_length (groups);
+
+		for (i = 0; i < ngroups; i++) {
+			if (strcmp (groups[i], group) == 0) {
+				if (enabled) {
+					return;
+				} else {
+					groups[i] = groups[ngroups-1];
+					ngroups--;
+				}
+			}
+		}
+	}
+
+	if (enabled) {
+		groups = g_realloc (groups, (ngroups+2) * sizeof(char *));
+		groups[ngroups] = g_strdup (group);
+		groups[ngroups+1] = NULL;
+		ngroups++;
+	}
+
+	g_key_file_set_string_list (priv->key_file, category, CATEGORY_GROUPS_KEY, (const char * const *)groups, ngroups);
+	g_strfreev (groups);
+
+	_save_idle (settings);
+}
+
+gboolean
+rb_media_player_sync_settings_group_enabled (RBMediaPlayerSyncSettings *settings,
+					     const char *category,
+					     const char *group)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	char **groups;
+	int i;
+	gboolean found = FALSE;
+
+	groups = g_key_file_get_string_list (priv->key_file, category, CATEGORY_GROUPS_KEY, NULL, NULL);
+	if (groups == NULL) {
+		return FALSE;
+	}
+
+	for (i = 0; groups[i] != NULL; i++) {
+		if (strcmp (groups[i], group) == 0) {
+			found = TRUE;
+			break;
+		}
+	}
+
+	g_strfreev (groups);
+	return found;
+}
+
+gboolean
+rb_media_player_sync_settings_sync_group (RBMediaPlayerSyncSettings *settings,
+					  const char *category,
+					  const char *group)
+{
+	if (rb_media_player_sync_settings_sync_category (settings, category) == FALSE) {
+		return FALSE;
+	}
+
+	return rb_media_player_sync_settings_group_enabled (settings, category, group);
+}
+
+GList *
+rb_media_player_sync_settings_get_enabled_groups (RBMediaPlayerSyncSettings *settings,
+						  const char *category)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (settings);
+	char **groups;
+	GList *glist = NULL;
+	int i;
+
+	groups = g_key_file_get_string_list (priv->key_file, category, CATEGORY_GROUPS_KEY, NULL, NULL);
+	if (groups == NULL) {
+		return NULL;
+	}
+
+	for (i = 0; groups[i] != NULL; i++) {
+		glist = g_list_prepend (glist, g_strdup (groups[i]));
+	}
+
+	g_strfreev (groups);
+	return g_list_reverse (glist);
+}
+
+
+
+static void
+rb_media_player_sync_settings_init (RBMediaPlayerSyncSettings *settings)
+{
+	/* nothing */
+}
+
+static void
+impl_constructed (GObject *object)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (object);
+	GError *error = NULL;
+
+	priv->key_file = g_key_file_new ();
+	if (g_key_file_load_from_file (priv->key_file,
+				       priv->key_file_path,
+				       G_KEY_FILE_KEEP_COMMENTS,
+				       &error) == FALSE) {
+		rb_debug ("unable to load sync settings from %s: %s", priv->key_file_path, error->message);
+		g_error_free (error);
+
+		/* probably need a way to set defaults.. syncing nothing by default
+		 * is kind of boring.  used to default to syncing all music and all
+		 * podcasts.
+		 */
+	}
+
+	RB_CHAIN_GOBJECT_METHOD(rb_media_player_sync_settings_parent_class, constructed, object);
+}
+
+static void
+impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (object);
+	switch (prop_id) {
+	case PROP_KEYFILE_PATH:
+		priv->key_file_path = g_value_dup_string (value);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (object);
+	switch (prop_id) {
+	case PROP_KEYFILE_PATH:
+		g_value_set_string (value, priv->key_file_path);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+impl_dispose (GObject *object)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (object);
+
+	/* if a save is pending, do it now */
+	if (priv->save_key_file_id != 0) {
+		g_source_remove (priv->save_key_file_id);
+		priv->save_key_file_id = 0;
+		rb_media_player_sync_settings_save (RB_MEDIA_PLAYER_SYNC_SETTINGS (object));
+	}
+
+	G_OBJECT_CLASS (rb_media_player_sync_settings_parent_class)->dispose (object);
+}
+
+static void
+impl_finalize (GObject *object)
+{
+	RBMediaPlayerSyncSettingsPrivate *priv = GET_PRIVATE (object);
+
+	g_key_file_free (priv->key_file);
+	g_free (priv->key_file_path);
+
+	G_OBJECT_CLASS (rb_media_player_sync_settings_parent_class)->finalize (object);
+}
+
+static void
+rb_media_player_sync_settings_class_init (RBMediaPlayerSyncSettingsClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+	object_class->finalize = impl_finalize;
+	object_class->dispose = impl_dispose;
+	object_class->constructed = impl_constructed;
+
+	object_class->set_property = impl_set_property;
+	object_class->get_property = impl_get_property;
+
+	g_object_class_install_property (object_class,
+					 PROP_KEYFILE_PATH,
+					 g_param_spec_string ("keyfile-path",
+							      "keyfile path",
+							      "path to the key file storing the sync settings",
+							      NULL,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+	g_type_class_add_private (object_class, sizeof (RBMediaPlayerSyncSettingsPrivate));
+}
diff --git a/sources/rb-media-player-sync-settings.h b/sources/rb-media-player-sync-settings.h
new file mode 100644
index 0000000..f118d54
--- /dev/null
+++ b/sources/rb-media-player-sync-settings.h
@@ -0,0 +1,94 @@
+/*
+ *  Copyright (C) 2009 Paul Bellamy <paul a bellamy gmail com>
+ *  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.
+ *
+ */
+
+#ifndef RB_MEDIA_PLAYER_SYNC_SETTINGS__H
+#define RB_MEDIA_PLAYER_SYNC_SETTINGS__H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define RB_TYPE_MEDIA_PLAYER_SYNC_SETTINGS         (rb_media_player_sync_settings_get_type ())
+#define RB_MEDIA_PLAYER_SYNC_SETTINGS(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_MEDIA_PLAYER_SYNC_SETTINGS, RBMediaPlayerSyncSettings))
+#define RB_MEDIA_PLAYER_SYNC_SETTINGS_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_MEDIA_PLAYER_SYNC_SETTINGS, RBMediaPlayerSyncSettingsClass))
+#define RB_IS_MEDIA_PLAYER_SYNC_SETTINGS(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_MEDIA_PLAYER_SYNC_SETTINGS))
+#define RB_IS_MEDIA_PLAYER_SYNC_SETTINGS_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_MEDIA_PLAYER_SYNC_SETTINGS))
+#define RB_MEDIA_PLAYER_SYNC_SETTINGS_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_MEDIA_PLAYER_SYNC_SETTINGS, RBMediaPlayerSyncSettingsClass))
+
+/* defined sync categories */
+#define 	SYNC_CATEGORY_MUSIC		"music"
+#define 	SYNC_CATEGORY_PLAYLIST		"playlist"
+#define		SYNC_CATEGORY_ALL_PODCASTS	"all-podcasts"		/* XXX seems a bit meaningless */
+#define 	SYNC_CATEGORY_PODCAST		"podcast"
+
+typedef struct
+{
+	GObject parent;
+} RBMediaPlayerSyncSettings;
+
+typedef struct
+{
+	GObjectClass parent;
+
+	/* signals */
+	void	(*updated) (void);
+} RBMediaPlayerSyncSettingsClass;
+
+GType				rb_media_player_sync_settings_get_type (void);
+
+RBMediaPlayerSyncSettings *	rb_media_player_sync_settings_new (const char *keyfile);
+
+gboolean			rb_media_player_sync_settings_save (RBMediaPlayerSyncSettings *settings);
+
+/* sync categories */
+
+void				rb_media_player_sync_settings_set_category (RBMediaPlayerSyncSettings *settings,
+									    const char *category,
+									    gboolean enabled);
+gboolean			rb_media_player_sync_settings_sync_category (RBMediaPlayerSyncSettings *settings,
+									     const char *category);
+GList *				rb_media_player_sync_settings_get_enabled_categories (RBMediaPlayerSyncSettings *settings);
+
+/* sync category groups */
+
+void				rb_media_player_sync_settings_set_group (RBMediaPlayerSyncSettings *settings,
+									 const char *category,
+									 const char *group,
+									 gboolean enabled);
+gboolean			rb_media_player_sync_settings_group_enabled (RBMediaPlayerSyncSettings *settings,
+									     const char *category,
+									     const char *group);
+gboolean			rb_media_player_sync_settings_sync_group (RBMediaPlayerSyncSettings *settings,
+									  const char *category,
+									  const char *group);
+GList *				rb_media_player_sync_settings_get_enabled_groups (RBMediaPlayerSyncSettings *settings,
+										  const char *category);
+
+G_END_DECLS
+
+#endif /* RB_MEDIA_PLAYER_SYNC_SETTINGS__H */



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