No issues found
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2011 Jonathan Matthew
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * The Rhythmbox authors hereby grant permission for non-GPL compatible
11 * GStreamer plugins to be used and distributed together with GStreamer
12 * and Rhythmbox. This permission is above and beyond the permissions granted
13 * by the GPL license by which Rhythmbox is covered. If you modify this code
14 * you may extend this exception to your version of the code, but you are not
15 * obligated to do so. If you do not wish to do so, delete this exception
16 * statement from your version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program; if not, write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
26 *
27 */
28
29 #include "config.h"
30
31 #include <string.h>
32 #include <gtk/gtk.h>
33 #include <glib/gi18n.h>
34 #include <grilo.h>
35
36 #include "rhythmdb.h"
37 #include "rb-shell.h"
38 #include "rb-shell-player.h"
39 #include "rb-grilo-source.h"
40 #include "rb-util.h"
41 #include "rb-debug.h"
42 #include "rb-file-helpers.h"
43 #include "rb-gst-media-types.h"
44 #include "rb-search-entry.h"
45
46 /* number of items to check before giving up on finding any
47 * of a particular type
48 */
49 #define CONTAINER_GIVE_UP_POINT 100
50
51 /* maximum number of tracks to fetch before stopping and
52 * requiring the user to ask for more.
53 */
54 #define CONTAINER_MAX_TRACKS 1000
55
56 /* number of items to fetch at once */
57 #define CONTAINER_FETCH_SIZE 50
58
59 enum {
60 CONTAINER_UNKNOWN_MEDIA = 0,
61 CONTAINER_NO_MEDIA,
62 CONTAINER_HAS_MEDIA
63 };
64
65 enum
66 {
67 PROP_0,
68 PROP_GRILO_SOURCE
69 };
70
71 static void rb_grilo_source_dispose (GObject *object);
72 static void rb_grilo_source_finalize (GObject *object);
73 static void rb_grilo_source_constructed (GObject *object);
74 static void impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);
75 static void impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec);
76
77 static void browser_selection_changed_cb (GtkTreeSelection *selection, RBGriloSource *source);
78 static void browser_row_expanded_cb (GtkTreeView *tree_view, GtkTreeIter *iter, GtkTreePath *path, RBGriloSource *source);
79 static void scroll_adjust_changed_cb (GtkAdjustment *adjustment, RBGriloSource *source);
80 static void scroll_adjust_value_changed_cb (GtkAdjustment *adjustment, RBGriloSource *source);
81 static gboolean maybe_expand_container (RBGriloSource *source);
82 static void fetch_more_cb (GtkInfoBar *bar, gint response, RBGriloSource *source);
83 static void search_cb (RBSearchEntry *search, const char *text, RBGriloSource *source);
84 static void notify_sort_order_cb (GObject *object, GParamSpec *pspec, RBGriloSource *source);
85
86 static void impl_delete_thyself (RBDisplayPage *page);
87 static void impl_selected (RBDisplayPage *page);
88 static void impl_deselected (RBDisplayPage *page);
89
90 static RBEntryView *impl_get_entry_view (RBSource *source);
91
92 struct _RBGriloSourcePrivate
93 {
94 GrlSource *grilo_source;
95 GList *grilo_keys;
96
97 RhythmDBEntryType *entry_type;
98
99 /* some widgets and things */
100 GtkWidget *paned;
101 RhythmDBQueryModel *query_model;
102 RBEntryView *entry_view;
103 GtkTreeStore *browser_model;
104 GtkWidget *browser_view;
105 gboolean done_initial_browse;
106 GtkWidget *info_bar;
107 GtkWidget *info_bar_label;
108 RBSearchEntry *search_entry;
109
110 /* current browsing operation (should allow multiple concurrent ops?) */
111 guint browse_op;
112 GrlMedia *browse_container;
113 GtkTreeIter browse_container_iter;
114 guint browse_position;
115 gboolean browse_got_results;
116 gboolean browse_got_media;
117 guint maybe_expand_idle;
118
119 /* current media browse operation */
120 guint media_browse_op;
121 char *search_text;
122 GrlMedia *media_browse_container;
123 GtkTreeIter media_browse_container_iter;
124 guint media_browse_position;
125 gboolean media_browse_got_results;
126 gboolean media_browse_got_containers;
127 guint media_browse_limit;
128
129 RhythmDB *db;
130 };
131
132 G_DEFINE_DYNAMIC_TYPE (RBGriloSource, rb_grilo_source, RB_TYPE_SOURCE)
133
134 /* entry type */
135
136 G_DEFINE_DYNAMIC_TYPE (RBGriloEntryType, rb_grilo_entry_type, RHYTHMDB_TYPE_ENTRY_TYPE);
137
138 static void
139 rb_grilo_entry_type_destroy_entry (RhythmDBEntryType *etype, RhythmDBEntry *entry)
140 {
141 RBGriloEntryData *data;
142
143 data = RHYTHMDB_ENTRY_GET_TYPE_DATA (entry, RBGriloEntryData);
144 g_object_unref (data->grilo_data);
145 if (data->grilo_container != NULL)
146 g_object_unref (data->grilo_container);
147 }
148
149 static void
150 rb_grilo_entry_type_class_init (RBGriloEntryTypeClass *klass)
151 {
152 RhythmDBEntryTypeClass *etype_class = RHYTHMDB_ENTRY_TYPE_CLASS (klass);
153 etype_class->can_sync_metadata = (RhythmDBEntryTypeBooleanFunc) rb_true_function;
154 etype_class->sync_metadata = (RhythmDBEntryTypeSyncFunc) rb_null_function;
155 etype_class->destroy_entry = rb_grilo_entry_type_destroy_entry;
156 }
157
158 static void
159 rb_grilo_entry_type_class_finalize (RBGriloEntryTypeClass *klass)
160 {
161 }
162
163 static void
164 rb_grilo_entry_type_init (RBGriloEntryType *etype)
165 {
166 }
167
168 static void
169 rb_grilo_source_class_init (RBGriloSourceClass *klass)
170 {
171 GObjectClass *object_class = G_OBJECT_CLASS (klass);
172 RBDisplayPageClass *page_class = RB_DISPLAY_PAGE_CLASS (klass);
173 RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
174
175 object_class->constructed = rb_grilo_source_constructed;
176 object_class->dispose = rb_grilo_source_dispose;
177 object_class->finalize = rb_grilo_source_finalize;
178 object_class->set_property = impl_set_property;
179 object_class->get_property = impl_get_property;
180
181 page_class->delete_thyself = impl_delete_thyself;
182 page_class->selected = impl_selected;
183 page_class->deselected = impl_deselected;
184
185 source_class->impl_get_entry_view = impl_get_entry_view;
186
187 g_object_class_install_property (object_class,
188 PROP_GRILO_SOURCE,
189 g_param_spec_object ("grilo-source",
190 "grilo source",
191 "grilo source object",
192 GRL_TYPE_SOURCE,
193 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
194
195 g_type_class_add_private (klass, sizeof (RBGriloSourcePrivate));
196 }
197
198 static void
199 rb_grilo_source_class_finalize (RBGriloSourceClass *klass)
200 {
201 }
202
203 static void
204 rb_grilo_source_init (RBGriloSource *self)
205 {
206 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, RB_TYPE_GRILO_SOURCE, RBGriloSourcePrivate);
207 }
208
209 static void
210 rb_grilo_source_finalize (GObject *object)
211 {
212 RBGriloSource *source = RB_GRILO_SOURCE (object);
213
214 g_free (source->priv->search_text);
215
216 g_list_free (source->priv->grilo_keys);
217
218 G_OBJECT_CLASS (rb_grilo_source_parent_class)->finalize (object);
219 }
220
221 static void
222 rb_grilo_source_dispose (GObject *object)
223 {
224 RBGriloSource *source = RB_GRILO_SOURCE (object);
225
226 if (source->priv->browse_op != 0) {
227 grl_operation_cancel (source->priv->browse_op);
228 source->priv->browse_op = 0;
229 }
230
231 if (source->priv->media_browse_op != 0) {
232 grl_operation_cancel (source->priv->media_browse_op);
233 source->priv->media_browse_op = 0;
234 }
235
236 if (source->priv->query_model != NULL) {
237 g_object_unref (source->priv->query_model);
238 source->priv->query_model = NULL;
239 }
240
241 if (source->priv->entry_type != NULL) {
242 g_object_unref (source->priv->entry_type);
243 source->priv->entry_type = NULL;
244 }
245
246 if (source->priv->maybe_expand_idle != 0) {
247 g_source_remove (source->priv->maybe_expand_idle);
248 source->priv->maybe_expand_idle = 0;
249 }
250
251 G_OBJECT_CLASS (rb_grilo_source_parent_class)->dispose (object);
252 }
253
254 static void
255 rb_grilo_source_constructed (GObject *object)
256 {
257 RBGriloSource *source;
258 RBShell *shell;
259 RBShellPlayer *shell_player;
260 const GList *source_keys;
261 GtkTreeViewColumn *column;
262 GtkCellRenderer *renderer;
263 GtkTreeSelection *selection;
264 GtkWidget *scrolled;
265 GtkWidget *browserbox;
266 GtkWidget *vbox;
267 GtkWidget *mainbox;
268 GtkAdjustment *adjustment;
269
270 RB_CHAIN_GOBJECT_METHOD (rb_grilo_source_parent_class, constructed, object);
271 source = RB_GRILO_SOURCE (object);
272
273 g_object_get (source, "shell", &shell, NULL);
274 g_object_get (shell,
275 "db", &source->priv->db,
276 "shell-player", &shell_player,
277 NULL);
278 g_object_unref (shell);
279
280 g_object_get (source, "entry-type", &source->priv->entry_type, NULL);
281
282 source->priv->entry_view = rb_entry_view_new (source->priv->db, G_OBJECT (shell_player), TRUE, FALSE);
283 g_object_unref (shell_player);
284 g_signal_connect (source->priv->entry_view,
285 "notify::sort-order",
286 G_CALLBACK (notify_sort_order_cb),
287 source);
288
289 source_keys = grl_source_supported_keys (source->priv->grilo_source);
290
291 if (g_list_find ((GList *)source_keys, GUINT_TO_POINTER(GRL_METADATA_KEY_TRACK_NUMBER))) {
292 rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_TRACK_NUMBER, FALSE);
293 source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
294 GUINT_TO_POINTER(GRL_METADATA_KEY_TRACK_NUMBER));
295 }
296
297 if (g_list_find ((GList *)source_keys, GUINT_TO_POINTER(GRL_METADATA_KEY_TITLE))) {
298 rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_TITLE, TRUE);
299 source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
300 GUINT_TO_POINTER(GRL_METADATA_KEY_TITLE));
301 }
302
303 if (g_list_find ((GList *)source_keys, GUINT_TO_POINTER(GRL_METADATA_KEY_GENRE))) {
304 rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_GENRE, FALSE);
305 source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
306 GUINT_TO_POINTER(GRL_METADATA_KEY_GENRE));
307 }
308 if (g_list_find ((GList *)source_keys, GUINT_TO_POINTER(GRL_METADATA_KEY_ARTIST))) {
309 rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_ARTIST, FALSE);
310 source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
311 GUINT_TO_POINTER(GRL_METADATA_KEY_ARTIST));
312 }
313 if (g_list_find ((GList *)source_keys, GUINT_TO_POINTER(GRL_METADATA_KEY_ALBUM))) {
314 rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_ALBUM, FALSE);
315 source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
316 GUINT_TO_POINTER(GRL_METADATA_KEY_ALBUM));
317 }
318 /*
319 if (g_list_find ((GList *)source_keys, GUINT_TO_POINTER(GRL_METADATA_KEY_DATE))) {
320 rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_YEAR, FALSE);
321 source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
322 GUINT_TO_POINTER(GRL_METADATA_KEY_DATE));
323 }
324 */
325 if (g_list_find ((GList *)source_keys, GUINT_TO_POINTER(GRL_METADATA_KEY_DURATION))) {
326 rb_entry_view_append_column (source->priv->entry_view, RB_ENTRY_VIEW_COL_DURATION, FALSE);
327 source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys,
328 GUINT_TO_POINTER(GRL_METADATA_KEY_DURATION));
329 }
330
331 source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys, GUINT_TO_POINTER(GRL_METADATA_KEY_CHILDCOUNT));
332 source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys, GUINT_TO_POINTER(GRL_METADATA_KEY_URL));
333 source->priv->grilo_keys = g_list_prepend (source->priv->grilo_keys, GUINT_TO_POINTER(GRL_METADATA_KEY_THUMBNAIL));
334
335 /* probably add an image column too? */
336 source->priv->browser_model = gtk_tree_store_new (4, GRL_TYPE_MEDIA, G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT);
337 source->priv->browser_view = gtk_tree_view_new ();
338 gtk_tree_view_set_model (GTK_TREE_VIEW (source->priv->browser_view), GTK_TREE_MODEL (source->priv->browser_model));
339
340 column = gtk_tree_view_column_new ();
341 renderer = gtk_cell_renderer_text_new ();
342 gtk_tree_view_column_set_title (column, _("Browse"));
343 gtk_tree_view_column_pack_start (column, renderer, FALSE);
344 gtk_tree_view_column_add_attribute (column, renderer, "text", 1);
345 gtk_tree_view_column_set_expand (column, TRUE);
346 gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
347
348 gtk_tree_view_append_column (GTK_TREE_VIEW (source->priv->browser_view), column);
349 gtk_tree_view_set_show_expanders (GTK_TREE_VIEW (source->priv->browser_view), TRUE);
350 gtk_tree_view_set_expander_column (GTK_TREE_VIEW (source->priv->browser_view), column);
351 gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (source->priv->browser_view), TRUE);
352 gtk_tree_view_set_fixed_height_mode (GTK_TREE_VIEW (source->priv->browser_view), TRUE);
353
354 g_signal_connect (source->priv->browser_view, "row-expanded", G_CALLBACK (browser_row_expanded_cb), source);
355 selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (source->priv->browser_view));
356 gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE); /* should be multiple eventually */
357 g_signal_connect (selection, "changed", G_CALLBACK (browser_selection_changed_cb), source);
358
359 scrolled = gtk_scrolled_window_new (NULL, NULL);
360 gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled), GTK_SHADOW_IN);
361 adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (scrolled));
362 g_signal_connect (adjustment, "changed", G_CALLBACK (scroll_adjust_changed_cb), source);
363 g_signal_connect (adjustment, "value-changed", G_CALLBACK (scroll_adjust_value_changed_cb), source);
364
365 browserbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
366
367 /* search bar (if the source supports searching) */
368 if (grl_source_supported_operations (source->priv->grilo_source) & GRL_OP_SEARCH) {
369 source->priv->search_entry = rb_search_entry_new (FALSE);
370 g_object_set (source->priv->search_entry, "explicit-mode", TRUE, NULL);
371 g_signal_connect (source->priv->search_entry, "search", G_CALLBACK (search_cb), source);
372 g_signal_connect (source->priv->search_entry, "activate", G_CALLBACK (search_cb), source);
373 gtk_box_pack_start (GTK_BOX (browserbox), GTK_WIDGET (source->priv->search_entry), FALSE, FALSE, 6);
374 }
375 gtk_container_add (GTK_CONTAINER (scrolled), source->priv->browser_view);
376 gtk_box_pack_start (GTK_BOX (browserbox), scrolled, TRUE, TRUE, 0);
377
378 mainbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
379 gtk_box_pack_start (GTK_BOX (source), mainbox, TRUE, TRUE, 0);
380
381 /* info bar */
382 source->priv->info_bar_label = gtk_label_new ("");
383 source->priv->info_bar = gtk_info_bar_new ();
384 gtk_info_bar_set_message_type (GTK_INFO_BAR (source->priv->info_bar), GTK_MESSAGE_INFO);
385 gtk_info_bar_add_button (GTK_INFO_BAR (source->priv->info_bar), _("Fetch more tracks"), GTK_RESPONSE_OK);
386 gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (GTK_INFO_BAR (source->priv->info_bar))),
387 source->priv->info_bar_label);
388 gtk_widget_show (GTK_WIDGET (source->priv->info_bar_label));
389 gtk_widget_set_no_show_all (GTK_WIDGET (source->priv->info_bar), TRUE);
390 g_signal_connect (source->priv->info_bar, "response", G_CALLBACK (fetch_more_cb), source);
391
392 /* don't allow the browser to be hidden? */
393 source->priv->paned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL);
394 rb_source_bind_settings (RB_SOURCE (source), GTK_WIDGET (source->priv->entry_view), source->priv->paned, NULL);
395 gtk_paned_pack1 (GTK_PANED (source->priv->paned), browserbox, FALSE, FALSE);
396
397 vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
398 gtk_box_pack_start (GTK_BOX (vbox), GTK_WIDGET (source->priv->entry_view), TRUE, TRUE, 0);
399 gtk_box_pack_start (GTK_BOX (vbox), source->priv->info_bar, FALSE, FALSE, 0);
400 gtk_paned_pack2 (GTK_PANED (source->priv->paned), vbox, TRUE, FALSE);
401
402 gtk_box_pack_start (GTK_BOX (mainbox), source->priv->paned, TRUE, TRUE, 0);
403
404 gtk_widget_show_all (GTK_WIDGET (source));
405 }
406
407 RBSource *
408 rb_grilo_source_new (GObject *plugin, GrlSource *grilo_source)
409 {
410 GObject *source;
411 RBShell *shell;
412 GSettings *settings;
413 RhythmDBEntryType *entry_type;
414 RhythmDB *db;
415 char *name;
416
417 name = g_strdup_printf ("grilo:%s", grl_source_get_id (grilo_source));
418
419 g_object_get (plugin, "object", &shell, NULL);
420 g_object_get (shell, "db", &db, NULL);
421 entry_type = g_object_new (rb_grilo_entry_type_get_type (),
422 "db", db,
423 "name", name,
424 "save-to-disk", FALSE,
425 "category", RHYTHMDB_ENTRY_NORMAL,
426 "type-data-size", sizeof(RBGriloEntryData),
427 NULL);
428 rhythmdb_register_entry_type (db, entry_type);
429 g_object_unref (db);
430 g_free (name);
431
432 settings = g_settings_new ("org.gnome.rhythmbox.plugins.grilo");
433 source = g_object_new (RB_TYPE_GRILO_SOURCE,
434 "name", grl_source_get_name (grilo_source),
435 "entry-type", entry_type,
436 "shell", shell,
437 "plugin", plugin,
438 "show-browser", FALSE,
439 "settings", g_settings_get_child (settings, "source"),
440 "grilo-source", grilo_source,
441 NULL);
442 g_object_unref (settings);
443
444 rb_shell_register_entry_type_for_source (shell, RB_SOURCE (source), entry_type);
445
446 g_object_unref (shell);
447 return RB_SOURCE (source);
448 }
449
450 static void
451 impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
452 {
453 RBGriloSource *source = RB_GRILO_SOURCE (object);
454 switch (prop_id) {
455 case PROP_GRILO_SOURCE:
456 source->priv->grilo_source = g_value_get_object (value);
457 break;
458 default:
459 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
460 break;
461 }
462 }
463
464 static void
465 impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
466 {
467 RBGriloSource *source = RB_GRILO_SOURCE (object);
468
469 switch (prop_id) {
470 case PROP_GRILO_SOURCE:
471 g_value_set_object (value, source->priv->grilo_source);
472 break;
473 default:
474 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
475 break;
476 }
477 }
478
479 static void
480 impl_delete_thyself (RBDisplayPage *page)
481 {
482 RBGriloSource *source = RB_GRILO_SOURCE (page);
483 RhythmDBEntryType *entry_type;
484
485 if (source->priv->browse_op != 0) {
486 grl_operation_cancel (source->priv->browse_op);
487 source->priv->browse_op = 0;
488 }
489
490 if (source->priv->media_browse_op != 0) {
491 grl_operation_cancel (source->priv->media_browse_op);
492 source->priv->media_browse_op = 0;
493 }
494
495 g_object_get (source, "entry-type", &entry_type, NULL);
496 rhythmdb_entry_delete_by_type (source->priv->db, entry_type);
497 g_object_unref (entry_type);
498
499 rhythmdb_commit (source->priv->db);
500 }
501
502 void
503 _rb_grilo_source_register_type (GTypeModule *module)
504 {
505 rb_grilo_source_register_type (module);
506 rb_grilo_entry_type_register_type (module);
507 }
508
509 static GrlOperationOptions *
510 make_operation_options (RBGriloSource *source, GrlSupportedOps op, int position)
511 {
512 GrlOperationOptions *options;
513 GrlCaps *caps;
514
515 caps = grl_source_get_caps (source->priv->grilo_source, op);
516
517 options = grl_operation_options_new (caps);
518 grl_operation_options_set_skip (options, position);
519 grl_operation_options_set_count (options,
520 CONTAINER_FETCH_SIZE);
521 grl_operation_options_set_type_filter (options, GRL_TYPE_FILTER_AUDIO);
522 grl_operation_options_set_flags (options, GRL_RESOLVE_NORMAL);
523
524 return options;
525 }
526
527 /* grilo media -> rhythmdb entry */
528
529 static void
530 set_string_prop_from_key (RhythmDB *db, RhythmDBEntry *entry, RhythmDBPropType prop, GrlData *data, GrlKeyID key)
531 {
532 GValue v = {0,};
533 if (grl_data_has_key (data, key) == FALSE)
534 return;
535
536 g_value_init (&v, G_TYPE_STRING);
537 g_value_set_string (&v, grl_data_get_string (data, key));
538 rhythmdb_entry_set (db, entry, prop, &v);
539 g_value_unset (&v);
540 }
541
542 static RhythmDBEntry *
543 create_entry_for_media (RhythmDB *db, RhythmDBEntryType *entry_type, GrlData *data, GrlData *container)
544 {
545 RhythmDBEntry *entry;
546 RBGriloEntryData *entry_data;
547
548 entry = rhythmdb_entry_lookup_by_location (db, grl_media_get_url (GRL_MEDIA (data)));
549 if (entry != NULL) {
550 return entry;
551 }
552
553 rb_debug ("creating entry for %s / %s", grl_media_get_url (GRL_MEDIA (data)), grl_media_get_id (GRL_MEDIA (data)));
554
555 entry = rhythmdb_entry_new (db, entry_type, grl_media_get_url (GRL_MEDIA (data))); /* just use the url? */
556 if (entry == NULL) {
557 /* crap. */
558 return NULL;
559 }
560
561 set_string_prop_from_key (db, entry, RHYTHMDB_PROP_TITLE, data, GRL_METADATA_KEY_TITLE);
562 set_string_prop_from_key (db, entry, RHYTHMDB_PROP_ALBUM, data, GRL_METADATA_KEY_ALBUM);
563 set_string_prop_from_key (db, entry, RHYTHMDB_PROP_ARTIST, data, GRL_METADATA_KEY_ARTIST);
564 set_string_prop_from_key (db, entry, RHYTHMDB_PROP_GENRE, data, GRL_METADATA_KEY_GENRE);
565 set_string_prop_from_key (db, entry, RHYTHMDB_PROP_TITLE, data, GRL_METADATA_KEY_TITLE);
566
567 if (grl_data_has_key (data, GRL_METADATA_KEY_PUBLICATION_DATE)) {
568 /* something - grilo has this as a string? */
569 }
570
571 if (grl_data_has_key (data, GRL_METADATA_KEY_BITRATE)) {
572 GValue v = {0,};
573 g_value_init (&v, G_TYPE_ULONG);
574 g_value_set_ulong (&v, grl_data_get_int (data, GRL_METADATA_KEY_BITRATE));
575 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_BITRATE, &v);
576 g_value_unset (&v);
577 }
578
579 if (grl_data_has_key (data, GRL_METADATA_KEY_DURATION)) {
580 /* this is probably in seconds */
581 GValue v = {0,};
582 g_value_init (&v, G_TYPE_ULONG);
583 g_value_set_ulong (&v, grl_data_get_int (data, GRL_METADATA_KEY_DURATION));
584 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_DURATION, &v);
585 g_value_unset (&v);
586 }
587
588 if (grl_data_has_key (data, GRL_METADATA_KEY_MIME)) {
589 const char *media_type;
590 media_type = rb_gst_mime_type_to_media_type (grl_data_get_string (data, GRL_METADATA_KEY_MIME));
591 if (media_type) {
592 GValue v = {0,};
593 g_value_init (&v, G_TYPE_STRING);
594 g_value_set_string (&v, media_type);
595 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_MEDIA_TYPE, &v);
596 g_value_unset (&v);
597 }
598 }
599
600 if (grl_data_has_key (data, GRL_METADATA_KEY_TRACK_NUMBER)) {
601 GValue v = {0,};
602 g_value_init (&v, G_TYPE_ULONG);
603 g_value_set_ulong (&v, grl_data_get_int (data, GRL_METADATA_KEY_TRACK_NUMBER));
604 rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_TRACK_NUMBER, &v);
605 g_value_unset (&v);
606 }
607
608 /* rating and play count? */
609
610 entry_data = RHYTHMDB_ENTRY_GET_TYPE_DATA (entry, RBGriloEntryData);
611 entry_data->grilo_data = g_object_ref (data);
612 if (container != NULL) {
613 entry_data->grilo_container = g_object_ref (container);
614 }
615
616 /* might want to consider batching this */
617 rhythmdb_commit (db);
618
619 return entry;
620 }
621
622 /* container browsing */
623
624 static void browse_next (RBGriloSource *source);
625
626 static void
627 delete_marker_row (RBGriloSource *source, GtkTreeIter *iter)
628 {
629 GtkTreeIter marker_iter;
630 if (gtk_tree_model_iter_children (GTK_TREE_MODEL (source->priv->browser_model), &marker_iter, iter)) {
631 do {
632 GrlMedia *container;
633 gtk_tree_model_get (GTK_TREE_MODEL (source->priv->browser_model), &marker_iter,
634 0, &container,
635 -1);
636 if (container == NULL) {
637 gtk_tree_store_remove (GTK_TREE_STORE (source->priv->browser_model), &marker_iter);
638 break;
639 }
640 } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (source->priv->browser_model), &marker_iter));
641 }
642 }
643
644 static void
645 set_container_type (RBGriloSource *source, GtkTreeIter *iter, gboolean has_media)
646 {
647 int container_type;
648
649 gtk_tree_model_get (GTK_TREE_MODEL (source->priv->browser_model),
650 iter,
651 2, &container_type,
652 -1);
653 if (container_type == CONTAINER_UNKNOWN_MEDIA) {
654 container_type = has_media ? CONTAINER_HAS_MEDIA : CONTAINER_NO_MEDIA;
655 }
656
657 gtk_tree_store_set (source->priv->browser_model,
658 iter,
659 2, container_type,
660 -1);
661 }
662
663 static void
664 grilo_browse_cb (GrlSource *grilo_source, guint operation_id, GrlMedia *media, guint remaining, RBGriloSource *source, const GError *error)
665 {
666 if (operation_id != source->priv->browse_op) {
667 return;
668 }
669
670 if (error != NULL) {
671 /* do something? */
672 rb_debug ("got error for %s: %s", grl_source_get_name (grilo_source), error->message);
673 source->priv->browse_op = 0;
674 return;
675 }
676
677 if (media != NULL) {
678 source->priv->browse_got_results = TRUE;
679 source->priv->browse_position++;
680 }
681
682 if (media && GRL_IS_MEDIA_BOX (media)) {
683
684 GtkTreeIter new_row;
685 if (source->priv->browse_container == NULL) {
686 /* insert at the end */
687 gtk_tree_store_insert_with_values (source->priv->browser_model,
688 &new_row,
689 NULL,
690 -1,
691 0, g_object_ref (media),
692 1, grl_media_get_title (media),
693 2, CONTAINER_UNKNOWN_MEDIA,
694 3, 0,
695 -1);
696 } else {
697 int n;
698 /* insert before the expand marker row */
699 n = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (source->priv->browser_model),
700 &source->priv->browse_container_iter);
701 gtk_tree_store_insert_with_values (source->priv->browser_model,
702 &new_row,
703 &source->priv->browse_container_iter,
704 n - 1,
705 0, g_object_ref (media),
706 1, grl_media_get_title (media),
707 2, CONTAINER_UNKNOWN_MEDIA,
708 3, 0,
709 -1);
710 }
711
712 /* and insert an expand marker below it too */
713 gtk_tree_store_insert_with_values (source->priv->browser_model,
714 NULL,
715 &new_row,
716 -1,
717 0, NULL,
718 1, "...", /* needs to be translatable? */
719 2, CONTAINER_NO_MEDIA,
720 3, 0,
721 -1);
722 } else if (media && GRL_IS_MEDIA_AUDIO (media)) {
723 source->priv->browse_got_media = TRUE;
724 }
725
726 if (remaining == 0) {
727 source->priv->browse_op = 0;
728 if (source->priv->browse_got_results == FALSE &&
729 source->priv->browse_container != NULL) {
730 /* no more results for this container, so delete the marker row */
731 delete_marker_row (source, &source->priv->browse_container_iter);
732
733 set_container_type (source, &source->priv->browse_container_iter, source->priv->browse_got_media);
734 gtk_tree_store_set (source->priv->browser_model,
735 &source->priv->browse_container_iter,
736 3, -1,
737 -1);
738 } else if (source->priv->browse_container != NULL) {
739 if (source->priv->browse_position >= CONTAINER_GIVE_UP_POINT &&
740 gtk_tree_model_iter_n_children (GTK_TREE_MODEL (source->priv->browser_model),
741 &source->priv->browse_container_iter) == 1) {
742 /* no containers yet, so remove the marker row */
743 delete_marker_row (source, &source->priv->browse_container_iter);
744 } else {
745 /* store browse position for next time we want more */
746 gtk_tree_store_set (source->priv->browser_model,
747 &source->priv->browse_container_iter,
748 3, source->priv->browse_position,
749 -1);
750 maybe_expand_container (source);
751 }
752
753 } else if (source->priv->browse_got_results && source->priv->browse_container == NULL) {
754 /* get all top-level containers */
755 browse_next (source);
756 }
757 }
758 }
759
760 static void
761 browse_next (RBGriloSource *source)
762 {
763 GrlOperationOptions *options;
764 rb_debug ("next browse op for %s (%d)",
765 grl_source_get_name (source->priv->grilo_source),
766 source->priv->browse_position);
767 source->priv->browse_got_results = FALSE;
768 options = make_operation_options (source, GRL_OP_BROWSE, source->priv->browse_position);
769 source->priv->browse_op = grl_source_browse (source->priv->grilo_source,
770 source->priv->browse_container,
771 source->priv->grilo_keys,
772 options,
773 (GrlSourceResultCb) grilo_browse_cb,
774 source);
775 }
776
777 static void
778 start_browse (RBGriloSource *source, GrlMedia *container, GtkTreeIter *container_iter, int position)
779 {
780 rb_debug ("starting browse op for %s", grl_source_get_name (source->priv->grilo_source));
781
782 /* cancel existing operation? */
783 if (source->priv->browse_op != 0) {
784 grl_operation_cancel (source->priv->browse_op);
785 }
786
787 if (source->priv->browse_container != NULL) {
788 g_object_unref (source->priv->browse_container);
789 }
790 source->priv->browse_container = container;
791 if (container_iter != NULL) {
792 /* hrm, probably have to use row references here.. */
793 source->priv->browse_container_iter = *container_iter;
794 }
795 source->priv->browse_position = position;
796 source->priv->browse_got_media = FALSE;
797
798 browse_next (source);
799 }
800
801 /* media browsing */
802
803 static void media_browse_next (RBGriloSource *source);
804
805 static void
806 grilo_media_browse_cb (GrlSource *grilo_source, guint operation_id, GrlMedia *media, guint remaining, RBGriloSource *source, const GError *error)
807 {
808 if (operation_id != source->priv->media_browse_op) {
809 return;
810 }
811
812 if (error != NULL) {
813 /* do something? */
814 rb_debug ("got error for %s: %s",
815 grl_source_get_name (grilo_source),
816 error->message);
817 return;
818 }
819
820 GDK_THREADS_ENTER ();
821 if (media != NULL) {
822 source->priv->media_browse_got_results = TRUE;
823 source->priv->media_browse_position++;
824
825 if (GRL_IS_MEDIA_AUDIO (media)) {
826 RhythmDBEntry *entry;
827 entry = create_entry_for_media (source->priv->db,
828 source->priv->entry_type,
829 GRL_DATA (media),
830 GRL_DATA (source->priv->browse_container));
831 if (entry != NULL) {
832 rhythmdb_query_model_add_entry (source->priv->query_model, entry, -1);
833 }
834 } else if (GRL_IS_MEDIA_BOX (media)) {
835 source->priv->media_browse_got_containers = TRUE;
836 }
837 }
838
839 if (remaining == 0) {
840 source->priv->media_browse_op = 0;
841
842 if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (source->priv->query_model), NULL) == 0 &&
843 source->priv->media_browse_position >= CONTAINER_GIVE_UP_POINT) {
844 /* if we don't find any media within the first 100 or so results,
845 * assume this container doesn't have any. otherwise we'd load
846 * the entire thing when it got selected, which would suck.
847 */
848 rb_debug ("didn't find any media in %s, giving up", grl_media_get_title (source->priv->media_browse_container));
849 gtk_tree_store_set (source->priv->browser_model,
850 &source->priv->media_browse_container_iter,
851 2, CONTAINER_NO_MEDIA,
852 -1);
853 } else if (source->priv->media_browse_got_results) {
854 if (source->priv->media_browse_position < source->priv->media_browse_limit) {
855 media_browse_next (source);
856 } else {
857 char *text;
858
859 text = g_strdup_printf (ngettext ("Only showing %d result",
860 "Only showing %d results",
861 source->priv->media_browse_position),
862 source->priv->media_browse_position);
863 gtk_label_set_text (GTK_LABEL (source->priv->info_bar_label), text);
864 g_free (text);
865
866 gtk_widget_show (source->priv->info_bar);
867 }
868 } else if (source->priv->media_browse_got_containers == FALSE &&
869 source->priv->media_browse_container != NULL) {
870 /* make sure there's no marker row for this container */
871 delete_marker_row (source, &source->priv->media_browse_container_iter);
872 }
873 }
874 GDK_THREADS_LEAVE ();
875 }
876
877 static void
878 media_browse_next (RBGriloSource *source)
879 {
880 GrlOperationOptions *options;
881
882 rb_debug ("next media_browse op for %s (%d)",
883 grl_source_get_name (source->priv->grilo_source),
884 source->priv->media_browse_position);
885
886 source->priv->media_browse_got_results = FALSE;
887 if (source->priv->media_browse_container != NULL) {
888 options = make_operation_options (source,
889 GRL_OP_BROWSE,
890 source->priv->media_browse_position);
891 source->priv->media_browse_op =
892 grl_source_browse (source->priv->grilo_source,
893 source->priv->media_browse_container,
894 source->priv->grilo_keys,
895 options,
896 (GrlSourceResultCb) grilo_media_browse_cb,
897 source);
898 } else {
899 options = make_operation_options (source,
900 GRL_OP_SEARCH,
901 source->priv->media_browse_position);
902 source->priv->media_browse_op =
903 grl_source_search (source->priv->grilo_source,
904 source->priv->search_text,
905 source->priv->grilo_keys,
906 options,
907 (GrlSourceResultCb) grilo_media_browse_cb,
908 source);
909 }
910 }
911
912 static void
913 start_media_browse (RBGriloSource *source, GrlMedia *container, GtkTreeIter *container_iter, guint limit)
914 {
915 rb_debug ("starting media browse for %s",
916 grl_source_get_name (source->priv->grilo_source));
917
918 /* cancel existing operation? */
919 if (source->priv->media_browse_op != 0) {
920 grl_operation_cancel (source->priv->media_browse_op);
921 }
922
923 if (source->priv->media_browse_container != NULL) {
924 g_object_unref (source->priv->media_browse_container);
925 }
926 source->priv->media_browse_container = container;
927 if (container_iter != NULL) {
928 /* hrm, probably have to use row references here.. */
929 source->priv->media_browse_container_iter = *container_iter;
930 }
931 source->priv->media_browse_position = 0;
932 source->priv->media_browse_limit = limit;
933 source->priv->media_browse_got_containers = FALSE;
934
935 if (source->priv->query_model != NULL) {
936 g_object_unref (source->priv->query_model);
937 }
938 source->priv->query_model = rhythmdb_query_model_new_empty (source->priv->db);
939 rb_entry_view_set_model (RB_ENTRY_VIEW (source->priv->entry_view), source->priv->query_model);
940 g_object_set (source, "query-model", source->priv->query_model, NULL);
941
942 media_browse_next (source);
943 }
944
945 static void
946 fetch_more_cb (GtkInfoBar *bar, gint response, RBGriloSource *source)
947 {
948 if (response != GTK_RESPONSE_OK) {
949 return;
950 }
951
952 gtk_widget_hide (GTK_WIDGET (bar));
953 source->priv->media_browse_limit += CONTAINER_MAX_TRACKS;
954 media_browse_next (source);
955 }
956
957 static gboolean
958 expand_from_marker (RBGriloSource *source, GtkTreeIter *iter)
959 {
960 /* this is a marker row, fetch more containers underneath the parent */
961 GrlMedia *container;
962 GtkTreeIter browse;
963 int position;
964 gtk_tree_model_iter_parent (GTK_TREE_MODEL (source->priv->browser_model), &browse, iter);
965 gtk_tree_model_get (GTK_TREE_MODEL (source->priv->browser_model),
966 &browse,
967 0, &container,
968 3, &position,
969 -1);
970 if (position >= 0) {
971 start_browse (source, container, &browse, position);
972 return TRUE;
973 }
974
975 return FALSE;
976 }
977
978 static void
979 browser_selection_changed_cb (GtkTreeSelection *selection, RBGriloSource *source)
980 {
981 GtkTreeIter iter;
982 GrlMedia *container;
983 int container_type;
984
985 gtk_widget_hide (GTK_WIDGET (source->priv->info_bar));
986 if (gtk_tree_selection_get_selected (selection, NULL, &iter) == FALSE) {
987 rb_debug ("nothing selected");
988 return;
989 }
990
991 if (source->priv->search_entry != NULL) {
992 rb_search_entry_clear (source->priv->search_entry);
993 }
994
995 gtk_tree_model_get (GTK_TREE_MODEL (source->priv->browser_model), &iter,
996 0, &container,
997 2, &container_type,
998 -1);
999
1000 if (container == NULL) {
1001 expand_from_marker (source, &iter);
1002 } else if (container_type != CONTAINER_NO_MEDIA) {
1003 /* fetch media directly under this container */
1004 start_media_browse (source, container, &iter, CONTAINER_MAX_TRACKS);
1005 } else {
1006 /* clear the track list? */
1007 }
1008 }
1009
1010 static gboolean
1011 maybe_expand_container (RBGriloSource *source)
1012 {
1013 GtkTreePath *path;
1014 GtkTreePath *end;
1015 GtkTreeIter iter;
1016 GtkTreeIter end_iter;
1017 GtkTreeIter next;
1018 GrlMedia *container;
1019 gboolean last;
1020
1021 source->priv->maybe_expand_idle = 0;
1022
1023 if (source->priv->browse_op != 0) {
1024 rb_debug ("not expanding, already browsing");
1025 return FALSE;
1026 }
1027
1028 /* if we find a visible marker row, find more results */
1029 if (gtk_tree_view_get_visible_range (GTK_TREE_VIEW (source->priv->browser_view), &path, &end) == FALSE) {
1030 rb_debug ("not expanding, nothing to expand");
1031 return FALSE;
1032 }
1033
1034 gtk_tree_model_get_iter (GTK_TREE_MODEL (source->priv->browser_model), &iter, path);
1035 gtk_tree_model_get_iter (GTK_TREE_MODEL (source->priv->browser_model), &end_iter, end);
1036
1037 do {
1038 gtk_tree_path_free (path);
1039 path = gtk_tree_model_get_path (GTK_TREE_MODEL (source->priv->browser_model), &iter);
1040 last = (gtk_tree_path_compare (path, end) >= 0);
1041 gtk_tree_model_get (GTK_TREE_MODEL (source->priv->browser_model), &iter,
1042 0, &container,
1043 -1);
1044 if (container == NULL) {
1045 if (expand_from_marker (source, &iter)) {
1046 rb_debug ("expanding");
1047 break;
1048 }
1049 }
1050
1051 next = iter;
1052 if (gtk_tree_view_row_expanded (GTK_TREE_VIEW (source->priv->browser_view), path) &&
1053 gtk_tree_model_iter_has_child (GTK_TREE_MODEL (source->priv->browser_model), &iter)) {
1054 gtk_tree_model_iter_children (GTK_TREE_MODEL (source->priv->browser_model), &iter, &next);
1055 } else if (gtk_tree_model_iter_next (GTK_TREE_MODEL (source->priv->browser_model), &next)) {
1056 iter = next;
1057 } else {
1058 if (gtk_tree_model_iter_parent (GTK_TREE_MODEL (source->priv->browser_model), &next, &iter) == FALSE) {
1059 break;
1060 }
1061 iter = next;
1062 if (gtk_tree_model_iter_next (GTK_TREE_MODEL (source->priv->browser_model), &iter) == FALSE) {
1063 break;
1064 }
1065 }
1066 } while (last == FALSE);
1067
1068 gtk_tree_path_free (path);
1069 gtk_tree_path_free (end);
1070 return FALSE;
1071 }
1072
1073 static void
1074 maybe_expand_container_idle (RBGriloSource *source)
1075 {
1076 if (source->priv->maybe_expand_idle == 0) {
1077 source->priv->maybe_expand_idle = g_idle_add ((GSourceFunc)maybe_expand_container, source);
1078 }
1079 }
1080
1081 static void
1082 browser_row_expanded_cb (GtkTreeView *tree_view, GtkTreeIter *iter, GtkTreePath *path, RBGriloSource *source)
1083 {
1084 maybe_expand_container_idle (source);
1085 }
1086
1087 static void
1088 scroll_adjust_changed_cb (GtkAdjustment *adjustment, RBGriloSource *source)
1089 {
1090 maybe_expand_container_idle (source);
1091 }
1092
1093 static void
1094 scroll_adjust_value_changed_cb (GtkAdjustment *adjustment, RBGriloSource *source)
1095 {
1096 maybe_expand_container_idle (source);
1097 }
1098
1099 static void
1100 impl_selected (RBDisplayPage *page)
1101 {
1102 RBGriloSource *source = RB_GRILO_SOURCE (page);
1103
1104 RB_DISPLAY_PAGE_CLASS (rb_grilo_source_parent_class)->selected (page);
1105
1106 if (source->priv->done_initial_browse == FALSE) {
1107 source->priv->done_initial_browse = TRUE;
1108 start_browse (source, NULL, NULL, 0);
1109 }
1110
1111 rb_search_entry_set_mnemonic (source->priv->search_entry, TRUE);
1112 }
1113
1114 static void
1115 impl_deselected (RBDisplayPage *page)
1116 {
1117 RBGriloSource *source = RB_GRILO_SOURCE (page);
1118
1119 RB_DISPLAY_PAGE_CLASS (rb_grilo_source_parent_class)->deselected (page);
1120
1121 rb_search_entry_set_mnemonic (source->priv->search_entry, FALSE);
1122 }
1123
1124 static RBEntryView *
1125 impl_get_entry_view (RBSource *bsource)
1126 {
1127 RBGriloSource *source = RB_GRILO_SOURCE (bsource);
1128 return source->priv->entry_view;
1129 }
1130
1131 static void
1132 search_cb (RBSearchEntry *search, const char *text, RBGriloSource *source)
1133 {
1134 g_free (source->priv->search_text);
1135 source->priv->search_text = g_strdup (text);
1136
1137 gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (GTK_TREE_VIEW (source->priv->browser_view)));
1138
1139 start_media_browse (source, NULL, NULL, CONTAINER_MAX_TRACKS);
1140 }
1141
1142 static void
1143 notify_sort_order_cb (GObject *object, GParamSpec *pspec, RBGriloSource *source)
1144 {
1145 rb_entry_view_resort_model (RB_ENTRY_VIEW (object));
1146 }