1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  *  Copyright (C) 2006  Jonathan Matthew <jonathan@kaolin.wh9.net>
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 <gtk/gtk.h>
32 #include <glib/gi18n.h>
33 
34 #include "rb-entry-view.h"
35 #include "rb-missing-files-source.h"
36 #include "rb-song-info.h"
37 #include "rb-util.h"
38 #include "rb-debug.h"
39 #include "rb-builder-helpers.h"
40 
41 /**
42  * SECTION:rb-missing-files-source
43  * @short_description: source displaying files missing from the library
44  *
45  * This source displays files that rhythmbox cannot find at the expected
46  * locations.  On startup, it does a file access check for every file
47  * in the library, hiding those that fail.  This source sets up a
48  * query model that matches only hidden entries.  It displays the file
49  * location and the last time the file was successfully accessed.
50  *
51  * The source only displayed in the source list when there are hidden
52  * entries to show.
53  */
54 
55 static void rb_missing_files_source_class_init (RBMissingFilesSourceClass *klass);
56 static void rb_missing_files_source_init (RBMissingFilesSource *source);
57 static void rb_missing_files_source_constructed (GObject *object);
58 static void rb_missing_files_source_dispose (GObject *object);
59 static void rb_missing_files_source_set_property (GObject *object,
60 						  guint prop_id,
61 						  const GValue *value,
62 						  GParamSpec *pspec);
63 static void rb_missing_files_source_get_property (GObject *object,
64 						  guint prop_id,
65 						  GValue *value,
66 						  GParamSpec *pspec);
67 
68 static RBEntryView *impl_get_entry_view (RBSource *source);
69 static void impl_song_properties (RBSource *source);
70 static void impl_delete_selected (RBSource *source);
71 static void impl_get_status (RBDisplayPage *page, char **text, gboolean *busy);
72 
73 static void rb_missing_files_source_songs_show_popup_cb (RBEntryView *view,
74 							 gboolean over_entry,
75 							 RBMissingFilesSource *source);
76 static void rb_missing_files_source_songs_sort_order_changed_cb (GObject *object,
77 								 GParamSpec *pspec,
78 								 RBMissingFilesSource *source);
79 
80 struct RBMissingFilesSourcePrivate
81 {
82 	RhythmDB *db;
83 	RBEntryView *view;
84 	GMenuModel *popup;
85 };
86 
87 G_DEFINE_TYPE (RBMissingFilesSource, rb_missing_files_source, RB_TYPE_SOURCE);
88 
89 static void
rb_missing_files_source_class_init(RBMissingFilesSourceClass * klass)90 rb_missing_files_source_class_init (RBMissingFilesSourceClass *klass)
91 {
92 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
93 	RBDisplayPageClass *page_class = RB_DISPLAY_PAGE_CLASS (klass);
94 	RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
95 
96 	object_class->dispose = rb_missing_files_source_dispose;
97 	object_class->constructed = rb_missing_files_source_constructed;
98 
99 	object_class->set_property = rb_missing_files_source_set_property;
100 	object_class->get_property = rb_missing_files_source_get_property;
101 
102 	page_class->get_status = impl_get_status;
103 
104 	source_class->get_entry_view = impl_get_entry_view;
105 	source_class->can_rename = (RBSourceFeatureFunc) rb_false_function;
106 
107 	source_class->can_cut = (RBSourceFeatureFunc) rb_false_function;
108 	source_class->can_delete = (RBSourceFeatureFunc) rb_true_function;
109 	source_class->can_move_to_trash = (RBSourceFeatureFunc) rb_false_function;
110 	source_class->can_copy = (RBSourceFeatureFunc) rb_false_function;
111 	source_class->can_add_to_queue = (RBSourceFeatureFunc) rb_false_function;
112 
113 	source_class->delete_selected = impl_delete_selected;
114 
115 	source_class->song_properties = impl_song_properties;
116 	source_class->try_playlist = (RBSourceFeatureFunc) rb_false_function;
117 	source_class->can_pause = (RBSourceFeatureFunc) rb_false_function;
118 
119 	g_type_class_add_private (klass, sizeof (RBMissingFilesSourcePrivate));
120 }
121 
122 static void
rb_missing_files_source_init(RBMissingFilesSource * source)123 rb_missing_files_source_init (RBMissingFilesSource *source)
124 {
125 	source->priv = G_TYPE_INSTANCE_GET_PRIVATE (source, RB_TYPE_MISSING_FILES_SOURCE, RBMissingFilesSourcePrivate);
126 }
127 
128 static void
rb_missing_files_source_constructed(GObject * object)129 rb_missing_files_source_constructed (GObject *object)
130 {
131 	GObject *shell_player;
132 	RBMissingFilesSource *source;
133 	RBShell *shell;
134 	GPtrArray *query;
135 	RhythmDBQueryModel *model;
136 	RhythmDBEntryType *entry_type;
137 
138 	RB_CHAIN_GOBJECT_METHOD (rb_missing_files_source_parent_class, constructed, object);
139 	source = RB_MISSING_FILES_SOURCE (object);
140 
141 	g_object_get (source,
142 		      "shell", &shell,
143 		      "entry-type", &entry_type,
144 		      NULL);
145 	g_object_get (shell,
146 		      "db", &source->priv->db,
147 		      "shell-player", &shell_player,
148 		      NULL);
149 	g_object_unref (shell);
150 
151 	/* construct real query */
152 	query = rhythmdb_query_parse (source->priv->db,
153 				      RHYTHMDB_QUERY_PROP_EQUALS,
154 				      	RHYTHMDB_PROP_TYPE,
155 					entry_type,
156 				      RHYTHMDB_QUERY_PROP_EQUALS,
157 				      	RHYTHMDB_PROP_HIDDEN,
158 					TRUE,
159 				      RHYTHMDB_QUERY_END);
160 	g_object_unref (entry_type);
161 
162 	model = rhythmdb_query_model_new (source->priv->db, query,
163 					  NULL, NULL, NULL, FALSE);
164 
165 	rhythmdb_query_free (query);
166 
167 	g_object_set (model, "show-hidden", TRUE, NULL);
168 
169 	/* set up entry view */
170 	source->priv->view = rb_entry_view_new (source->priv->db, shell_player,
171 						FALSE, FALSE);
172 	g_object_unref (shell_player);
173 
174 	rb_entry_view_set_model (source->priv->view, model);
175 
176 	rb_entry_view_append_column (source->priv->view, RB_ENTRY_VIEW_COL_TRACK_NUMBER, FALSE);
177 	rb_entry_view_append_column (source->priv->view, RB_ENTRY_VIEW_COL_TITLE, TRUE);
178 /*	rb_entry_view_append_column (source->priv->view, RB_ENTRY_VIEW_COL_GENRE, FALSE); */
179 	rb_entry_view_append_column (source->priv->view, RB_ENTRY_VIEW_COL_ARTIST, FALSE);
180 	rb_entry_view_append_column (source->priv->view, RB_ENTRY_VIEW_COL_ALBUM, FALSE);
181 	rb_entry_view_append_column (source->priv->view, RB_ENTRY_VIEW_COL_LOCATION, TRUE);
182 	rb_entry_view_append_column (source->priv->view, RB_ENTRY_VIEW_COL_LAST_SEEN, TRUE);
183 
184 	rb_entry_view_set_columns_clickable (source->priv->view, TRUE);
185 
186 	gtk_container_add (GTK_CONTAINER (source), GTK_WIDGET (source->priv->view));
187 	g_signal_connect_object (source->priv->view, "show_popup",
188 				 G_CALLBACK (rb_missing_files_source_songs_show_popup_cb), source, 0);
189 	g_signal_connect_object (source->priv->view, "notify::sort-order",
190 				 G_CALLBACK (rb_missing_files_source_songs_sort_order_changed_cb), source, 0);
191 
192 	gtk_widget_show_all (GTK_WIDGET (source));
193 
194 	g_object_set (source, "query-model", model, NULL);
195 	g_object_unref (model);
196 
197 	rb_display_page_set_icon_name (RB_DISPLAY_PAGE (source), "dialog-warning-symbolic");
198 }
199 
200 static void
rb_missing_files_source_dispose(GObject * object)201 rb_missing_files_source_dispose (GObject *object)
202 {
203 	RBMissingFilesSource *source = RB_MISSING_FILES_SOURCE (object);
204 
205 	if (source->priv->db != NULL) {
206 		g_object_unref (source->priv->db);
207 		source->priv->db = NULL;
208 	}
209 
210 	G_OBJECT_CLASS (rb_missing_files_source_parent_class)->dispose (object);
211 }
212 
213 static RBEntryView *
impl_get_entry_view(RBSource * asource)214 impl_get_entry_view (RBSource *asource)
215 {
216 	RBMissingFilesSource *source = RB_MISSING_FILES_SOURCE (asource);
217 	return source->priv->view;
218 }
219 
220 static void
rb_missing_files_source_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)221 rb_missing_files_source_set_property (GObject *object,
222 				      guint prop_id,
223 				      const GValue *value,
224 				      GParamSpec *pspec)
225 {
226 	/*RBMissingFilesSource *source = RB_MISSING_FILES_SOURCE (object);*/
227 
228 	switch (prop_id)
229 	{
230 	default:
231 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
232 		break;
233 	}
234 }
235 
236 static void
rb_missing_files_source_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)237 rb_missing_files_source_get_property (GObject *object,
238 				      guint prop_id,
239 				      GValue *value,
240 				      GParamSpec *pspec)
241 {
242 	/*RBMissingFilesSource *source = RB_MISSING_FILES_SOURCE (object);*/
243 
244 	switch (prop_id)
245 	{
246 	default:
247 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
248 		break;
249 	}
250 }
251 
252 /**
253  * rb_missing_files_source_new:
254  * @shell: the #RBShell instance
255  * @library: the #RBLibrarySource instance
256  *
257  * Creates the missing files source.  It extracts the
258  * entry type from the library source instance, so it
259  * currently only works for files in the library, but
260  * it would be trivial to make it use any source type
261  * that did file access checks for its contents.
262  *
263  * Return value: the #RBMissingFilesSource
264  */
265 RBSource *
rb_missing_files_source_new(RBShell * shell,RBLibrarySource * library)266 rb_missing_files_source_new (RBShell *shell,
267 			     RBLibrarySource *library)
268 {
269 	RBSource *source;
270 	RhythmDBEntryType *entry_type;
271 
272 	g_object_get (library, "entry-type", &entry_type, NULL);
273 	source = RB_SOURCE (g_object_new (RB_TYPE_MISSING_FILES_SOURCE,
274 					  "name", _("Missing Files"),
275 					  "entry-type", entry_type,
276 					  "shell", shell,
277 					  "visibility", FALSE,
278 					  "hidden-when-empty", TRUE,
279 					  NULL));
280 	g_object_unref (entry_type);
281 	return source;
282 }
283 
284 static void
rb_missing_files_source_songs_show_popup_cb(RBEntryView * view,gboolean over_entry,RBMissingFilesSource * source)285 rb_missing_files_source_songs_show_popup_cb (RBEntryView *view,
286 					     gboolean over_entry,
287 					     RBMissingFilesSource *source)
288 {
289 	GtkWidget *menu;
290 	GtkBuilder *builder;
291 
292 	if (over_entry == FALSE)
293 		return;
294 
295 	if (source->priv->popup == NULL) {
296 		builder = rb_builder_load ("missing-files-popup.ui", NULL);
297 		source->priv->popup = G_MENU_MODEL (gtk_builder_get_object (builder, "missing-files-popup"));
298 		g_object_ref (source->priv->popup);
299 		g_object_unref (builder);
300 	}
301 
302 	menu = gtk_menu_new_from_model (source->priv->popup);
303 	gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (source), NULL);
304 	gtk_menu_popup (GTK_MENU (menu),
305 			NULL,
306 			NULL,
307 			NULL,
308 			NULL,
309 			3,
310 			gtk_get_current_event_time ());
311 }
312 
313 static void
impl_song_properties(RBSource * asource)314 impl_song_properties (RBSource *asource)
315 {
316 	RBMissingFilesSource *source = RB_MISSING_FILES_SOURCE (asource);
317 	GtkWidget *song_info = NULL;
318 
319 	g_return_if_fail (source->priv->view != NULL);
320 
321 	song_info = rb_song_info_new (asource, NULL);
322 	if (song_info)
323 		gtk_widget_show_all (song_info);
324 	else
325 		rb_debug ("failed to create dialog, or no selection!");
326 }
327 
328 static void
impl_delete_selected(RBSource * asource)329 impl_delete_selected (RBSource *asource)
330 {
331 	RBMissingFilesSource *source = RB_MISSING_FILES_SOURCE (asource);
332 	GList *sel, *tem;
333 
334 	sel = rb_entry_view_get_selected_entries (source->priv->view);
335 	for (tem = sel; tem != NULL; tem = tem->next) {
336 		rhythmdb_entry_delete (source->priv->db, tem->data);
337 		rhythmdb_commit (source->priv->db);
338 	}
339 
340 	g_list_foreach (sel, (GFunc)rhythmdb_entry_unref, NULL);
341 	g_list_free (sel);
342 }
343 
344 static void
rb_missing_files_source_songs_sort_order_changed_cb(GObject * object,GParamSpec * pspec,RBMissingFilesSource * source)345 rb_missing_files_source_songs_sort_order_changed_cb (GObject *object,
346 						     GParamSpec *pspec,
347 						     RBMissingFilesSource *source)
348 {
349 	rb_entry_view_resort_model (RB_ENTRY_VIEW (object));
350 }
351 
352 static void
impl_get_status(RBDisplayPage * page,char ** text,gboolean * busy)353 impl_get_status (RBDisplayPage *page, char **text, gboolean *busy)
354 {
355 	RhythmDBQueryModel *model;
356 	gint count;
357 
358 	g_object_get (page, "query-model", &model, NULL);
359 	count = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (model), NULL);
360 	g_object_unref (model);
361 
362 	*text = g_strdup_printf (ngettext ("%d missing file", "%d missing files", count),
363 				 count);
364 }
365