hythmbox-2.98/sources/sync/rb-sync-state.c

No issues found

  1 /*
  2  *  Copyright (C) 2010 Jonathan Matthew <jonathan@d14n.org>
  3  *
  4  *  This program is free software; you can redistribute it and/or modify
  5  *  it under the terms of the GNU General Public License as published by
  6  *  the Free Software Foundation; either version 2 of the License, or
  7  *  (at your option) any later version.
  8  *
  9  *  The Rhythmbox authors hereby grant permission for non-GPL compatible
 10  *  GStreamer plugins to be used and distributed together with GStreamer
 11  *  and Rhythmbox. This permission is above and beyond the permissions granted
 12  *  by the GPL license by which Rhythmbox is covered. If you modify this code
 13  *  you may extend this exception to your version of the code, but you are not
 14  *  obligated to do so. If you do not wish to do so, delete this exception
 15  *  statement from your version.
 16  *
 17  *  This program is distributed in the hope that it will be useful,
 18  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 19  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 20  *  GNU General Public License for more details.
 21  *
 22  *  You should have received a copy of the GNU General Public License
 23  *  along with this program; if not, write to the Free Software
 24  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
 25  *
 26  */
 27 
 28 #include "config.h"
 29 
 30 #include "rb-sync-state.h"
 31 #include "rb-util.h"
 32 #include "rb-debug.h"
 33 
 34 #include "rhythmdb-query-model.h"
 35 #include "rb-podcast-manager.h"
 36 #include "rb-podcast-entry-types.h"
 37 #include "rb-playlist-manager.h"
 38 #include "rb-shell.h"
 39 
 40 struct _RBSyncStatePrivate
 41 {
 42 	/* we don't own a reference on these */
 43 	RBMediaPlayerSource *source;
 44 	RBSyncSettings *sync_settings;
 45 };
 46 
 47 enum {
 48 	PROP_0,
 49 	PROP_SOURCE,
 50 	PROP_SYNC_SETTINGS
 51 };
 52 
 53 enum {
 54 	UPDATED,
 55 	LAST_SIGNAL
 56 };
 57 
 58 static guint signals[LAST_SIGNAL] = { 0 };
 59 
 60 G_DEFINE_TYPE (RBSyncState, rb_sync_state, G_TYPE_OBJECT)
 61 
 62 static gboolean
 63 entry_is_undownloaded_podcast (RhythmDBEntry *entry)
 64 {
 65 	if (rhythmdb_entry_get_entry_type (entry) == RHYTHMDB_ENTRY_TYPE_PODCAST_POST) {
 66 		return (!rb_podcast_manager_entry_downloaded (entry));
 67 	}
 68 
 69 	return FALSE;
 70 }
 71 
 72 static guint64
 73 _sum_entry_size (GHashTable *entries)
 74 {
 75 	GHashTableIter iter;
 76 	gpointer key, value;
 77 	guint64 sum = 0;
 78 
 79 	g_hash_table_iter_init (&iter, entries);
 80 	while (g_hash_table_iter_next (&iter, &key, &value)) {
 81 		RhythmDBEntry *entry = (RhythmDBEntry *)value;
 82 		sum += rhythmdb_entry_get_uint64 (entry, RHYTHMDB_PROP_FILE_SIZE);
 83 	}
 84 
 85 	return sum;
 86 }
 87 
 88 static void
 89 _g_hash_table_transfer_all (GHashTable *target, GHashTable *source)
 90 {
 91 	GHashTableIter iter;
 92 	gpointer key, value;
 93 
 94 	g_hash_table_iter_init (&iter, source);
 95 	while (g_hash_table_iter_next (&iter, &key, &value)) {
 96 		g_hash_table_insert (target, key, value);
 97 		g_hash_table_iter_steal (&iter);
 98 	}
 99 }
100 
101 char *
102 rb_sync_state_make_track_uuid  (RhythmDBEntry *entry)
103 {
104 	/* This function is for hashing the two databases for syncing. */
105 	GString *str = g_string_new ("");
106 	char *result;
107 
108 	/*
109 	 * possible improvements here:
110 	 * - use musicbrainz track ID if known (maybe not a great idea?)
111 	 * - fuzz the duration a bit (round to nearest 5 seconds?) to catch slightly
112 	 *   different encodings of the same track
113 	 * - maybe don't include genre, since there's no canonical genre for anything
114 	 */
115 
116 	g_string_printf (str, "%s%s%s%s%lu%lu%lu",
117 			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE),
118 			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST),
119 			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_GENRE),
120 			 rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM),
121 			 rhythmdb_entry_get_ulong  (entry, RHYTHMDB_PROP_DURATION),
122 			 rhythmdb_entry_get_ulong  (entry, RHYTHMDB_PROP_TRACK_NUMBER),
123 			 rhythmdb_entry_get_ulong  (entry, RHYTHMDB_PROP_DISC_NUMBER));
124 
125 	/* not sure why we md5 this.  how does it help? */
126 	result = g_compute_checksum_for_string (G_CHECKSUM_MD5, str->str, str->len);
127 
128 	g_string_free (str, TRUE);
129 
130 	return result;
131 }
132 
133 static void
134 free_sync_lists (RBSyncState *state)
135 {
136 	rb_list_destroy_free (state->sync_to_add, (GDestroyNotify) rhythmdb_entry_unref);
137 	rb_list_destroy_free (state->sync_to_remove, (GDestroyNotify) rhythmdb_entry_unref);
138 	state->sync_to_add = NULL;
139 	state->sync_to_remove = NULL;
140 }
141 
142 typedef struct {
143 	GHashTable *target;
144 	GList *result;
145 	guint64 bytes;
146 	guint64 duration;
147 } BuildSyncListData;
148 
149 static void
150 build_sync_list_cb (char *uuid, RhythmDBEntry *entry, BuildSyncListData *data)
151 {
152 	guint64 bytes;
153 	gulong duration;
154 
155 	if (g_hash_table_lookup (data->target, uuid) != NULL) {
156 		/* already present */
157 		return;
158 	}
159 
160 	bytes = rhythmdb_entry_get_uint64 (entry, RHYTHMDB_PROP_FILE_SIZE);
161 	duration = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DURATION);
162 	rb_debug ("adding %s (%" G_GINT64_FORMAT " bytes); id %s to sync list",
163 		  rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION),
164 		  bytes,
165 		  uuid);
166 	data->bytes += bytes;
167 	data->duration += duration;
168 	data->result = g_list_prepend (data->result, rhythmdb_entry_ref (entry));
169 }
170 
171 
172 static gboolean
173 hash_table_insert_from_tree_model_cb (GtkTreeModel *query_model,
174 				      GtkTreePath  *path,
175 				      GtkTreeIter  *iter,
176 				      GHashTable   *target)
177 {
178 	RhythmDBEntry *entry;
179 
180 	entry = rhythmdb_query_model_iter_to_entry (RHYTHMDB_QUERY_MODEL (query_model), iter);
181 	if (!entry_is_undownloaded_podcast (entry)) {
182 		g_hash_table_insert (target,
183 				     rb_sync_state_make_track_uuid (entry),
184 				     rhythmdb_entry_ref (entry));
185 	}
186 
187 	return FALSE;
188 }
189 
190 static void
191 itinerary_insert_all_of_type (RhythmDB *db,
192 			      RhythmDBEntryType *entry_type,
193 			      GHashTable *target)
194 {
195 	GtkTreeModel *query_model;
196 
197 	query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
198 	rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
199 				RHYTHMDB_QUERY_PROP_EQUALS,
200 				RHYTHMDB_PROP_TYPE, entry_type,
201 				RHYTHMDB_QUERY_END);
202 
203 	gtk_tree_model_foreach (query_model,
204 				(GtkTreeModelForeachFunc) hash_table_insert_from_tree_model_cb,
205 				target);
206 }
207 
208 static void
209 itinerary_insert_some_playlists (RBSyncState *state,
210 				 GHashTable *target)
211 {
212 	GList *list_iter;
213 	GList *playlists;
214 	RBPlaylistManager *playlist_manager;
215 	RBShell *shell;
216 
217 	g_object_get (state->priv->source, "shell", &shell, NULL);
218 	g_object_get (shell, "playlist-manager", &playlist_manager, NULL);
219 	playlists = rb_playlist_manager_get_playlists (playlist_manager);
220 	g_object_unref (playlist_manager);
221 	g_object_unref (shell);
222 
223 	for (list_iter = playlists; list_iter; list_iter = list_iter->next) {
224 		gchar *name;
225 
226 		g_object_get (list_iter->data, "name", &name, NULL);
227 
228 		/* See if we should sync it */
229 		if (rb_sync_settings_sync_group (state->priv->sync_settings, SYNC_CATEGORY_MUSIC, name)) {
230 			GtkTreeModel *query_model;
231 
232 			rb_debug ("adding entries from playlist %s to itinerary", name);
233 			g_object_get (RB_SOURCE (list_iter->data), "base-query-model", &query_model, NULL);
234 			gtk_tree_model_foreach (query_model,
235 						(GtkTreeModelForeachFunc) hash_table_insert_from_tree_model_cb,
236 						target);
237 			g_object_unref (query_model);
238 		} else {
239 			rb_debug ("not adding playlist %s to itinerary", name);
240 		}
241 
242 		g_free (name);
243 	}
244 
245 	g_list_free (playlists);
246 }
247 
248 static void
249 itinerary_insert_some_podcasts (RBSyncState *state,
250 				RhythmDB *db,
251 				GHashTable *target)
252 {
253 	GList *podcasts;
254 	GList *i;
255 
256 	podcasts = rb_sync_settings_get_enabled_groups (state->priv->sync_settings, SYNC_CATEGORY_PODCAST);
257 	for (i = podcasts; i != NULL; i = i->next) {
258 		GtkTreeModel *query_model;
259 		rb_debug ("adding entries from podcast %s to itinerary", (char *)i->data);
260 		query_model = GTK_TREE_MODEL (rhythmdb_query_model_new_empty (db));
261 		rhythmdb_do_full_query (db, RHYTHMDB_QUERY_RESULTS (query_model),
262 					RHYTHMDB_QUERY_PROP_EQUALS,
263 					RHYTHMDB_PROP_TYPE, RHYTHMDB_ENTRY_TYPE_PODCAST_POST,
264 					RHYTHMDB_QUERY_PROP_EQUALS,
265 					RHYTHMDB_PROP_SUBTITLE, i->data,
266 					RHYTHMDB_QUERY_END);
267 
268 		/* TODO: exclude undownloaded episodes, sort by post date, set limit, optionally exclude things with play count > 0
269 		 * RHYTHMDB_QUERY_PROP_NOT_EQUAL, RHYTHMDB_PROP_MOUNTPOINT, NULL,	(will this work?)
270 		 * RHYTHMDB_QUERY_PROP_NOT_EQUAL, RHYTHMDB_PROP_STATUS, RHYTHMDB_PODCAST_STATUS_ERROR,
271 		 *
272 		 * RHYTHMDB_QUERY_PROP_EQUALS, RHYTHMDB_PROP_PLAYCOUNT, 0
273 		 */
274 
275 		gtk_tree_model_foreach (query_model,
276 					(GtkTreeModelForeachFunc) hash_table_insert_from_tree_model_cb,
277 					target);
278 		g_object_unref (query_model);
279 	}
280 }
281 
282 static GHashTable *
283 build_sync_itinerary (RBSyncState *state)
284 {
285 	RBShell *shell;
286 	RhythmDB *db;
287 	GHashTable *itinerary;
288 
289 	rb_debug ("building itinerary hash");
290 
291 	g_object_get (state->priv->source, "shell", &shell, NULL);
292 	g_object_get (shell, "db", &db, NULL);
293 
294 	itinerary = g_hash_table_new_full (g_str_hash,
295 					   g_str_equal,
296 					   g_free,
297 					   (GDestroyNotify)rhythmdb_entry_unref);
298 
299 	if (rb_sync_settings_sync_category (state->priv->sync_settings, SYNC_CATEGORY_MUSIC) ||
300 	    rb_sync_settings_sync_group (state->priv->sync_settings, SYNC_CATEGORY_MUSIC, SYNC_GROUP_ALL_MUSIC)) {
301 		rb_debug ("adding all music to the itinerary");
302 		itinerary_insert_all_of_type (db, RHYTHMDB_ENTRY_TYPE_SONG, itinerary);
303 	} else if (rb_sync_settings_has_enabled_groups (state->priv->sync_settings, SYNC_CATEGORY_MUSIC)) {
304 		rb_debug ("adding selected playlists to the itinerary");
305 		itinerary_insert_some_playlists (state, itinerary);
306 	}
307 
308 	state->sync_music_size = _sum_entry_size (itinerary);
309 
310 	if (rb_sync_settings_sync_category (state->priv->sync_settings, SYNC_CATEGORY_PODCAST)) {
311 		rb_debug ("adding all podcasts to the itinerary");
312 		/* TODO: when we get #episodes/not-if-played settings, use
313 		 * equivalent of insert_some_podcasts, iterating through all feeds
314 		 * (use a query for all entries of type PODCAST_FEED to find them)
315 		 */
316 		itinerary_insert_all_of_type (db, RHYTHMDB_ENTRY_TYPE_PODCAST_POST, itinerary);
317 	} else if (rb_sync_settings_has_enabled_groups (state->priv->sync_settings, SYNC_CATEGORY_PODCAST)) {
318 		rb_debug ("adding selected podcasts to the itinerary");
319 		itinerary_insert_some_podcasts (state, db, itinerary);
320 	}
321 
322 	state->sync_podcast_size = _sum_entry_size (itinerary) - state->sync_music_size;
323 
324 	g_object_unref (shell);
325 	g_object_unref (db);
326 
327 	rb_debug ("finished building itinerary hash; has %d entries", g_hash_table_size (itinerary));
328 	return itinerary;
329 }
330 
331 static GHashTable *
332 build_device_state (RBSyncState *state)
333 {
334 	GHashTable *device;
335 	GHashTable *entries;
336 
337 	rb_debug ("building device contents hash");
338 
339 	device = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)rhythmdb_entry_unref);
340 
341 	rb_debug ("getting music entries from device");
342 	entries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) rhythmdb_entry_unref);
343 
344 	rb_media_player_source_get_entries (state->priv->source, SYNC_CATEGORY_MUSIC, entries);
345 	/*klass->impl_get_entries (source, SYNC_CATEGORY_MUSIC, entries);*/
346 	state->total_music_size = _sum_entry_size (entries);
347 	if (rb_sync_settings_sync_category (state->priv->sync_settings, SYNC_CATEGORY_MUSIC) ||
348 	    rb_sync_settings_has_enabled_groups (state->priv->sync_settings, SYNC_CATEGORY_MUSIC)) {
349 		_g_hash_table_transfer_all (device, entries);
350 	}
351 	g_hash_table_destroy (entries);
352 	rb_debug ("done getting music entries from device");
353 
354 	rb_debug ("getting podcast entries from device");
355 	entries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) rhythmdb_entry_unref);
356 	rb_media_player_source_get_entries (state->priv->source, SYNC_CATEGORY_PODCAST, entries);
357 	/*klass->impl_get_entries (source, SYNC_CATEGORY_PODCAST, entries);*/
358 	state->total_podcast_size = _sum_entry_size (entries);
359 	if (rb_sync_settings_sync_category (state->priv->sync_settings, SYNC_CATEGORY_PODCAST) ||
360 	    rb_sync_settings_has_enabled_groups (state->priv->sync_settings, SYNC_CATEGORY_PODCAST)) {
361 		_g_hash_table_transfer_all (device, entries);
362 	}
363 	g_hash_table_destroy (entries);
364 	rb_debug ("done getting podcast entries from device");
365 
366 	rb_debug ("done building device contents hash; has %d entries", g_hash_table_size (device));
367 	return device;
368 }
369 
370 void
371 rb_sync_state_update (RBSyncState *state)
372 {
373 	GHashTable *device;
374 	GHashTable *itinerary;
375 	BuildSyncListData data;
376 	GList *list_iter;
377 	gint64 add_size = 0;
378 	gint64 remove_size = 0;
379 
380 	/* clear existing state */
381 	free_sync_lists (state);
382 
383 	/* figure out what we want on the device and what's already there */
384 	itinerary = build_sync_itinerary (state);
385 	device = build_device_state (state);
386 
387 	/* figure out what to add to the device */
388 	rb_debug ("building list of files to transfer to device");
389 	data.target = device;
390 	data.result = NULL;
391 	g_hash_table_foreach (itinerary, (GHFunc)build_sync_list_cb, &data);
392 	state->sync_to_add = data.result;
393 	state->sync_add_size = data.bytes;
394 	state->sync_add_count = g_list_length (state->sync_to_add);
395 	rb_debug ("decided to transfer %d files (%" G_GINT64_FORMAT" bytes) to the device",
396 		  state->sync_add_count,
397 		  state->sync_add_size);
398 
399 	/* and what to remove */
400 	rb_debug ("building list of files to remove from device");
401 	data.target = itinerary;
402 	data.result = NULL;
403 	g_hash_table_foreach (device, (GHFunc)build_sync_list_cb, &data);
404 	state->sync_to_remove = data.result;
405 	state->sync_remove_size = data.bytes;
406 	state->sync_remove_count = g_list_length (state->sync_to_remove);
407 	rb_debug ("decided to remove %d files (%" G_GINT64_FORMAT" bytes) from the device",
408 		  state->sync_remove_count,
409 		  state->sync_remove_size);
410 
411 	state->sync_keep_count = g_hash_table_size (device) - g_list_length (state->sync_to_remove);
412 	rb_debug ("keeping %d files on the device", state->sync_keep_count);
413 
414 	g_hash_table_destroy (device);
415 	g_hash_table_destroy (itinerary);
416 
417 	/* calculate space requirements */
418 	for (list_iter = state->sync_to_add; list_iter; list_iter = list_iter->next) {
419 		add_size += rhythmdb_entry_get_uint64 (list_iter->data, RHYTHMDB_PROP_FILE_SIZE);
420 	}
421 
422 	for (list_iter = state->sync_to_remove; list_iter; list_iter = list_iter->next) {
423 		remove_size += rhythmdb_entry_get_uint64 (list_iter->data, RHYTHMDB_PROP_FILE_SIZE);
424 	}
425 
426 	state->sync_space_needed = rb_media_player_source_get_capacity (state->priv->source) -
427 				   rb_media_player_source_get_free_space (state->priv->source);
428 	rb_debug ("current space used: %" G_GINT64_FORMAT " bytes; adding %" G_GINT64_FORMAT ", removing %" G_GINT64_FORMAT,
429 		  state->sync_space_needed,
430 		  add_size,
431 		  remove_size);
432 	state->sync_space_needed = state->sync_space_needed + add_size - remove_size;
433 	rb_debug ("space used after sync: %" G_GINT64_FORMAT " bytes", state->sync_space_needed);
434 
435 	g_signal_emit (state, signals[UPDATED], 0);
436 }
437 
438 static void
439 sync_settings_updated (RBSyncSettings *settings, RBSyncState *state)
440 {
441 	rb_debug ("sync settings updated, updating state");
442 	rb_sync_state_update (state);
443 }
444 
445 
446 RBSyncState *
447 rb_sync_state_new (RBMediaPlayerSource *source, RBSyncSettings *settings)
448 {
449 	GObject *state;
450 	state = g_object_new (RB_TYPE_SYNC_STATE,
451 			      "source", source,
452 			      "sync-settings", settings,
453 			      NULL);
454 	return RB_SYNC_STATE (state);
455 }
456 
457 
458 static void
459 rb_sync_state_init (RBSyncState *state)
460 {
461 	state->priv = G_TYPE_INSTANCE_GET_PRIVATE (state, RB_TYPE_SYNC_STATE, RBSyncStatePrivate);
462 }
463 
464 static void
465 impl_constructed (GObject *object)
466 {
467 	RBSyncState *state = RB_SYNC_STATE (object);
468 
469 	rb_sync_state_update (state);
470 
471 	g_signal_connect_object (state->priv->sync_settings,
472 				 "updated",
473 				 G_CALLBACK (sync_settings_updated),
474 				 state, 0);
475 
476 	RB_CHAIN_GOBJECT_METHOD(rb_sync_state_parent_class, constructed, object);
477 }
478 
479 static void
480 impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
481 {
482 	RBSyncState *state = RB_SYNC_STATE (object);
483 	switch (prop_id) {
484 	case PROP_SOURCE:
485 		state->priv->source = g_value_get_object (value);
486 		break;
487 	case PROP_SYNC_SETTINGS:
488 		state->priv->sync_settings = g_value_get_object (value);
489 		break;
490 	default:
491 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
492 		break;
493 	}
494 }
495 
496 static void
497 impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
498 {
499 	RBSyncState *state = RB_SYNC_STATE (object);
500 	switch (prop_id) {
501 	case PROP_SOURCE:
502 		g_value_set_object (value, state->priv->source);
503 		break;
504 	case PROP_SYNC_SETTINGS:
505 		g_value_set_object (value, state->priv->sync_settings);
506 		break;
507 	default:
508 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
509 		break;
510 	}
511 }
512 
513 static void
514 impl_finalize (GObject *object)
515 {
516 	RBSyncState *state = RB_SYNC_STATE (object);
517 
518 	free_sync_lists (state);
519 
520 	G_OBJECT_CLASS (rb_sync_state_parent_class)->finalize (object);
521 }
522 
523 static void
524 rb_sync_state_class_init (RBSyncStateClass *klass)
525 {
526 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
527 
528 	object_class->finalize = impl_finalize;
529 	object_class->constructed = impl_constructed;
530 	object_class->set_property = impl_set_property;
531 	object_class->get_property = impl_get_property;
532 
533 	g_object_class_install_property (object_class,
534 					 PROP_SOURCE,
535 					 g_param_spec_object ("source",
536 							      "source",
537 							      "RBMediaPlayerSource instance",
538 							      RB_TYPE_MEDIA_PLAYER_SOURCE,
539 							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
540 	g_object_class_install_property (object_class,
541 					 PROP_SYNC_SETTINGS,
542 					 g_param_spec_object ("sync-settings",
543 							      "sync-settings",
544 							      "RBSyncSettings instance",
545 							      RB_TYPE_SYNC_SETTINGS,
546 							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
547 
548 	signals[UPDATED] = g_signal_new ("updated",
549 					 RB_TYPE_SYNC_STATE,
550 					 G_SIGNAL_RUN_LAST,
551 					 G_STRUCT_OFFSET (RBSyncStateClass, updated),
552 					 NULL, NULL,
553 					 g_cclosure_marshal_VOID__VOID,
554 					 G_TYPE_NONE,
555 					 0);
556 
557 	g_type_class_add_private (object_class, sizeof (RBSyncStatePrivate));
558 }