1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2  *
3  *  Copyright (C) 2005 Renato Araujo Oliveira Filho <renato.filho@indt.org.br>
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 /*
30  * Base source for podcast sources.  This provides the feed
31  * and post views, the search actions, and so on.
32  */
33 
34 #include "config.h"
35 
36 #include <string.h>
37 #define __USE_XOPEN
38 #include <time.h>
39 
40 #include <glib.h>
41 #include <glib/gi18n.h>
42 #include <gtk/gtk.h>
43 #include <libsoup/soup.h>
44 
45 #include "rb-podcast-source.h"
46 #include "rb-podcast-settings.h"
47 #include "rb-podcast-entry-types.h"
48 
49 #include "rhythmdb.h"
50 #include "rhythmdb-query-model.h"
51 #include "rb-shell-player.h"
52 #include "rb-entry-view.h"
53 #include "rb-property-view.h"
54 #include "rb-util.h"
55 #include "rb-file-helpers.h"
56 #include "rb-dialog.h"
57 #include "rb-podcast-properties-dialog.h"
58 #include "rb-feed-podcast-properties-dialog.h"
59 #include "rb-playlist-manager.h"
60 #include "rb-debug.h"
61 #include "rb-podcast-manager.h"
62 #include "rb-static-playlist-source.h"
63 #include "rb-cut-and-paste-code.h"
64 #include "rb-source-search-basic.h"
65 #include "rb-cell-renderer-pixbuf.h"
66 #include "rb-podcast-add-dialog.h"
67 #include "rb-source-toolbar.h"
68 #include "rb-builder-helpers.h"
69 #include "rb-application.h"
70 
71 static void podcast_add_action_cb (GSimpleAction *, GVariant *, gpointer);
72 static void podcast_download_action_cb (GSimpleAction *, GVariant *, gpointer);
73 static void podcast_download_cancel_action_cb (GSimpleAction *, GVariant *, gpointer);
74 static void podcast_feed_properties_action_cb (GSimpleAction *, GVariant *, gpointer);
75 static void podcast_feed_update_action_cb (GSimpleAction *, GVariant *, gpointer);
76 static void podcast_feed_update_all_action_cb (GSimpleAction *, GVariant *, gpointer);
77 static void podcast_feed_delete_action_cb (GSimpleAction *, GVariant *, gpointer);
78 
79 
80 struct _RBPodcastSourcePrivate
81 {
82 	RhythmDB *db;
83 
84 	guint prefs_notify_id;
85 
86 	GtkWidget *grid;
87 	GtkWidget *paned;
88 	GtkWidget *add_dialog;
89 	RBSourceToolbar *toolbar;
90 
91 	RhythmDBPropertyModel *feed_model;
92 	RBPropertyView *feeds;
93 	RBEntryView *posts;
94 
95 	GList *selected_feeds;
96 	RhythmDBQuery *base_query;
97 	RhythmDBQuery *search_query;
98 	RBSourceSearch *default_search;
99 	gboolean show_all_feeds;
100 
101 	RBPodcastManager *podcast_mgr;
102 
103 	GdkPixbuf *error_pixbuf;
104 	GdkPixbuf *refresh_pixbuf;
105 
106 	GMenuModel *feed_popup;
107 	GMenuModel *episode_popup;
108 	GMenuModel *search_popup;
109 	GAction *search_action;
110 };
111 
112 
113 static const GtkTargetEntry posts_view_drag_types[] = {
114 	{  "text/uri-list", 0, 0 },
115 	{  "_NETSCAPE_URL", 0, 1 },
116 	{  "application/rss+xml", 0, 2 },
117 };
118 
119 enum
120 {
121 	PROP_0,
122 	PROP_PODCAST_MANAGER,
123 	PROP_BASE_QUERY,
124 	PROP_SHOW_ALL_FEEDS,
125 	PROP_SHOW_BROWSER
126 };
127 
G_DEFINE_TYPE(RBPodcastSource,rb_podcast_source,RB_TYPE_SOURCE)128 G_DEFINE_TYPE (RBPodcastSource, rb_podcast_source, RB_TYPE_SOURCE)
129 
130 static void
131 podcast_posts_view_sort_order_changed_cb (GObject *object,
132 					  GParamSpec *pspec,
133 					  RBPodcastSource *source)
134 {
135 	rb_debug ("sort order changed");
136 	rb_entry_view_resort_model (RB_ENTRY_VIEW (object));
137 }
138 
139 static void
podcast_posts_show_popup_cb(RBEntryView * view,gboolean over_entry,RBPodcastSource * source)140 podcast_posts_show_popup_cb (RBEntryView *view,
141 			     gboolean over_entry,
142 			     RBPodcastSource *source)
143 {
144 	GAction* action;
145 	GList *lst;
146 	gboolean downloadable = FALSE;
147 	gboolean cancellable = FALSE;
148 	GtkWidget *menu;
149 	GActionMap *map;
150 
151 	lst = rb_entry_view_get_selected_entries (view);
152 
153 	while (lst) {
154 		RhythmDBEntry *entry = (RhythmDBEntry*) lst->data;
155 		gulong status = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS);
156 
157 		if (rb_podcast_manager_entry_in_download_queue (source->priv->podcast_mgr, entry)) {
158 			cancellable = TRUE;
159 		} else if (status != RHYTHMDB_PODCAST_STATUS_COMPLETE) {
160 			downloadable = TRUE;
161 		}
162 
163 		lst = lst->next;
164 	}
165 
166 	g_list_foreach (lst, (GFunc)rhythmdb_entry_unref, NULL);
167 	g_list_free (lst);
168 
169 	map = G_ACTION_MAP (g_application_get_default ());
170 	action = g_action_map_lookup_action (map, "podcast-download");
171 	g_simple_action_set_enabled (G_SIMPLE_ACTION (action), downloadable);
172 
173 	action = g_action_map_lookup_action (map, "podcast-cancel-download");
174 	g_simple_action_set_enabled (G_SIMPLE_ACTION (action), cancellable);
175 
176 	menu = gtk_menu_new_from_model (source->priv->episode_popup);
177 	gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (source), NULL);
178 	gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 3, gtk_get_current_event_time ());
179 }
180 
181 static void
podcast_feeds_show_popup_cb(RBPropertyView * view,RBPodcastSource * source)182 podcast_feeds_show_popup_cb (RBPropertyView *view,
183 			     RBPodcastSource *source)
184 {
185 	GActionMap *map;
186 	GAction *act_update;
187 	GAction *act_properties;
188 	GAction *act_delete;
189 	GtkWidget *menu;
190 	GList *lst;
191 
192 	lst = source->priv->selected_feeds;
193 
194 	map = G_ACTION_MAP (g_application_get_default ());
195 	act_update = g_action_map_lookup_action (map, "podcast-feed-update");
196 	act_properties = g_action_map_lookup_action (map, "podcast-feed-properties");
197 	act_delete = g_action_map_lookup_action (map, "podcast-feed-delete");
198 
199 	g_simple_action_set_enabled (G_SIMPLE_ACTION (act_update), lst != NULL);
200 	g_simple_action_set_enabled (G_SIMPLE_ACTION (act_properties), lst != NULL);
201 	g_simple_action_set_enabled (G_SIMPLE_ACTION (act_delete), lst != NULL);
202 
203 	menu = gtk_menu_new_from_model (source->priv->feed_popup);
204 	gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (source), NULL);
205 	gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 3, gtk_get_current_event_time ());
206 }
207 
208 static GPtrArray *
construct_query_from_selection(RBPodcastSource * source)209 construct_query_from_selection (RBPodcastSource *source)
210 {
211 	GPtrArray *query;
212 	query = rhythmdb_query_copy (source->priv->base_query);
213 
214 	if (source->priv->search_query) {
215 		rhythmdb_query_append (source->priv->db,
216 				       query,
217 				       RHYTHMDB_QUERY_SUBQUERY,
218 				       source->priv->search_query,
219 				       RHYTHMDB_QUERY_END);
220 	}
221 
222 	if (source->priv->selected_feeds) {
223 		GPtrArray *subquery = g_ptr_array_new ();
224 		GList *l;
225 
226 		for (l = source->priv->selected_feeds; l != NULL; l = g_list_next (l)) {
227 			const char *location;
228 
229 			location = (char *) l->data;
230 			rb_debug ("subquery SUBTITLE equals %s", location);
231 
232 			rhythmdb_query_append (source->priv->db,
233 					       subquery,
234 					       RHYTHMDB_QUERY_PROP_EQUALS,
235 					       RHYTHMDB_PROP_SUBTITLE,
236 					       location,
237 					       RHYTHMDB_QUERY_END);
238 			if (g_list_next (l))
239 				rhythmdb_query_append (source->priv->db, subquery,
240 						       RHYTHMDB_QUERY_DISJUNCTION,
241 						       RHYTHMDB_QUERY_END);
242 		}
243 
244 		rhythmdb_query_append (source->priv->db, query,
245 				       RHYTHMDB_QUERY_SUBQUERY, subquery,
246 				       RHYTHMDB_QUERY_END);
247 
248 		rhythmdb_query_free (subquery);
249 	}
250 
251 	return query;
252 }
253 
254 static void
rb_podcast_source_do_query(RBPodcastSource * source,gboolean feed_query)255 rb_podcast_source_do_query (RBPodcastSource *source, gboolean feed_query)
256 {
257 	RhythmDBQueryModel *query_model;
258 	GPtrArray *query;
259 
260 	/* set up new query model */
261 	query_model = rhythmdb_query_model_new_empty (source->priv->db);
262 
263 	rb_entry_view_set_model (source->priv->posts, query_model);
264 	g_object_set (source, "query-model", query_model, NULL);
265 
266 	if (feed_query) {
267 		if (source->priv->feed_model != NULL) {
268 			g_object_unref (source->priv->feed_model);
269 			source->priv->feed_model = NULL;
270 		}
271 
272 		if (source->priv->show_all_feeds && (source->priv->search_query == NULL)) {
273 			RhythmDBQueryModel *feed_query_model;
274 
275 			rb_debug ("showing all feeds in browser");
276 			source->priv->feed_model = rhythmdb_property_model_new (source->priv->db, RHYTHMDB_PROP_LOCATION);
277 			g_object_set (source->priv->feeds, "property-model", source->priv->feed_model, NULL);
278 
279 			feed_query_model = rhythmdb_query_model_new_empty (source->priv->db);
280 			g_object_set (source->priv->feed_model, "query-model", feed_query_model, NULL);
281 
282 			rhythmdb_do_full_query_async (source->priv->db,
283 						      RHYTHMDB_QUERY_RESULTS (feed_query_model),
284 						      RHYTHMDB_QUERY_PROP_EQUALS,
285 						      RHYTHMDB_PROP_TYPE,
286 						      RHYTHMDB_ENTRY_TYPE_PODCAST_FEED,
287 						      RHYTHMDB_QUERY_PROP_NOT_EQUAL,
288 						      RHYTHMDB_PROP_STATUS,
289 						      RHYTHMDB_PODCAST_FEED_STATUS_HIDDEN,
290 						      RHYTHMDB_QUERY_END);
291 			g_object_unref (feed_query_model);
292 		} else {
293 			rb_debug ("only showing matching feeds in browser");
294 			source->priv->feed_model = rhythmdb_property_model_new (source->priv->db, RHYTHMDB_PROP_SUBTITLE);
295 			g_object_set (source->priv->feeds, "property-model", source->priv->feed_model, NULL);
296 
297 			g_object_set (source->priv->feed_model, "query-model", query_model, NULL);
298 		}
299 	}
300 
301 	/* build and run the query */
302 	query = construct_query_from_selection (source);
303 	rhythmdb_do_full_query_async_parsed (source->priv->db,
304 					     RHYTHMDB_QUERY_RESULTS (query_model),
305 					     query);
306 
307 	rhythmdb_query_free (query);
308 
309 	g_object_unref (query_model);
310 }
311 
312 static void
feed_select_change_cb(RBPropertyView * propview,GList * feeds,RBPodcastSource * source)313 feed_select_change_cb (RBPropertyView *propview,
314 		       GList *feeds,
315 		       RBPodcastSource *source)
316 {
317 	if (rb_string_list_equal (feeds, source->priv->selected_feeds))
318 		return;
319 
320 	if (source->priv->selected_feeds) {
321 		g_list_foreach (source->priv->selected_feeds, (GFunc) g_free, NULL);
322 	        g_list_free (source->priv->selected_feeds);
323 	}
324 
325 	source->priv->selected_feeds = rb_string_list_copy (feeds);
326 
327 	rb_podcast_source_do_query (source, FALSE);
328 	rb_source_notify_filter_changed (RB_SOURCE (source));
329 }
330 
331 static void
posts_view_drag_data_received_cb(GtkWidget * widget,GdkDragContext * dc,gint x,gint y,GtkSelectionData * selection_data,guint info,guint time,RBPodcastSource * source)332 posts_view_drag_data_received_cb (GtkWidget *widget,
333 				  GdkDragContext *dc,
334 				  gint x,
335 				  gint y,
336 				  GtkSelectionData *selection_data,
337 				  guint info,
338 				  guint time,
339 				  RBPodcastSource *source)
340 {
341 	rb_display_page_receive_drag (RB_DISPLAY_PAGE (source), selection_data);
342 }
343 
344 static void
podcast_add_dialog_closed_cb(RBPodcastAddDialog * dialog,RBPodcastSource * source)345 podcast_add_dialog_closed_cb (RBPodcastAddDialog *dialog, RBPodcastSource *source)
346 {
347 	rb_podcast_source_do_query (source, FALSE);
348 	gtk_widget_set_margin_top (GTK_WIDGET (source->priv->grid), 6);
349 	gtk_widget_hide (source->priv->add_dialog);
350 	gtk_widget_show (GTK_WIDGET (source->priv->toolbar));
351 	gtk_widget_show (source->priv->paned);
352 }
353 
354 static void
yank_clipboard_url(GtkClipboard * clipboard,const char * text,RBPodcastSource * source)355 yank_clipboard_url (GtkClipboard *clipboard, const char *text, RBPodcastSource *source)
356 {
357 	SoupURI *uri;
358 
359 	if (text == NULL) {
360 		return;
361 	}
362 
363 	uri = soup_uri_new (text);
364 	if (SOUP_URI_VALID_FOR_HTTP (uri)) {
365 		rb_podcast_add_dialog_reset (RB_PODCAST_ADD_DIALOG (source->priv->add_dialog), text, FALSE);
366 	}
367 
368 	if (uri != NULL) {
369 		soup_uri_free (uri);
370 	}
371 }
372 
373 static void
podcast_add_action_cb(GSimpleAction * action,GVariant * parameter,gpointer data)374 podcast_add_action_cb (GSimpleAction *action, GVariant *parameter, gpointer data)
375 {
376 	RBPodcastSource *source = RB_PODCAST_SOURCE (data);
377 	RhythmDBQueryModel *query_model;
378 
379 	rb_podcast_add_dialog_reset (RB_PODCAST_ADD_DIALOG (source->priv->add_dialog), NULL, FALSE);
380 
381 	/* if we can get a url from the clipboard, populate the dialog with that,
382 	 * since there's a good chance that's what the user wants to do anyway.
383 	 */
384 	gtk_clipboard_request_text (gtk_clipboard_get (GDK_SELECTION_CLIPBOARD),
385 				    (GtkClipboardTextReceivedFunc) yank_clipboard_url,
386 				    source);
387 	gtk_clipboard_request_text (gtk_clipboard_get (GDK_SELECTION_PRIMARY),
388 				    (GtkClipboardTextReceivedFunc) yank_clipboard_url,
389 				    source);
390 
391 	query_model = rhythmdb_query_model_new_empty (source->priv->db);
392 	rb_entry_view_set_model (source->priv->posts, query_model);
393 	g_object_set (source, "query-model", query_model, NULL);
394 	g_object_unref (query_model);
395 
396 	gtk_widget_set_margin_top (GTK_WIDGET (source->priv->grid), 0);
397 	gtk_widget_hide (source->priv->paned);
398 	gtk_widget_hide (GTK_WIDGET (source->priv->toolbar));
399 	gtk_widget_show (source->priv->add_dialog);
400 }
401 
402 void
rb_podcast_source_add_feed(RBPodcastSource * source,const char * text)403 rb_podcast_source_add_feed (RBPodcastSource *source, const char *text)
404 {
405 	g_action_group_activate_action (G_ACTION_GROUP (g_application_get_default ()), "podcast-add", NULL);
406 
407 	rb_podcast_add_dialog_reset (RB_PODCAST_ADD_DIALOG (source->priv->add_dialog), text, TRUE);
408 }
409 
410 static void
podcast_download_action_cb(GSimpleAction * action,GVariant * parameter,gpointer data)411 podcast_download_action_cb (GSimpleAction *action, GVariant *parameter, gpointer data)
412 {
413 	RBPodcastSource *source = RB_PODCAST_SOURCE (data);
414 	GList *lst;
415 	GValue val = {0, };
416 	RBEntryView *posts;
417 
418 	rb_debug ("Add to download action");
419 	posts = source->priv->posts;
420 
421 	lst = rb_entry_view_get_selected_entries (posts);
422 	g_value_init (&val, G_TYPE_ULONG);
423 
424 	while (lst != NULL) {
425 		RhythmDBEntry *entry  = (RhythmDBEntry *) lst->data;
426 		gulong status = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS);
427 
428 		if (status == RHYTHMDB_PODCAST_STATUS_PAUSED ||
429 		    status == RHYTHMDB_PODCAST_STATUS_ERROR) {
430 			g_value_set_ulong (&val, RHYTHMDB_PODCAST_STATUS_WAITING);
431 			rhythmdb_entry_set (source->priv->db, entry, RHYTHMDB_PROP_STATUS, &val);
432 			rb_podcast_manager_download_entry (source->priv->podcast_mgr, entry);
433 		}
434 
435 		lst = lst->next;
436 	}
437 	g_value_unset (&val);
438 	rhythmdb_commit (source->priv->db);
439 
440 	g_list_foreach (lst, (GFunc)rhythmdb_entry_unref, NULL);
441 	g_list_free (lst);
442 }
443 
444 static void
podcast_download_cancel_action_cb(GSimpleAction * action,GVariant * parameter,gpointer data)445 podcast_download_cancel_action_cb (GSimpleAction *action, GVariant *parameter, gpointer data)
446 {
447 	RBPodcastSource *source = RB_PODCAST_SOURCE (data);
448 	GList *lst;
449 	GValue val = {0, };
450 	RBEntryView *posts;
451 
452 	posts = source->priv->posts;
453 
454 	lst = rb_entry_view_get_selected_entries (posts);
455 	g_value_init (&val, G_TYPE_ULONG);
456 
457 	while (lst != NULL) {
458 		RhythmDBEntry *entry  = (RhythmDBEntry *) lst->data;
459 		gulong status = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS);
460 
461 		if ((status > 0 && status < RHYTHMDB_PODCAST_STATUS_COMPLETE) ||
462 		    status == RHYTHMDB_PODCAST_STATUS_WAITING) {
463 			g_value_set_ulong (&val, RHYTHMDB_PODCAST_STATUS_PAUSED);
464 			rhythmdb_entry_set (source->priv->db, entry, RHYTHMDB_PROP_STATUS, &val);
465 			rb_podcast_manager_cancel_download (source->priv->podcast_mgr, entry);
466 		}
467 
468 		lst = lst->next;
469 	}
470 
471 	g_value_unset (&val);
472 	rhythmdb_commit (source->priv->db);
473 
474 	g_list_foreach (lst, (GFunc)rhythmdb_entry_unref, NULL);
475 	g_list_free (lst);
476 }
477 
478 static void
podcast_remove_response_cb(GtkDialog * dialog,int response,RBPodcastSource * source)479 podcast_remove_response_cb (GtkDialog *dialog, int response, RBPodcastSource *source)
480 {
481 	GList *feeds, *l;
482 
483 	gtk_widget_destroy (GTK_WIDGET (dialog));
484 
485 	if (response == GTK_RESPONSE_CANCEL || response == GTK_RESPONSE_DELETE_EVENT) {
486 		return;
487 	}
488 
489 	feeds = rb_string_list_copy (source->priv->selected_feeds);
490 	for (l = feeds; l != NULL; l = g_list_next (l)) {
491 		const char *location = l->data;
492 
493 		rb_debug ("Removing podcast location: %s", location);
494 		rb_podcast_manager_remove_feed (source->priv->podcast_mgr,
495 						location,
496 						(response == GTK_RESPONSE_YES));
497 	}
498 
499 	rb_list_deep_free (feeds);
500 }
501 
502 static void
podcast_feed_delete_action_cb(GSimpleAction * action,GVariant * parameter,gpointer data)503 podcast_feed_delete_action_cb (GSimpleAction *action, GVariant *parameter, gpointer data)
504 {
505 	RBPodcastSource *source = RB_PODCAST_SOURCE (data);
506 	GtkWidget *dialog;
507 	GtkWidget *button;
508 	GtkWindow *window;
509 	RBShell *shell;
510 
511 	rb_debug ("Delete feed action");
512 
513 	g_object_get (source, "shell", &shell, NULL);
514 	g_object_get (shell, "window", &window, NULL);
515 	g_object_unref (shell);
516 
517 	dialog = gtk_message_dialog_new (window,
518 			                 GTK_DIALOG_DESTROY_WITH_PARENT,
519 					 GTK_MESSAGE_WARNING,
520 					 GTK_BUTTONS_NONE,
521 					 _("Delete the podcast feed and downloaded files?"));
522 
523 	gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
524 	                                          _("If you choose to delete the feed and files, "
525 						    "they will be permanently lost.  Please note that "
526 						    "you can delete the feed but keep the downloaded "
527 						    "files by choosing to delete the feed only."));
528 
529 	gtk_window_set_title (GTK_WINDOW (dialog), "");
530 
531 	gtk_dialog_add_buttons (GTK_DIALOG (dialog),
532 	                        _("Delete _Feed Only"),
533 	                        GTK_RESPONSE_NO,
534 	                        _("_Cancel"),
535 	                        GTK_RESPONSE_CANCEL,
536 	                        NULL);
537 
538 	button = gtk_dialog_add_button (GTK_DIALOG (dialog),
539 	                                _("_Delete Feed And Files"),
540 			                GTK_RESPONSE_YES);
541 
542 	gtk_window_set_focus (GTK_WINDOW (dialog), button);
543 	gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_YES);
544 
545 	gtk_widget_show_all (dialog);
546 	g_signal_connect (dialog, "response", G_CALLBACK (podcast_remove_response_cb), source);
547 }
548 
549 static void
podcast_feed_properties_action_cb(GSimpleAction * action,GVariant * parameter,gpointer data)550 podcast_feed_properties_action_cb (GSimpleAction *action, GVariant *parameter, gpointer data)
551 {
552 	RBPodcastSource *source = RB_PODCAST_SOURCE (data);
553 	RhythmDBEntry *entry;
554 	GtkWidget *dialog;
555 	const char *location;
556 
557 	location = (char *) source->priv->selected_feeds->data;
558 
559 	entry = rhythmdb_entry_lookup_by_location (source->priv->db,
560 						   location);
561 
562 	if (entry != NULL) {
563 		dialog = rb_feed_podcast_properties_dialog_new (entry);
564 		rb_debug ("in feed properties");
565 		if (dialog)
566 			gtk_widget_show_all (dialog);
567 		else
568 			rb_debug ("no selection!");
569 	}
570 }
571 
572 static void
podcast_feed_update_action_cb(GSimpleAction * action,GVariant * parameter,gpointer data)573 podcast_feed_update_action_cb (GSimpleAction *action, GVariant *parameter, gpointer data)
574 {
575 	RBPodcastSource *source = RB_PODCAST_SOURCE (data);
576 	GList *feeds, *l;
577 
578 	rb_debug ("Update action");
579 
580 	feeds = rb_string_list_copy (source->priv->selected_feeds);
581 	if (feeds == NULL) {
582 		rb_podcast_manager_update_feeds (source->priv->podcast_mgr);
583 		return;
584 	}
585 
586 	for (l = feeds; l != NULL; l = g_list_next (l)) {
587 		const char *location = l->data;
588 
589 		rb_podcast_manager_subscribe_feed (source->priv->podcast_mgr,
590 						   location,
591 						   FALSE);
592 	}
593 
594 	rb_list_deep_free (feeds);
595 }
596 
597 static void
podcast_feed_update_all_action_cb(GSimpleAction * action,GVariant * parameter,gpointer data)598 podcast_feed_update_all_action_cb (GSimpleAction *action, GVariant *parameter, gpointer data)
599 {
600 	RBPodcastSource *source = RB_PODCAST_SOURCE (data);
601 	rb_podcast_manager_update_feeds (source->priv->podcast_mgr);
602 }
603 
604 static void
podcast_post_status_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,RBPodcastSource * source)605 podcast_post_status_cell_data_func (GtkTreeViewColumn *column,
606 				    GtkCellRenderer *renderer,
607 				    GtkTreeModel *tree_model,
608 				    GtkTreeIter *iter,
609 				    RBPodcastSource *source)
610 
611 {
612 	RhythmDBEntry *entry;
613 	guint value;
614 	char *s;
615 
616 	gtk_tree_model_get (tree_model, iter, 0, &entry, -1);
617 
618 	switch (rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS)) {
619 	case RHYTHMDB_PODCAST_STATUS_COMPLETE:
620 		g_object_set (renderer, "text", _("Downloaded"), NULL);
621 		value = 100;
622 		break;
623 	case RHYTHMDB_PODCAST_STATUS_ERROR:
624 		g_object_set (renderer, "text", _("Failed"), NULL);
625 		value = 0;
626 		break;
627 	case RHYTHMDB_PODCAST_STATUS_WAITING:
628 		g_object_set (renderer, "text", _("Waiting"), NULL);
629 		value = 0;
630 		break;
631 	case RHYTHMDB_PODCAST_STATUS_PAUSED:
632 		g_object_set (renderer, "text", "", NULL);
633 		value = 0;
634 		break;
635 	default:
636 		value = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS);
637 		s = g_strdup_printf ("%u %%", value);
638 
639 		g_object_set (renderer, "text", s, NULL);
640 		g_free (s);
641 		break;
642 	}
643 
644 	g_object_set (renderer, "visible",
645 		      rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS) != RHYTHMDB_PODCAST_STATUS_PAUSED,
646 		      "value", value,
647 		      NULL);
648 
649 	rhythmdb_entry_unref (entry);
650 }
651 
652 static void
podcast_post_feed_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,RBPodcastSource * source)653 podcast_post_feed_cell_data_func (GtkTreeViewColumn *column,
654 				  GtkCellRenderer *renderer,
655 				  GtkTreeModel *tree_model,
656 				  GtkTreeIter *iter,
657 				  RBPodcastSource *source)
658 
659 {
660 	RhythmDBEntry *entry;
661 	const gchar *album;
662 
663 	gtk_tree_model_get (tree_model, iter, 0, &entry, -1);
664 	album = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM);
665 
666 	g_object_set (renderer, "text", album, NULL);
667 
668 	rhythmdb_entry_unref (entry);
669 }
670 
671 static gboolean
podcast_feed_title_search_func(GtkTreeModel * model,gint column,const gchar * key,GtkTreeIter * iter,RBPodcastSource * source)672 podcast_feed_title_search_func (GtkTreeModel *model,
673 				gint column,
674 				const gchar *key,
675 				GtkTreeIter *iter,
676 				RBPodcastSource *source)
677 {
678 	char *title;
679 	char *fold_key;
680 	RhythmDBEntry *entry = NULL;
681 	gboolean ret;
682 
683 	fold_key = rb_search_fold (key);
684 	gtk_tree_model_get (model, iter,
685 			    RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &title,
686 			    -1);
687 
688 	entry = rhythmdb_entry_lookup_by_location (source->priv->db, title);
689 	if (entry != NULL) {
690 		g_free (title);
691 		title = rhythmdb_entry_dup_string (entry, RHYTHMDB_PROP_TITLE_FOLDED);
692 	}
693 
694 	ret = g_str_has_prefix (title, fold_key);
695 
696 	g_free (fold_key);
697 	g_free (title);
698 
699 	return !ret;
700 }
701 
702 static void
podcast_feed_title_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,RBPodcastSource * source)703 podcast_feed_title_cell_data_func (GtkTreeViewColumn *column,
704 				   GtkCellRenderer *renderer,
705 				   GtkTreeModel *tree_model,
706 				   GtkTreeIter *iter,
707 				   RBPodcastSource *source)
708 {
709 	char *title;
710 	char *str;
711 	gboolean is_all;
712 	guint number;
713 	RhythmDBEntry *entry = NULL;
714 
715 	str = NULL;
716 	gtk_tree_model_get (tree_model, iter,
717 			    RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &title,
718 			    RHYTHMDB_PROPERTY_MODEL_COLUMN_PRIORITY, &is_all,
719 			    RHYTHMDB_PROPERTY_MODEL_COLUMN_NUMBER, &number, -1);
720 
721 	entry = rhythmdb_entry_lookup_by_location (source->priv->db, title);
722 	if (entry != NULL) {
723 		g_free (title);
724 		title = g_strdup (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE));
725 	}
726 
727 	if (is_all) {
728 		int nodes;
729 		const char *fmt;
730 
731 		nodes = gtk_tree_model_iter_n_children  (GTK_TREE_MODEL (tree_model), NULL);
732 		/* Subtract one for the All node */
733 		nodes--;
734 
735 		fmt = ngettext ("%d feed", "All %d feeds", nodes);
736 
737 		str = g_strdup_printf (fmt, nodes, number);
738 	} else {
739 		str = g_strdup_printf ("%s", title);
740 	}
741 
742 	g_object_set (G_OBJECT (renderer), "text", str,
743 		      "weight", G_UNLIKELY (is_all) ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
744 		      NULL);
745 
746 	g_free (str);
747 	g_free (title);
748 }
749 
750 static void
podcast_feed_pixbuf_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,RBPodcastSource * source)751 podcast_feed_pixbuf_cell_data_func (GtkTreeViewColumn *column,
752 				    GtkCellRenderer *renderer,
753 				    GtkTreeModel *tree_model,
754 				    GtkTreeIter *iter,
755 				    RBPodcastSource *source)
756 {
757 	char *title;
758 	RhythmDBEntry *entry = NULL;
759 	GdkPixbuf *pixbuf = NULL;
760 
761 	gtk_tree_model_get (tree_model, iter,
762 			    RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &title,
763 			    -1);
764 
765 	entry = rhythmdb_entry_lookup_by_location (source->priv->db, title);
766 	g_free (title);
767 
768 	if (entry != NULL) {
769 		gulong status = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_STATUS);
770 		if (status == RHYTHMDB_PODCAST_FEED_STATUS_UPDATING) {
771 			pixbuf = source->priv->refresh_pixbuf;
772 		} else if (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_PLAYBACK_ERROR)) {
773 			pixbuf = source->priv->error_pixbuf;
774 		}
775 	}
776 	g_object_set (renderer, "pixbuf", pixbuf, NULL);
777 }
778 
779 static void
podcast_post_date_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * tree_model,GtkTreeIter * iter,RBPodcastSource * source)780 podcast_post_date_cell_data_func (GtkTreeViewColumn *column,
781 				  GtkCellRenderer *renderer,
782 				  GtkTreeModel *tree_model,
783 				  GtkTreeIter *iter,
784 				  RBPodcastSource *source)
785 {
786 	RhythmDBEntry *entry;
787 	gulong value;
788 	char *str;
789 
790 	gtk_tree_model_get (tree_model, iter, 0, &entry, -1);
791 
792 	value = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_POST_TIME);
793         if (value == 0) {
794 		str = g_strdup (_("Unknown"));
795 	} else {
796 		str = rb_utf_friendly_time (value);
797 	}
798 
799 	g_object_set (G_OBJECT (renderer), "text", str, NULL);
800 	g_free (str);
801 
802 	rhythmdb_entry_unref (entry);
803 }
804 
805 
806 static gint
podcast_post_feed_sort_func(RhythmDBEntry * a,RhythmDBEntry * b,RhythmDBQueryModel * model)807 podcast_post_feed_sort_func (RhythmDBEntry *a,
808 			     RhythmDBEntry *b,
809 			     RhythmDBQueryModel *model)
810 {
811 	const char *a_str, *b_str;
812 	gulong a_val, b_val;
813 	gint ret;
814 
815 	/* feeds */
816 	a_str = rhythmdb_entry_get_string (a, RHYTHMDB_PROP_ALBUM_SORT_KEY);
817 	b_str = rhythmdb_entry_get_string (b, RHYTHMDB_PROP_ALBUM_SORT_KEY);
818 
819 	ret = strcmp (a_str, b_str);
820 	if (ret != 0)
821 		return ret;
822 
823 	a_val = rhythmdb_entry_get_ulong (a, RHYTHMDB_PROP_POST_TIME);
824 	b_val = rhythmdb_entry_get_ulong (b, RHYTHMDB_PROP_POST_TIME);
825 
826 	if (a_val != b_val)
827 		return (a_val > b_val) ? 1 : -1;
828 
829 	/* titles */
830 	a_str = rhythmdb_entry_get_string (a, RHYTHMDB_PROP_TITLE_SORT_KEY);
831 	b_str = rhythmdb_entry_get_string (b, RHYTHMDB_PROP_TITLE_SORT_KEY);
832 
833 	ret = strcmp (a_str, b_str);
834 	if (ret != 0)
835 		return ret;
836 
837 	/* location */
838 	a_str = rhythmdb_entry_get_string (a, RHYTHMDB_PROP_LOCATION);
839 	b_str = rhythmdb_entry_get_string (b, RHYTHMDB_PROP_LOCATION);
840 
841 	ret = strcmp (a_str, b_str);
842 	return ret;
843 }
844 
845 static gint
podcast_post_date_sort_func(RhythmDBEntry * a,RhythmDBEntry * b,RhythmDBQueryModel * model)846 podcast_post_date_sort_func (RhythmDBEntry *a,
847 			     RhythmDBEntry *b,
848 			     RhythmDBQueryModel *model)
849 {
850 	gulong a_val, b_val;
851 	gint ret;
852 
853 	a_val = rhythmdb_entry_get_ulong (a, RHYTHMDB_PROP_POST_TIME);
854 	b_val = rhythmdb_entry_get_ulong (b, RHYTHMDB_PROP_POST_TIME);
855 
856 	if (a_val != b_val)
857 		ret = (a_val > b_val) ? 1 : -1;
858 	else
859 		ret = podcast_post_feed_sort_func (a, b, model);
860 
861         return ret;
862 }
863 
864 static gint
podcast_post_status_sort_func(RhythmDBEntry * a,RhythmDBEntry * b,RhythmDBQueryModel * model)865 podcast_post_status_sort_func (RhythmDBEntry *a,
866 			       RhythmDBEntry *b,
867 			       RhythmDBQueryModel *model)
868 {
869 	gulong a_val, b_val;
870 	gint ret;
871 
872 	a_val = rhythmdb_entry_get_ulong (a, RHYTHMDB_PROP_STATUS);
873 	b_val = rhythmdb_entry_get_ulong (b, RHYTHMDB_PROP_STATUS);
874 
875         if (a_val != b_val)
876 		ret = (a_val > b_val) ? 1 : -1;
877 	else
878 		ret = podcast_post_feed_sort_func (a, b, model);
879 
880 	return ret;
881 }
882 
883 static void
podcast_entry_changed_cb(RhythmDB * db,RhythmDBEntry * entry,GPtrArray * changes,RBPodcastSource * source)884 podcast_entry_changed_cb (RhythmDB *db,
885 			  RhythmDBEntry *entry,
886 			  GPtrArray *changes,
887 			  RBPodcastSource *source)
888 {
889 	RhythmDBEntryType *entry_type;
890 	gboolean feed_changed;
891 	int i;
892 
893 	entry_type = rhythmdb_entry_get_entry_type (entry);
894 	if (entry_type != RHYTHMDB_ENTRY_TYPE_PODCAST_FEED)
895 		return;
896 
897 	feed_changed = FALSE;
898 	for (i = 0; i < changes->len; i++) {
899 		RhythmDBEntryChange *change = g_ptr_array_index (changes, i);
900 
901 		switch (change->prop) {
902 		case RHYTHMDB_PROP_PLAYBACK_ERROR:
903 		case RHYTHMDB_PROP_STATUS:
904 			feed_changed = TRUE;
905 			break;
906 		default:
907 			break;
908 		}
909 	}
910 
911 	if (feed_changed) {
912 		const char *loc;
913 		GtkTreeIter iter;
914 
915 		loc = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
916 		if (rhythmdb_property_model_iter_from_string (source->priv->feed_model,
917 							      loc,
918 							      &iter)) {
919 			GtkTreePath *path;
920 
921 			path = gtk_tree_model_get_path (GTK_TREE_MODEL (source->priv->feed_model),
922 						        &iter);
923 			gtk_tree_model_row_changed (GTK_TREE_MODEL (source->priv->feed_model),
924 						    path,
925 						    &iter);
926 			gtk_tree_path_free (path);
927 		}
928 	}
929 }
930 
931 static void
podcast_status_pixbuf_clicked_cb(RBCellRendererPixbuf * renderer,const char * path_string,RBPodcastSource * source)932 podcast_status_pixbuf_clicked_cb (RBCellRendererPixbuf *renderer,
933 				  const char *path_string,
934 				  RBPodcastSource *source)
935 {
936 	GtkTreePath *path;
937 	GtkTreeIter iter;
938 
939 	g_return_if_fail (path_string != NULL);
940 
941 	path = gtk_tree_path_new_from_string (path_string);
942 	if (gtk_tree_model_get_iter (GTK_TREE_MODEL (source->priv->feed_model), &iter, path)) {
943 		RhythmDBEntry *entry;
944 		char *feed_url;
945 
946 		gtk_tree_model_get (GTK_TREE_MODEL (source->priv->feed_model),
947 				    &iter,
948 				    RHYTHMDB_PROPERTY_MODEL_COLUMN_TITLE, &feed_url,
949 				    -1);
950 
951 		entry = rhythmdb_entry_lookup_by_location (source->priv->db, feed_url);
952 		if (entry != NULL) {
953 			const gchar *error;
954 
955 			error = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_PLAYBACK_ERROR);
956 			if (error) {
957 				rb_error_dialog (NULL, _("Podcast Error"), "%s", error);
958 			}
959 		}
960 
961 		g_free (feed_url);
962 	}
963 
964 	gtk_tree_path_free (path);
965 }
966 
967 static void
settings_changed_cb(GSettings * settings,const char * key,RBPodcastSource * source)968 settings_changed_cb (GSettings *settings, const char *key, RBPodcastSource *source)
969 {
970 	if (g_strcmp0 (key, PODCAST_PANED_POSITION) == 0) {
971 		gtk_paned_set_position (GTK_PANED (source->priv->paned),
972 					g_settings_get_int (settings, key));
973 	}
974 }
975 
976 RBSource *
rb_podcast_source_new(RBShell * shell,RBPodcastManager * podcast_manager,RhythmDBQuery * base_query,const char * name,const char * icon_name)977 rb_podcast_source_new (RBShell *shell,
978 		       RBPodcastManager *podcast_manager,
979 		       RhythmDBQuery *base_query,
980 		       const char *name,
981 		       const char *icon_name)
982 {
983 	RBSource *source;
984 	GSettings *settings;
985 	GtkBuilder *builder;
986 	GMenu *toolbar;
987 
988 	settings = g_settings_new (PODCAST_SETTINGS_SCHEMA);
989 
990 	builder = rb_builder_load ("podcast-toolbar.ui", NULL);
991 	toolbar = G_MENU (gtk_builder_get_object (builder, "podcast-toolbar"));
992 	rb_application_link_shared_menus (RB_APPLICATION (g_application_get_default ()), toolbar);
993 
994 	source = RB_SOURCE (g_object_new (RB_TYPE_PODCAST_SOURCE,
995 					  "name", name,
996 					  "shell", shell,
997 					  "entry-type", RHYTHMDB_ENTRY_TYPE_PODCAST_POST,
998 					  "podcast-manager", podcast_manager,
999 					  "base-query", base_query,
1000 					  "settings", g_settings_get_child (settings, "source"),
1001 					  "toolbar-menu", toolbar,
1002 					  NULL));
1003 	rb_display_page_set_icon_name (RB_DISPLAY_PAGE (source), icon_name);
1004 	g_object_unref (settings);
1005 	g_object_unref (builder);
1006 
1007 	return source;
1008 }
1009 
1010 static void
impl_add_to_queue(RBSource * source,RBSource * queue)1011 impl_add_to_queue (RBSource *source, RBSource *queue)
1012 {
1013 	RBEntryView *songs;
1014 	GList *selection;
1015 	GList *iter;
1016 
1017 	songs = rb_source_get_entry_view (source);
1018 	selection = rb_entry_view_get_selected_entries (songs);
1019 
1020 	if (selection == NULL)
1021 		return;
1022 
1023 	for (iter = selection; iter; iter = iter->next) {
1024 		RhythmDBEntry *entry = (RhythmDBEntry *)iter->data;
1025 		if (!rb_podcast_manager_entry_downloaded (entry))
1026 			continue;
1027 		rb_static_playlist_source_add_entry (RB_STATIC_PLAYLIST_SOURCE (queue),
1028 						     entry, -1);
1029 	}
1030 
1031 	g_list_foreach (selection, (GFunc)rhythmdb_entry_unref, NULL);
1032 	g_list_free (selection);
1033 }
1034 
1035 static void
delete_response_cb(GtkDialog * dialog,int response,RBPodcastSource * source)1036 delete_response_cb (GtkDialog *dialog, int response, RBPodcastSource *source)
1037 {
1038 	GList *entries;
1039 	GList *l;
1040 
1041 	gtk_widget_destroy (GTK_WIDGET (dialog));
1042 
1043 	if (response == GTK_RESPONSE_CANCEL || response == GTK_RESPONSE_DELETE_EVENT) {
1044 		return;
1045 	}
1046 
1047 	entries = rb_entry_view_get_selected_entries (source->priv->posts);
1048 	for (l = entries; l != NULL; l = g_list_next (l)) {
1049 		RhythmDBEntry *entry = l->data;
1050 
1051 		rb_podcast_manager_cancel_download (source->priv->podcast_mgr, entry);
1052 		if (response == GTK_RESPONSE_YES) {
1053 			rb_podcast_manager_delete_download (source->priv->podcast_mgr, entry);
1054 		}
1055 
1056 		/* set podcast entries to invisible instead of deleted so they will
1057 		 * not reappear after the podcast has been updated
1058 		 */
1059 		GValue v = {0,};
1060 		g_value_init (&v, G_TYPE_BOOLEAN);
1061 		g_value_set_boolean (&v, TRUE);
1062 		rhythmdb_entry_set (source->priv->db, entry, RHYTHMDB_PROP_HIDDEN, &v);
1063 		g_value_unset (&v);
1064 	}
1065 
1066 	g_list_foreach (entries, (GFunc)rhythmdb_entry_unref, NULL);
1067 	g_list_free (entries);
1068 
1069 	rhythmdb_commit (source->priv->db);
1070 }
1071 
1072 static void
impl_delete_selected(RBSource * asource)1073 impl_delete_selected (RBSource *asource)
1074 {
1075 	RBPodcastSource *source = RB_PODCAST_SOURCE (asource);
1076 	GtkWidget *dialog;
1077 	GtkWidget *button;
1078 	GtkWindow *window;
1079 	RBShell *shell;
1080 
1081 	rb_debug ("Delete episode action");
1082 
1083 	g_object_get (source, "shell", &shell, NULL);
1084 	g_object_get (shell, "window", &window, NULL);
1085 	g_object_unref (shell);
1086 
1087 	dialog = gtk_message_dialog_new (window,
1088 			                 GTK_DIALOG_DESTROY_WITH_PARENT,
1089 					 GTK_MESSAGE_WARNING,
1090 					 GTK_BUTTONS_NONE,
1091 					 _("Delete the podcast episode and downloaded file?"));
1092 
1093 	gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
1094 	                                          _("If you choose to delete the episode and file, "
1095 						    "they will be permanently lost.  Please note that "
1096 						    "you can delete the episode but keep the downloaded "
1097 						    "file by choosing to delete the episode only."));
1098 
1099 	gtk_window_set_title (GTK_WINDOW (dialog), "");
1100 
1101 	gtk_dialog_add_buttons (GTK_DIALOG (dialog),
1102 	                        _("Delete _Episode Only"),
1103 	                        GTK_RESPONSE_NO,
1104 	                        _("_Cancel"),
1105 	                        GTK_RESPONSE_CANCEL,
1106 	                        NULL);
1107 	button = gtk_dialog_add_button (GTK_DIALOG (dialog),
1108 	                                _("_Delete Episode And File"),
1109 			                GTK_RESPONSE_YES);
1110 
1111 	gtk_window_set_focus (GTK_WINDOW (dialog), button);
1112 	gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_YES);
1113 	g_signal_connect (dialog, "response", G_CALLBACK (delete_response_cb), source);
1114 	gtk_widget_show_all (dialog);
1115 }
1116 
1117 static RBEntryView *
impl_get_entry_view(RBSource * asource)1118 impl_get_entry_view (RBSource *asource)
1119 {
1120 	RBPodcastSource *source = RB_PODCAST_SOURCE (asource);
1121 	return source->priv->posts;
1122 }
1123 
1124 static GList *
impl_get_property_views(RBSource * asource)1125 impl_get_property_views (RBSource *asource)
1126 {
1127 	RBPodcastSource *source = RB_PODCAST_SOURCE (asource);
1128 	return g_list_append (NULL, source->priv->feeds);
1129 }
1130 
1131 static RBSourceEOFType
impl_handle_eos(RBSource * asource)1132 impl_handle_eos (RBSource *asource)
1133 {
1134 	return RB_SOURCE_EOF_STOP;
1135 }
1136 
1137 
1138 static gboolean
impl_receive_drag(RBDisplayPage * page,GtkSelectionData * selection_data)1139 impl_receive_drag (RBDisplayPage *page, GtkSelectionData *selection_data)
1140 {
1141 	GList *list, *i;
1142 	RBPodcastSource *source = RB_PODCAST_SOURCE (page);
1143 
1144 	list = rb_uri_list_parse ((const char *) gtk_selection_data_get_data (selection_data));
1145 
1146 	for (i = list; i != NULL; i = i->next) {
1147 		char *uri = NULL;
1148 
1149 		uri = i->data;
1150 		if ((uri != NULL) && (!rhythmdb_entry_lookup_by_location (source->priv->db, uri))) {
1151 			rb_podcast_manager_subscribe_feed (source->priv->podcast_mgr, uri, FALSE);
1152 		}
1153 
1154 		if (gtk_selection_data_get_data_type (selection_data) == gdk_atom_intern ("_NETSCAPE_URL", FALSE)) {
1155 			i = i->next;
1156 		}
1157 	}
1158 
1159 	rb_list_deep_free (list);
1160 	return TRUE;
1161 }
1162 
1163 static void
impl_search(RBSource * asource,RBSourceSearch * search,const char * cur_text,const char * new_text)1164 impl_search (RBSource *asource, RBSourceSearch *search, const char *cur_text, const char *new_text)
1165 {
1166 	RBPodcastSource *source = RB_PODCAST_SOURCE (asource);
1167 
1168 	if (search == NULL) {
1169 		search = source->priv->default_search;
1170 	}
1171 
1172 	if (source->priv->search_query != NULL) {
1173 		rhythmdb_query_free (source->priv->search_query);
1174 		source->priv->search_query = NULL;
1175 	}
1176 	source->priv->search_query = rb_source_search_create_query (search, source->priv->db, new_text);
1177 	rb_podcast_source_do_query (source, TRUE);
1178 
1179 	rb_source_notify_filter_changed (RB_SOURCE (source));
1180 }
1181 
1182 static void
impl_reset_filters(RBSource * asource)1183 impl_reset_filters (RBSource *asource)
1184 {
1185 	RBPodcastSource *source = RB_PODCAST_SOURCE (asource);
1186 
1187 	if (source->priv->search_query != NULL) {
1188 		rhythmdb_query_free (source->priv->search_query);
1189 		source->priv->search_query = NULL;
1190 	}
1191 	rb_source_toolbar_clear_search_entry (source->priv->toolbar);
1192 
1193 	rb_property_view_set_selection (source->priv->feeds, NULL);
1194 	rb_podcast_source_do_query (source, TRUE);
1195 }
1196 
1197 static void
impl_song_properties(RBSource * asource)1198 impl_song_properties (RBSource *asource)
1199 {
1200 	RBPodcastSource *source = RB_PODCAST_SOURCE (asource);
1201 	GtkWidget *dialog = rb_podcast_properties_dialog_new (source->priv->posts);
1202 	if (dialog)
1203 		gtk_widget_show_all (dialog);
1204 }
1205 
1206 static void
impl_get_status(RBDisplayPage * page,char ** text,gboolean * busy)1207 impl_get_status (RBDisplayPage *page, char **text, gboolean *busy)
1208 {
1209 	RBPodcastSource *source = RB_PODCAST_SOURCE (page);
1210 	RhythmDBQueryModel *query_model;
1211 
1212 	/* hack to get these strings marked for translation */
1213 	if (0) {
1214 		ngettext ("%d episode", "%d episodes", 0);
1215 	}
1216 
1217 	g_object_get (page, "query-model", &query_model, NULL);
1218 	if (query_model != NULL) {
1219 		*text = rhythmdb_query_model_compute_status_normal (query_model,
1220 								    "%d episode",
1221 								    "%d episodes");
1222 		g_object_unref (query_model);
1223 	}
1224 
1225 	g_object_get (source->priv->podcast_mgr, "updating", busy, NULL);
1226 }
1227 
1228 static void
podcast_manager_updating_cb(GObject * obj,GParamSpec * pspec,gpointer data)1229 podcast_manager_updating_cb (GObject *obj, GParamSpec *pspec, gpointer data)
1230 {
1231 	rb_display_page_notify_status_changed (RB_DISPLAY_PAGE (data));
1232 }
1233 
1234 static char *
impl_get_delete_label(RBSource * source)1235 impl_get_delete_label (RBSource *source)
1236 {
1237 	return g_strdup (_("Delete"));
1238 }
1239 
1240 static void
impl_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)1241 impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
1242 {
1243 	RBPodcastSource *source = RB_PODCAST_SOURCE (object);
1244 
1245 	switch (prop_id) {
1246 	case PROP_PODCAST_MANAGER:
1247 		source->priv->podcast_mgr = g_value_dup_object (value);
1248 		break;
1249 	case PROP_BASE_QUERY:
1250 		source->priv->base_query = rhythmdb_query_copy (g_value_get_pointer (value));
1251 		break;
1252 	case PROP_SHOW_ALL_FEEDS:
1253 		source->priv->show_all_feeds = g_value_get_boolean (value);
1254 		break;
1255 	case PROP_SHOW_BROWSER:
1256 		gtk_widget_set_visible (GTK_WIDGET (source->priv->feeds), g_value_get_boolean (value));
1257 		break;
1258 	default:
1259 		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
1260 		break;
1261 	}
1262 }
1263 
1264 static void
impl_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)1265 impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
1266 {
1267 	RBPodcastSource *source = RB_PODCAST_SOURCE (object);
1268 
1269 	switch (prop_id) {
1270 	case PROP_PODCAST_MANAGER:
1271 		g_value_set_object (value, source->priv->podcast_mgr);
1272 		break;
1273 	case PROP_BASE_QUERY:
1274 		g_value_set_pointer (value, source->priv->base_query);
1275 		break;
1276 	case PROP_SHOW_ALL_FEEDS:
1277 		g_value_set_boolean (value, source->priv->show_all_feeds);
1278 		break;
1279 	case PROP_SHOW_BROWSER:
1280 		g_value_set_boolean (value, gtk_widget_get_visible (GTK_WIDGET (source->priv->feeds)));
1281 		break;
1282 	default:
1283 		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
1284 		break;
1285 	}
1286 }
1287 
1288 static void
impl_constructed(GObject * object)1289 impl_constructed (GObject *object)
1290 {
1291 	RBPodcastSource *source;
1292 	GtkTreeViewColumn *column;
1293 	GtkCellRenderer *renderer;
1294 	RBShell *shell;
1295 	RBShellPlayer *shell_player;
1296 	GSettings *settings;
1297 	int position;
1298 	GtkAccelGroup *accel_group;
1299 	GtkBuilder *builder;
1300 	GMenu *section;
1301 	GApplication *app;
1302 	GActionEntry actions[] = {
1303 		{ "podcast-add", podcast_add_action_cb },
1304 		{ "podcast-download", podcast_download_action_cb },
1305 		{ "podcast-cancel-download", podcast_download_cancel_action_cb },
1306 		{ "podcast-feed-properties", podcast_feed_properties_action_cb },
1307 		{ "podcast-feed-update", podcast_feed_update_action_cb },
1308 		{ "podcast-feed-update-all", podcast_feed_update_all_action_cb },
1309 		{ "podcast-feed-delete", podcast_feed_delete_action_cb }
1310 	};
1311 
1312 	app = g_application_get_default ();
1313 	RB_CHAIN_GOBJECT_METHOD (rb_podcast_source_parent_class, constructed, object);
1314 	source = RB_PODCAST_SOURCE (object);
1315 
1316 	g_object_get (source, "shell", &shell, NULL);
1317 	g_object_get (shell,
1318 		      "db", &source->priv->db,
1319 		      "shell-player", &shell_player,
1320 		      "accel-group", &accel_group,
1321 		      NULL);
1322 
1323 	_rb_add_display_page_actions (G_ACTION_MAP (app), G_OBJECT (shell), actions, G_N_ELEMENTS (actions));
1324 
1325 	builder = rb_builder_load ("podcast-popups.ui", NULL);
1326 	source->priv->feed_popup = G_MENU_MODEL (gtk_builder_get_object (builder, "podcast-feed-popup"));
1327 	source->priv->episode_popup = G_MENU_MODEL (gtk_builder_get_object (builder, "podcast-episode-popup"));
1328 	rb_application_link_shared_menus (RB_APPLICATION (app), G_MENU (source->priv->feed_popup));
1329 	rb_application_link_shared_menus (RB_APPLICATION (app), G_MENU (source->priv->episode_popup));
1330 
1331 	g_object_ref (source->priv->feed_popup);
1332 	g_object_ref (source->priv->episode_popup);
1333 	g_object_unref (builder);
1334 
1335 	source->priv->default_search = rb_source_search_basic_new (RHYTHMDB_PROP_SEARCH_MATCH, NULL);
1336 
1337 	source->priv->paned = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
1338 
1339 
1340 	/* set up posts view */
1341 	source->priv->posts = rb_entry_view_new (source->priv->db,
1342 						 G_OBJECT (shell_player),
1343 						 TRUE, FALSE);
1344 	g_object_unref (shell_player);
1345 
1346 	/* Podcast date column */
1347 	column = gtk_tree_view_column_new ();
1348 	renderer = gtk_cell_renderer_text_new();
1349 
1350 	gtk_tree_view_column_pack_start (column, renderer, TRUE);
1351 
1352 	gtk_tree_view_column_set_clickable (column, TRUE);
1353 	gtk_tree_view_column_set_resizable (column, TRUE);
1354 	gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
1355 	{
1356 		const char *sample_strings[3];
1357 		sample_strings[0] = _("Date");
1358 		sample_strings[1] = rb_entry_view_get_time_date_column_sample ();
1359 		sample_strings[2] = NULL;
1360 		rb_entry_view_set_fixed_column_width (source->priv->posts, column, renderer, sample_strings);
1361 	}
1362 
1363 	gtk_tree_view_column_set_cell_data_func (column, renderer,
1364 						 (GtkTreeCellDataFunc) podcast_post_date_cell_data_func,
1365 						 source, NULL);
1366 
1367 	rb_entry_view_append_column_custom (source->priv->posts, column,
1368 					    _("Date"), "Date",
1369 					    (GCompareDataFunc) podcast_post_date_sort_func,
1370 					    0, NULL);
1371 
1372 	rb_entry_view_append_column (source->priv->posts, RB_ENTRY_VIEW_COL_TITLE, TRUE);
1373 
1374 	/* COLUMN FEED */
1375 	column = gtk_tree_view_column_new ();
1376 	renderer = gtk_cell_renderer_text_new();
1377 
1378 	gtk_tree_view_column_pack_start (column, renderer, TRUE);
1379 
1380 	gtk_tree_view_column_set_clickable (column, TRUE);
1381 	gtk_tree_view_column_set_resizable (column, TRUE);
1382 	gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
1383 	gtk_tree_view_column_set_expand (column, TRUE);
1384 
1385 	gtk_tree_view_column_set_cell_data_func (column, renderer,
1386 						 (GtkTreeCellDataFunc) podcast_post_feed_cell_data_func,
1387 						 source, NULL);
1388 
1389 	rb_entry_view_append_column_custom (source->priv->posts, column,
1390 					    _("Feed"), "Feed",
1391 					    (GCompareDataFunc) podcast_post_feed_sort_func,
1392 					    0, NULL);
1393 
1394 	rb_entry_view_append_column (source->priv->posts, RB_ENTRY_VIEW_COL_DURATION, FALSE);
1395 	rb_entry_view_append_column (source->priv->posts, RB_ENTRY_VIEW_COL_RATING, FALSE);
1396 	rb_entry_view_append_column (source->priv->posts, RB_ENTRY_VIEW_COL_PLAY_COUNT, FALSE);
1397 	rb_entry_view_append_column (source->priv->posts, RB_ENTRY_VIEW_COL_LAST_PLAYED, FALSE);
1398 
1399 	/* Status column */
1400 	column = gtk_tree_view_column_new ();
1401 	renderer = gtk_cell_renderer_progress_new();
1402 
1403 	gtk_tree_view_column_pack_start (column, renderer, TRUE);
1404 
1405 	gtk_tree_view_column_set_clickable (column, TRUE);
1406 	gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
1407 
1408 	{
1409 		static const char *status_strings[6];
1410 		status_strings[0] = _("Status");
1411 		status_strings[1] = _("Downloaded");
1412 		status_strings[2] = _("Waiting");
1413 		status_strings[3] = _("Failed");
1414 		status_strings[4] = "100 %";
1415 		status_strings[5] = NULL;
1416 
1417 		rb_entry_view_set_fixed_column_width (source->priv->posts,
1418 						      column,
1419 						      renderer,
1420 						      status_strings);
1421 	}
1422 
1423 	gtk_tree_view_column_set_cell_data_func (column, renderer,
1424 						 (GtkTreeCellDataFunc) podcast_post_status_cell_data_func,
1425 						 source, NULL);
1426 
1427 	rb_entry_view_append_column_custom (source->priv->posts, column,
1428 					    _("Status"), "Status",
1429 					    (GCompareDataFunc) podcast_post_status_sort_func,
1430 					    0, NULL);
1431 
1432 	g_signal_connect_object (source->priv->posts,
1433 				 "notify::sort-order",
1434 				 G_CALLBACK (podcast_posts_view_sort_order_changed_cb),
1435 				 source, 0);
1436 
1437 	g_signal_connect_object (source->priv->posts,
1438 				 "show_popup",
1439 				 G_CALLBACK (podcast_posts_show_popup_cb),
1440 				 source, 0);
1441 
1442 	/* configure feed view */
1443 	source->priv->feeds = rb_property_view_new (source->priv->db,
1444 						    RHYTHMDB_PROP_SUBTITLE,
1445 						    _("Feed"));
1446 	rb_property_view_set_column_visible (RB_PROPERTY_VIEW (source->priv->feeds), FALSE);
1447 	rb_property_view_set_selection_mode (RB_PROPERTY_VIEW (source->priv->feeds),
1448 					     GTK_SELECTION_MULTIPLE);
1449 
1450 	/* status indicator column */
1451 	column = gtk_tree_view_column_new ();
1452 	renderer = rb_cell_renderer_pixbuf_new ();
1453 	gtk_tree_view_column_pack_start (column, renderer, TRUE);
1454 	gtk_tree_view_column_set_cell_data_func (column, renderer,
1455 						 (GtkTreeCellDataFunc) podcast_feed_pixbuf_cell_data_func,
1456 						 source, NULL);
1457 	gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
1458 	gtk_tree_view_column_set_fixed_width (column, gdk_pixbuf_get_width (source->priv->error_pixbuf) + 5);
1459 	gtk_tree_view_column_set_reorderable (column, FALSE);
1460 	gtk_tree_view_column_set_visible (column, TRUE);
1461 	rb_property_view_append_column_custom (source->priv->feeds, column);
1462 	g_signal_connect_object (renderer,
1463 				 "pixbuf-clicked",
1464 				 G_CALLBACK (podcast_status_pixbuf_clicked_cb),
1465 				 source, 0);
1466 
1467 	/* redraw status when errors are set or cleared */
1468 	g_signal_connect_object (source->priv->db,
1469 				 "entry_changed",
1470 				 G_CALLBACK (podcast_entry_changed_cb),
1471 				 source, 0);
1472 
1473 	/* title column */
1474 	column = gtk_tree_view_column_new ();
1475 	renderer = gtk_cell_renderer_text_new ();
1476 
1477 	gtk_tree_view_column_pack_start (column, renderer, TRUE);
1478 
1479 	gtk_tree_view_column_set_cell_data_func (column,
1480 						 renderer,
1481 						 (GtkTreeCellDataFunc) podcast_feed_title_cell_data_func,
1482 						 source, NULL);
1483 
1484 	gtk_tree_view_column_set_title (column, _("Feed"));
1485 	gtk_tree_view_column_set_reorderable (column, FALSE);
1486 	gtk_tree_view_column_set_visible (column, TRUE);
1487 	rb_property_view_append_column_custom (source->priv->feeds, column);
1488 
1489 	g_signal_connect_object (source->priv->feeds, "show_popup",
1490 				 G_CALLBACK (podcast_feeds_show_popup_cb),
1491 				 source, 0);
1492 
1493 	g_signal_connect_object (source->priv->feeds,
1494 				 "properties-selected",
1495 				 G_CALLBACK (feed_select_change_cb),
1496 				 source, 0);
1497 
1498 	rb_property_view_set_search_func (source->priv->feeds,
1499 					  (GtkTreeViewSearchEqualFunc) podcast_feed_title_search_func,
1500 					  source,
1501 					  NULL);
1502 
1503 	/* set up drag and drop */
1504 	g_signal_connect_object (source->priv->feeds,
1505 				 "drag_data_received",
1506 				 G_CALLBACK (posts_view_drag_data_received_cb),
1507 				 source, 0);
1508 
1509 	gtk_drag_dest_set (GTK_WIDGET (source->priv->feeds),
1510 			   GTK_DEST_DEFAULT_ALL,
1511 			   posts_view_drag_types, 2,
1512 			   GDK_ACTION_COPY | GDK_ACTION_MOVE);
1513 
1514 	g_signal_connect_object (G_OBJECT (source->priv->posts),
1515 				 "drag_data_received",
1516 				 G_CALLBACK (posts_view_drag_data_received_cb),
1517 				 source, 0);
1518 
1519 	gtk_drag_dest_set (GTK_WIDGET (source->priv->posts),
1520 			   GTK_DEST_DEFAULT_ALL,
1521 			   posts_view_drag_types, 2,
1522 			   GDK_ACTION_COPY | GDK_ACTION_MOVE);
1523 
1524 	/* set up toolbar */
1525 	source->priv->toolbar = rb_source_toolbar_new (RB_DISPLAY_PAGE (source), accel_group);
1526 
1527 	source->priv->search_action = rb_source_create_search_action (RB_SOURCE (source));
1528 	g_action_map_add_action (G_ACTION_MAP (g_application_get_default ()), source->priv->search_action);
1529 
1530 	rb_source_search_basic_register (RHYTHMDB_PROP_SEARCH_MATCH, "search-match", _("Search all fields"));
1531 	rb_source_search_basic_register (RHYTHMDB_PROP_ALBUM_FOLDED, "podcast-feed", _("Search podcast feeds"));
1532 	rb_source_search_basic_register (RHYTHMDB_PROP_TITLE_FOLDED, "podcast-episode", _("Search podcast episodes"));
1533 
1534 	section = g_menu_new ();
1535 	rb_source_search_add_to_menu (section, "app", source->priv->search_action, "search-match");
1536 	rb_source_search_add_to_menu (section, "app", source->priv->search_action, "podcast-feed");
1537 	rb_source_search_add_to_menu (section, "app", source->priv->search_action, "podcast-episode");
1538 	source->priv->search_popup = G_MENU_MODEL (g_menu_new ());
1539 	g_menu_append_section (G_MENU (source->priv->search_popup), NULL, G_MENU_MODEL (section));
1540 
1541 	rb_source_toolbar_add_search_entry_menu (source->priv->toolbar, source->priv->search_popup, source->priv->search_action);
1542 
1543 	/* pack the feed and post views into the source */
1544 	gtk_paned_pack1 (GTK_PANED (source->priv->paned),
1545 			 GTK_WIDGET (source->priv->feeds), FALSE, FALSE);
1546 	gtk_paned_pack2 (GTK_PANED (source->priv->paned),
1547 			 GTK_WIDGET (source->priv->posts), TRUE, FALSE);
1548 
1549 	source->priv->grid = gtk_grid_new ();
1550 	gtk_widget_set_margin_top (GTK_WIDGET (source->priv->grid), 6);
1551 	gtk_grid_set_column_spacing (GTK_GRID (source->priv->grid), 6);
1552 	gtk_grid_set_row_spacing (GTK_GRID (source->priv->grid), 6);
1553 	gtk_grid_attach (GTK_GRID (source->priv->grid), GTK_WIDGET (source->priv->toolbar), 0, 0, 1, 1);
1554 	gtk_grid_attach (GTK_GRID (source->priv->grid), source->priv->paned, 0, 1, 1, 1);
1555 
1556 	gtk_container_add (GTK_CONTAINER (source), source->priv->grid);
1557 
1558 	/* podcast add dialog */
1559 	source->priv->add_dialog = rb_podcast_add_dialog_new (shell, source->priv->podcast_mgr);
1560 	gtk_widget_show_all (source->priv->add_dialog);
1561 	gtk_widget_set_margin_top (source->priv->add_dialog, 0);
1562 	gtk_grid_attach (GTK_GRID (source->priv->grid), GTK_WIDGET (source->priv->add_dialog), 0, 2, 1, 1);
1563 	gtk_widget_set_no_show_all (source->priv->add_dialog, TRUE);
1564 	g_signal_connect_object (source->priv->add_dialog, "closed", G_CALLBACK (podcast_add_dialog_closed_cb), source, 0);
1565 
1566 	gtk_widget_show_all (GTK_WIDGET (source));
1567 	gtk_widget_hide (source->priv->add_dialog);
1568 
1569 	g_object_get (source, "settings", &settings, NULL);
1570 
1571 	g_signal_connect_object (settings, "changed", G_CALLBACK (settings_changed_cb), source, 0);
1572 
1573 	position = g_settings_get_int (settings, PODCAST_PANED_POSITION);
1574 	gtk_paned_set_position (GTK_PANED (source->priv->paned), position);
1575 
1576 	rb_source_bind_settings (RB_SOURCE (source),
1577 				 GTK_WIDGET (source->priv->posts),
1578 				 source->priv->paned,
1579 				 GTK_WIDGET (source->priv->feeds),
1580 				 TRUE);
1581 
1582 	g_object_unref (settings);
1583 	g_object_unref (accel_group);
1584 	g_object_unref (shell);
1585 
1586 	g_signal_connect (source->priv->podcast_mgr, "notify::updating", G_CALLBACK (podcast_manager_updating_cb), source);
1587 
1588 	rb_podcast_source_do_query (source, TRUE);
1589 }
1590 
1591 static void
impl_dispose(GObject * object)1592 impl_dispose (GObject *object)
1593 {
1594 	RBPodcastSource *source;
1595 
1596 	source = RB_PODCAST_SOURCE (object);
1597 
1598 	g_clear_pointer (&source->priv->search_query, rhythmdb_query_free);
1599 	g_clear_object (&source->priv->db);
1600 	g_clear_object (&source->priv->podcast_mgr);
1601 	g_clear_object (&source->priv->error_pixbuf);
1602 	g_clear_object (&source->priv->refresh_pixbuf);
1603 	g_clear_object (&source->priv->search_action);
1604 	g_clear_object (&source->priv->search_popup);
1605 
1606 	G_OBJECT_CLASS (rb_podcast_source_parent_class)->dispose (object);
1607 }
1608 
1609 static void
impl_finalize(GObject * object)1610 impl_finalize (GObject *object)
1611 {
1612 	RBPodcastSource *source;
1613 
1614 	g_return_if_fail (object != NULL);
1615 	g_return_if_fail (RB_IS_PODCAST_SOURCE (object));
1616 
1617 	source = RB_PODCAST_SOURCE (object);
1618 
1619 	g_return_if_fail (source->priv != NULL);
1620 
1621 	if (source->priv->selected_feeds) {
1622 		g_list_foreach (source->priv->selected_feeds, (GFunc) g_free, NULL);
1623 	        g_list_free (source->priv->selected_feeds);
1624 	}
1625 
1626 	G_OBJECT_CLASS (rb_podcast_source_parent_class)->finalize (object);
1627 }
1628 
1629 static void
rb_podcast_source_init(RBPodcastSource * source)1630 rb_podcast_source_init (RBPodcastSource *source)
1631 {
1632 	GtkIconTheme *icon_theme;
1633 	source->priv = G_TYPE_INSTANCE_GET_PRIVATE (source,
1634 						    RB_TYPE_PODCAST_SOURCE,
1635 						    RBPodcastSourcePrivate);
1636 
1637 	source->priv->selected_feeds = NULL;
1638 
1639 	icon_theme = gtk_icon_theme_get_default ();
1640 	source->priv->error_pixbuf = gtk_icon_theme_load_icon (icon_theme,
1641 							       "dialog-error-symbolic",
1642 							       16,
1643 							       0,
1644 							       NULL);
1645 	source->priv->refresh_pixbuf = gtk_icon_theme_load_icon (icon_theme,
1646 								 "view-refresh-symbolic",
1647 								 16,
1648 								 0,
1649 								 NULL);
1650 }
1651 
1652 static void
rb_podcast_source_class_init(RBPodcastSourceClass * klass)1653 rb_podcast_source_class_init (RBPodcastSourceClass *klass)
1654 {
1655 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
1656 	RBDisplayPageClass *page_class = RB_DISPLAY_PAGE_CLASS (klass);
1657 	RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
1658 
1659 	object_class->dispose = impl_dispose;
1660 	object_class->finalize = impl_finalize;
1661 	object_class->constructed = impl_constructed;
1662 	object_class->set_property = impl_set_property;
1663 	object_class->get_property = impl_get_property;
1664 
1665 	page_class->get_status = impl_get_status;
1666 	page_class->receive_drag = impl_receive_drag;
1667 
1668 	source_class->reset_filters = impl_reset_filters;
1669 	source_class->add_to_queue = impl_add_to_queue;
1670 	source_class->can_add_to_queue = (RBSourceFeatureFunc) rb_true_function;
1671 	source_class->can_copy = (RBSourceFeatureFunc) rb_false_function;
1672 	source_class->can_cut = (RBSourceFeatureFunc) rb_false_function;
1673 	source_class->can_delete = (RBSourceFeatureFunc) rb_true_function;
1674 	source_class->delete_selected = impl_delete_selected;
1675 	source_class->get_entry_view = impl_get_entry_view;
1676 	source_class->get_property_views = impl_get_property_views;
1677 	source_class->handle_eos = impl_handle_eos;
1678 	source_class->search = impl_search;
1679 	source_class->song_properties = impl_song_properties;
1680 	source_class->get_delete_label = impl_get_delete_label;
1681 
1682 	g_object_class_install_property (object_class,
1683 					 PROP_PODCAST_MANAGER,
1684 					 g_param_spec_object ("podcast-manager",
1685 					                      "RBPodcastManager",
1686 					                      "RBPodcastManager object",
1687 					                      RB_TYPE_PODCAST_MANAGER,
1688 					                      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
1689 	g_object_class_install_property (object_class,
1690 					 PROP_BASE_QUERY,
1691 					 g_param_spec_pointer ("base-query",
1692 							       "Base query",
1693 							       "Base query for the source",
1694 							       G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
1695 	g_object_class_install_property (object_class,
1696 					 PROP_SHOW_ALL_FEEDS,
1697 					 g_param_spec_boolean ("show-all-feeds",
1698 							       "show-all-feeds",
1699 							       "show all feeds",
1700 							       FALSE,
1701 							       G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
1702 
1703 	g_object_class_override_property (object_class, PROP_SHOW_BROWSER, "show-browser");
1704 
1705 	g_type_class_add_private (klass, sizeof (RBPodcastSourcePrivate));
1706 }
1707