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 }