hythmbox-2.98/podcast/rb-podcast-source.c

No issues found

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