1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  *  Copyright (C) 2006-2010  Jonathan Matthew <jonathan@d14n.org>
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 #include <gtk/gtk.h>
31 #include <glib/gi18n.h>
32 
33 #include "rb-entry-view.h"
34 #include "rb-import-errors-source.h"
35 #include "rb-util.h"
36 #include "rb-debug.h"
37 #include "rb-missing-plugins.h"
38 #include "rb-builder-helpers.h"
39 
40 static void rb_import_errors_source_class_init (RBImportErrorsSourceClass *klass);
41 static void rb_import_errors_source_init (RBImportErrorsSource *source);
42 static void rb_import_errors_source_constructed (GObject *object);
43 static void rb_import_errors_source_dispose (GObject *object);
44 
45 static void impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec);
46 static void impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);
47 
48 static RBEntryView *impl_get_entry_view (RBSource *source);
49 static void impl_delete_selected (RBSource *source);
50 static void impl_get_status (RBDisplayPage *page, char **text, gboolean *busy);
51 
52 static void rb_import_errors_source_songs_show_popup_cb (RBEntryView *view,
53 							 gboolean over_entry,
54 							 RBImportErrorsSource *source);
55 static void infobar_response_cb (GtkInfoBar *bar, gint response, RBImportErrorsSource *source);
56 
57 static void missing_plugin_row_inserted_cb (GtkTreeModel *model,
58 					    GtkTreePath *path,
59 					    GtkTreeIter *iter,
60 					    RBImportErrorsSource *source);
61 static void missing_plugin_row_deleted_cb (GtkTreeModel *model,
62 					   GtkTreePath *path,
63 					   RBImportErrorsSource *source);
64 
65 enum {
66 	PROP_0,
67 	PROP_NORMAL_ENTRY_TYPE,
68 	PROP_IGNORE_ENTRY_TYPE
69 };
70 
71 struct _RBImportErrorsSourcePrivate
72 {
73 	RhythmDB *db;
74 	RBEntryView *view;
75 
76 	RhythmDBQueryModel *missing_plugin_model;
77 	GtkWidget *infobar;
78 
79 	RhythmDBEntryType *normal_entry_type;
80 	RhythmDBEntryType *ignore_entry_type;
81 
82 	GMenuModel *popup;
83 };
84 
85 G_DEFINE_TYPE (RBImportErrorsSource, rb_import_errors_source, RB_TYPE_SOURCE);
86 
87 /**
88  * SECTION:rb-import-errors-source
89  * @short_description: source for displaying import errors
90  *
91  * This source is used to display the names of files that could not
92  * be imported into the library, along with any error messages from
93  * the import process.  When there are no import errors to display,
94  * the source is hidden.
95  *
96  * The source allows the user to delete the import error entries,
97  * and to move the files to the trash.
98  *
99  * When a file import fails, a #RhythmDBEntry is created with a
100  * specific entry type for import errors.  This source uses a query
101  * model that matches all such import error entries.
102  *
103  * To keep import errors from removable devices separate from those
104  * from the main library, multiple import error sources can be created,
105  * with separate entry types.  The generic audio player plugin, for
106  * example, creates an import error source for each device and inserts
107  * it into the source list as a child of the main source for the device.
108  */
109 
110 static void
rb_import_errors_source_class_init(RBImportErrorsSourceClass * klass)111 rb_import_errors_source_class_init (RBImportErrorsSourceClass *klass)
112 {
113 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
114 	RBDisplayPageClass *page_class = RB_DISPLAY_PAGE_CLASS (klass);
115 	RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
116 
117 	object_class->dispose = rb_import_errors_source_dispose;
118 	object_class->constructed = rb_import_errors_source_constructed;
119 	object_class->get_property = impl_get_property;
120 	object_class->set_property = impl_set_property;
121 
122 	page_class->get_status = impl_get_status;
123 
124 	source_class->get_entry_view = impl_get_entry_view;
125 	source_class->can_rename = (RBSourceFeatureFunc) rb_false_function;
126 
127 	source_class->can_cut = (RBSourceFeatureFunc) rb_false_function;
128 	source_class->can_delete = (RBSourceFeatureFunc) rb_true_function;
129 	source_class->can_move_to_trash = (RBSourceFeatureFunc) rb_true_function;
130 	source_class->can_copy = (RBSourceFeatureFunc) rb_false_function;
131 	source_class->can_add_to_queue = (RBSourceFeatureFunc) rb_false_function;
132 
133 	source_class->delete_selected = impl_delete_selected;
134 
135 	source_class->try_playlist = (RBSourceFeatureFunc) rb_false_function;
136 	source_class->can_pause = (RBSourceFeatureFunc) rb_false_function;
137 
138 	g_object_class_install_property (object_class,
139 					 PROP_NORMAL_ENTRY_TYPE,
140 					 g_param_spec_object ("normal-entry-type",
141 							      "Normal entry type",
142 							      "Entry type for successfully imported entries of this type",
143 							      RHYTHMDB_TYPE_ENTRY_TYPE,
144 							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
145 	g_object_class_install_property (object_class,
146 					 PROP_IGNORE_ENTRY_TYPE,
147 					 g_param_spec_object ("ignore-entry-type",
148 							      "Ignore entry type",
149 							      "Entry type for entries of this type to be ignored",
150 							      RHYTHMDB_TYPE_ENTRY_TYPE,
151 							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
152 
153 	g_type_class_add_private (klass, sizeof (RBImportErrorsSourcePrivate));
154 }
155 
156 static void
rb_import_errors_source_init(RBImportErrorsSource * source)157 rb_import_errors_source_init (RBImportErrorsSource *source)
158 {
159 	source->priv = G_TYPE_INSTANCE_GET_PRIVATE (source, RB_TYPE_IMPORT_ERRORS_SOURCE, RBImportErrorsSourcePrivate);
160 }
161 
162 static void
rb_import_errors_source_constructed(GObject * object)163 rb_import_errors_source_constructed (GObject *object)
164 {
165 	GObject *shell_player;
166 	RBImportErrorsSource *source;
167 	RBShell *shell;
168 	GPtrArray *query;
169 	RhythmDBQueryModel *model;
170 	RhythmDBEntryType *entry_type;
171 	GtkWidget *box;
172 	GtkWidget *label;
173 
174 	RB_CHAIN_GOBJECT_METHOD (rb_import_errors_source_parent_class, constructed, object);
175 
176 	source = RB_IMPORT_ERRORS_SOURCE (object);
177 
178 	g_object_get (source,
179 		      "shell", &shell,
180 		      "entry-type", &entry_type,
181 		      NULL);
182 	g_object_get (shell,
183 		      "db", &source->priv->db,
184 		      "shell-player", &shell_player,
185 		      NULL);
186 	g_object_unref (shell);
187 
188 	/* construct real query */
189 	query = rhythmdb_query_parse (source->priv->db,
190 				      RHYTHMDB_QUERY_PROP_EQUALS,
191 				      	RHYTHMDB_PROP_TYPE,
192 					entry_type,
193 				      RHYTHMDB_QUERY_END);
194 
195 	model = rhythmdb_query_model_new (source->priv->db, query,
196 					  (GCompareDataFunc) rhythmdb_query_model_string_sort_func,
197 					  GUINT_TO_POINTER (RHYTHMDB_PROP_LOCATION), NULL, FALSE);
198 	rhythmdb_query_free (query);
199 
200 	/* set up entry view */
201 	source->priv->view = rb_entry_view_new (source->priv->db, shell_player,
202 						FALSE, FALSE);
203 	g_object_unref (shell_player);
204 
205 	rb_entry_view_set_model (source->priv->view, model);
206 
207 	rb_entry_view_append_column (source->priv->view, RB_ENTRY_VIEW_COL_LOCATION, TRUE);
208 	rb_entry_view_append_column (source->priv->view, RB_ENTRY_VIEW_COL_ERROR, TRUE);
209 
210 	g_signal_connect_object (source->priv->view, "show_popup",
211 				 G_CALLBACK (rb_import_errors_source_songs_show_popup_cb), source, 0);
212 
213 	g_object_set (source, "query-model", model, NULL);
214 	g_object_unref (model);
215 
216 	/* set up query model for tracking missing plugin information */
217 	query = rhythmdb_query_parse (source->priv->db,
218 				      RHYTHMDB_QUERY_PROP_EQUALS,
219 				        RHYTHMDB_PROP_TYPE,
220 					entry_type,
221 				      RHYTHMDB_QUERY_PROP_NOT_EQUAL,
222 				        RHYTHMDB_PROP_COMMENT,
223 					"",
224 				      RHYTHMDB_QUERY_END);
225 
226 	source->priv->missing_plugin_model = rhythmdb_query_model_new_empty (source->priv->db);
227 	rhythmdb_do_full_query_async_parsed (source->priv->db,
228 					     RHYTHMDB_QUERY_RESULTS (source->priv->missing_plugin_model),
229 					     query);
230 	rhythmdb_query_free (query);
231 
232 	/* set up info bar for triggering codec installation */
233 	source->priv->infobar = gtk_info_bar_new_with_buttons (_("Install Additional Software"), GTK_RESPONSE_OK, NULL);
234 	g_signal_connect_object (source->priv->infobar,
235 				 "response",
236 				 G_CALLBACK (infobar_response_cb),
237 				 source, 0);
238 
239 	label = gtk_label_new (_("Additional software is required to play some of these files."));
240 	gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
241 	gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (GTK_INFO_BAR (source->priv->infobar))),
242 			   label);
243 
244 	g_object_unref (entry_type);
245 
246 	box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
247 	gtk_box_pack_start (GTK_BOX (box), GTK_WIDGET (source->priv->view), TRUE, TRUE, 0);
248 	gtk_box_pack_start (GTK_BOX (box), source->priv->infobar, FALSE, FALSE, 0);
249 
250 	gtk_container_add (GTK_CONTAINER (source), box);
251 	gtk_widget_show_all (GTK_WIDGET (source));
252 	gtk_widget_hide (source->priv->infobar);
253 
254 	/* show the info bar when there are missing plugin entries */
255 	g_signal_connect_object (source->priv->missing_plugin_model,
256 				 "row-inserted",
257 				 G_CALLBACK (missing_plugin_row_inserted_cb),
258 				 source, 0);
259 	g_signal_connect_object (source->priv->missing_plugin_model,
260 				 "row-deleted",
261 				 G_CALLBACK (missing_plugin_row_deleted_cb),
262 				 source, 0);
263 
264 	rb_display_page_set_icon_name (RB_DISPLAY_PAGE (source), "dialog-error-symbolic");
265 }
266 
267 static void
impl_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)268 impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
269 {
270 	RBImportErrorsSource *source = RB_IMPORT_ERRORS_SOURCE (object);
271 	switch (prop_id) {
272 	case PROP_NORMAL_ENTRY_TYPE:
273 		g_value_set_object (value, source->priv->normal_entry_type);
274 		break;
275 	case PROP_IGNORE_ENTRY_TYPE:
276 		g_value_set_object (value, source->priv->ignore_entry_type);
277 		break;
278 	default:
279 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
280 		break;
281 	}
282 }
283 
284 static void
impl_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)285 impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
286 {
287 	RBImportErrorsSource *source = RB_IMPORT_ERRORS_SOURCE (object);
288 	switch (prop_id) {
289 	case PROP_NORMAL_ENTRY_TYPE:
290 		source->priv->normal_entry_type = g_value_get_object (value);
291 		break;
292 	case PROP_IGNORE_ENTRY_TYPE:
293 		source->priv->ignore_entry_type = g_value_get_object (value);
294 		break;
295 	default:
296 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
297 		break;
298 	}
299 }
300 
301 static void
rb_import_errors_source_dispose(GObject * object)302 rb_import_errors_source_dispose (GObject *object)
303 {
304 	RBImportErrorsSource *source = RB_IMPORT_ERRORS_SOURCE (object);
305 
306 	if (source->priv->db) {
307 		g_object_unref (source->priv->db);
308 		source->priv->db = NULL;
309 	}
310 	if (source->priv->missing_plugin_model) {
311 		g_object_unref (source->priv->missing_plugin_model);
312 		source->priv->missing_plugin_model = NULL;
313 	}
314 
315 	G_OBJECT_CLASS (rb_import_errors_source_parent_class)->dispose (object);
316 }
317 
318 static RBEntryView *
impl_get_entry_view(RBSource * asource)319 impl_get_entry_view (RBSource *asource)
320 {
321 	RBImportErrorsSource *source = RB_IMPORT_ERRORS_SOURCE (asource);
322 	return source->priv->view;
323 }
324 
325 /**
326  * rb_import_errors_source_new:
327  * @shell: the #RBShell instance
328  * @entry_type: the entry type to display in the source
329  * @normal_entry_type: entry type for successfully imported entries of this type
330  * @ignore_entry_type: entry type for entries of this type to be ignored
331  *
332  * Creates a new source for displaying import errors of the
333  * specified type.
334  *
335  * Return value: a new import error source
336  */
337 RBSource *
rb_import_errors_source_new(RBShell * shell,RhythmDBEntryType * entry_type,RhythmDBEntryType * normal_entry_type,RhythmDBEntryType * ignore_entry_type)338 rb_import_errors_source_new (RBShell *shell,
339 			     RhythmDBEntryType *entry_type,
340 			     RhythmDBEntryType *normal_entry_type,
341 			     RhythmDBEntryType *ignore_entry_type)
342 {
343 	RBSource *source;
344 
345 	source = RB_SOURCE (g_object_new (RB_TYPE_IMPORT_ERRORS_SOURCE,
346 					  "name", _("Import Errors"),
347 					  "shell", shell,
348 					  "visibility", FALSE,
349 					  "hidden-when-empty", TRUE,
350 					  "entry-type", entry_type,
351 					  "normal-entry-type", normal_entry_type,
352 					  "ignore-entry-type", ignore_entry_type,
353 					  NULL));
354 	return source;
355 }
356 
357 static void
impl_delete_selected(RBSource * asource)358 impl_delete_selected (RBSource *asource)
359 {
360 	RBImportErrorsSource *source = RB_IMPORT_ERRORS_SOURCE (asource);
361 	GList *sel, *tem;
362 
363 	sel = rb_entry_view_get_selected_entries (source->priv->view);
364 	for (tem = sel; tem != NULL; tem = tem->next) {
365 		rhythmdb_entry_delete (source->priv->db, tem->data);
366 		rhythmdb_commit (source->priv->db);
367 	}
368 
369 	g_list_foreach (sel, (GFunc)rhythmdb_entry_unref, NULL);
370 	g_list_free (sel);
371 }
372 
373 static void
impl_get_status(RBDisplayPage * page,char ** text,gboolean * busy)374 impl_get_status (RBDisplayPage *page, char **text, gboolean *busy)
375 {
376 	RhythmDBQueryModel *model;
377 	gint count;
378 
379 	g_object_get (page, "query-model", &model, NULL);
380 	count = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (model), NULL);
381 	g_object_unref (model);
382 
383 	*text = g_strdup_printf (ngettext ("%d import error", "%d import errors", count),
384 				 count);
385 }
386 
387 static void
rb_import_errors_source_songs_show_popup_cb(RBEntryView * view,gboolean over_entry,RBImportErrorsSource * source)388 rb_import_errors_source_songs_show_popup_cb (RBEntryView *view,
389 					     gboolean over_entry,
390 					     RBImportErrorsSource *source)
391 {
392 	GtkWidget *menu;
393 	GtkBuilder *builder;
394 
395 	if (over_entry == FALSE)
396 		return;
397 
398 	if (source->priv->popup == NULL) {
399 		builder = rb_builder_load ("import-errors-popup.ui", NULL);
400 		source->priv->popup = G_MENU_MODEL (gtk_builder_get_object (builder, "import-errors-popup"));
401 		g_object_ref (source->priv->popup);
402 		g_object_unref (builder);
403 	}
404 
405 	menu = gtk_menu_new_from_model (source->priv->popup);
406 	gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (source), NULL);
407 	gtk_menu_popup (GTK_MENU (menu),
408 			NULL,
409 			NULL,
410 			NULL,
411 			NULL,
412 			3,
413 			gtk_get_current_event_time ());
414 }
415 
416 static void
missing_plugin_row_inserted_cb(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,RBImportErrorsSource * source)417 missing_plugin_row_inserted_cb (GtkTreeModel *model,
418 				GtkTreePath *path,
419 				GtkTreeIter *iter,
420 				RBImportErrorsSource *source)
421 {
422 	gtk_widget_show (source->priv->infobar);
423 }
424 
425 static void
missing_plugin_row_deleted_cb(GtkTreeModel * model,GtkTreePath * path,RBImportErrorsSource * source)426 missing_plugin_row_deleted_cb (GtkTreeModel *model,
427 			       GtkTreePath *path,
428 			       RBImportErrorsSource *source)
429 {
430 	/* row hasn't been deleted from the model yet, so the count
431 	 * still includes it.
432 	 */
433 	if (gtk_tree_model_iter_n_children (model, NULL) == 1) {
434 		gtk_widget_hide (source->priv->infobar);
435 	}
436 }
437 
438 static void
missing_plugins_retry_cb(gpointer instance,gboolean installed,RBImportErrorsSource * source)439 missing_plugins_retry_cb (gpointer instance, gboolean installed, RBImportErrorsSource *source)
440 {
441 	GtkTreeIter iter;
442 	RhythmDBEntryType *error_entry_type;
443 
444 	gtk_info_bar_set_response_sensitive (GTK_INFO_BAR (source->priv->infobar),
445 					     GTK_RESPONSE_OK,
446 					     TRUE);
447 
448 	if (installed == FALSE) {
449 		rb_debug ("installer failed, not retrying imports");
450 		return;
451 	}
452 
453 	if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (source->priv->missing_plugin_model), &iter) == FALSE) {
454 		return;
455 	}
456 
457 	g_object_get (source, "entry-type", &error_entry_type, NULL);
458 	do {
459 		RhythmDBEntry *entry;
460 
461 		entry = rhythmdb_query_model_iter_to_entry (source->priv->missing_plugin_model, &iter);
462 		rhythmdb_add_uri_with_types (source->priv->db,
463 					     rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION),
464 					     source->priv->normal_entry_type,
465 					     source->priv->ignore_entry_type,
466 					     error_entry_type);
467 	} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (source->priv->missing_plugin_model), &iter));
468 
469 	g_object_unref (error_entry_type);
470 }
471 
472 static void
missing_plugins_retry_cleanup(RBImportErrorsSource * source)473 missing_plugins_retry_cleanup (RBImportErrorsSource *source)
474 {
475 	g_object_unref (source);
476 }
477 
478 static void
infobar_response_cb(GtkInfoBar * infobar,gint response,RBImportErrorsSource * source)479 infobar_response_cb (GtkInfoBar *infobar, gint response, RBImportErrorsSource *source)
480 {
481 	char **details = NULL;
482 	GtkTreeIter iter;
483 	GClosure *closure;
484 	int i;
485 
486 	/* gather plugin installer detail strings */
487 	if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (source->priv->missing_plugin_model), &iter) == FALSE) {
488 		return;
489 	}
490 
491 	i = 0;
492 	do {
493 		RhythmDBEntry *entry;
494 		char **bits;
495 		int j;
496 
497 		entry = rhythmdb_query_model_iter_to_entry (source->priv->missing_plugin_model, &iter);
498 		bits = g_strsplit (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_COMMENT), "\n", 0);
499 
500 		for (j = 0; bits[j] != NULL; j++) {
501 			if (rb_str_in_strv (bits[j], (const char **)details) == FALSE) {
502 				details = g_realloc (details, sizeof (char *) * i+2);
503 				details[i++] = g_strdup (bits[j]);
504 				details[i] = NULL;
505 			}
506 		}
507 
508 		g_strfreev (bits);
509 	} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (source->priv->missing_plugin_model), &iter));
510 
511 	/* run the installer */
512 	closure = g_cclosure_new ((GCallback) missing_plugins_retry_cb,
513 				  g_object_ref (source),
514 				  (GClosureNotify) missing_plugins_retry_cleanup);
515 	g_closure_set_marshal (closure, g_cclosure_marshal_VOID__BOOLEAN);
516 	if (rb_missing_plugins_install ((const char **)details, TRUE, closure) == TRUE) {
517 		/* disable the button while the installer is running */
518 		gtk_info_bar_set_response_sensitive (infobar, response, FALSE);
519 	}
520 	g_closure_sink (closure);
521 
522 	g_strfreev (details);
523 }
524