hythmbox-2.98/podcast/rb-podcast-add-dialog.c

No issues found

  1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
  2  *
  3  *  Copyright (C) 2010 Jonathan Matthew <jonathan@d14n.org>
  4  *
  5  *  This program is free software; you can redistribute it and/or modify
  6  *  it under the terms of the GNU General Public License as published by
  7  *  the Free Software Foundation; either version 2 of the License, or
  8  *  (at your option) any later version.
  9  *
 10  *  The Rhythmbox authors hereby grant permission for non-GPL compatible
 11  *  GStreamer plugins to be used and distributed together with GStreamer
 12  *  and Rhythmbox. This permission is above and beyond the permissions granted
 13  *  by the GPL license by which Rhythmbox is covered. If you modify this code
 14  *  you may extend this exception to your version of the code, but you are not
 15  *  obligated to do so. If you do not wish to do so, delete this exception
 16  *  statement from your version.
 17  *
 18  *  This program is distributed in the hope that it will be useful,
 19  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 20  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 21  *  GNU General Public License for more details.
 22  *
 23  *  You should have received a copy of the GNU General Public License
 24  *  along with this program; if not, write to the Free Software
 25  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
 26  *
 27  */
 28 
 29 #include "config.h"
 30 
 31 #include <glib/gi18n.h>
 32 #include <gtk/gtk.h>
 33 
 34 #include "rb-shell.h"
 35 #include "rb-shell-player.h"
 36 #include "rb-podcast-add-dialog.h"
 37 #include "rb-podcast-search.h"
 38 #include "rb-podcast-entry-types.h"
 39 #include "rb-builder-helpers.h"
 40 #include "rb-debug.h"
 41 #include "rb-util.h"
 42 #include "rb-cut-and-paste-code.h"
 43 #include "rb-search-entry.h"
 44 
 45 static void rb_podcast_add_dialog_class_init (RBPodcastAddDialogClass *klass);
 46 static void rb_podcast_add_dialog_init (RBPodcastAddDialog *dialog);
 47 
 48 enum {
 49 	PROP_0,
 50 	PROP_PODCAST_MANAGER,
 51 	PROP_SHELL,
 52 };
 53 
 54 enum {
 55 	CLOSE,
 56 	CLOSED,
 57 	LAST_SIGNAL
 58 };
 59 
 60 enum {
 61 	FEED_COLUMN_TITLE = 0,
 62 	FEED_COLUMN_AUTHOR,
 63 	FEED_COLUMN_IMAGE,
 64 	FEED_COLUMN_IMAGE_FILE,
 65 	FEED_COLUMN_EPISODE_COUNT,
 66 	FEED_COLUMN_PARSED_FEED,
 67 	FEED_COLUMN_DATE,
 68 };
 69 
 70 struct RBPodcastAddDialogPrivate
 71 {
 72 	RBPodcastManager *podcast_mgr;
 73 	RhythmDB *db;
 74 	RBShell *shell;
 75 
 76 	GtkWidget *feed_view;
 77 	GtkListStore *feed_model;
 78 
 79 	GtkWidget *episode_view;
 80 
 81 	GtkWidget *text_entry;
 82 	GtkWidget *subscribe_button;
 83 	GtkWidget *info_bar;
 84 	GtkWidget *info_bar_message;
 85 
 86 	RBSearchEntry *search_entry;
 87 
 88 	gboolean paned_size_set;
 89 	gboolean have_selection;
 90 	gboolean clearing;
 91 	GtkTreeIter selected_feed;
 92 
 93 	int running_searches;
 94 	gboolean search_successful;
 95 };
 96 
 97 /* various prefixes that identify things we treat as feed URLs rather than search terms */
 98 static const char *podcast_uri_prefixes[] = {
 99 	"http://",
100 	"https://",
101 	"feed://",
102 	"zcast://",
103 	"zune://",
104 	"itpc://",
105 	"itms://",
106 	"www.",
107 };
108 
109 /* number of search results to request from each available search */
110 #define PODCAST_SEARCH_LIMIT		25
111 
112 #define PODCAST_IMAGE_SIZE		50
113 
114 static guint signals[LAST_SIGNAL] = {0,};
115 
116 G_DEFINE_TYPE (RBPodcastAddDialog, rb_podcast_add_dialog, GTK_TYPE_BOX);
117 
118 
119 static gboolean
120 remove_all_feeds_cb (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, RBPodcastAddDialog *dialog)
121 {
122 	RBPodcastChannel *channel;
123 	gtk_tree_model_get (model, iter, FEED_COLUMN_PARSED_FEED, &channel, -1);
124 	rb_podcast_parse_channel_free (channel);
125 	return FALSE;
126 }
127 
128 static void
129 remove_all_feeds (RBPodcastAddDialog *dialog)
130 {
131 	/* remove all feeds from the model and free associated data */
132 	gtk_tree_model_foreach (GTK_TREE_MODEL (dialog->priv->feed_model),
133 				(GtkTreeModelForeachFunc) remove_all_feeds_cb,
134 				dialog);
135 
136 	dialog->priv->clearing = TRUE;
137 	gtk_list_store_clear (dialog->priv->feed_model);
138 	dialog->priv->clearing = FALSE;
139 
140 	dialog->priv->have_selection = FALSE;
141 	gtk_widget_set_sensitive (dialog->priv->subscribe_button, FALSE);
142 }
143 
144 static void
145 add_posts_for_feed (RBPodcastAddDialog *dialog, RBPodcastChannel *channel)
146 {
147 	GList *l;
148 
149 	for (l = channel->posts; l != NULL; l = l->next) {
150 		RBPodcastItem *item = (RBPodcastItem *) l->data;
151 
152 		rb_podcast_manager_add_post (dialog->priv->db,
153 					     TRUE,
154 					     channel->title ? channel->title : channel->url,
155 					     item->title,
156 					     channel->url,
157 					     (item->author ? item->author : channel->author),
158 					     item->url,
159 					     item->description,
160 					     (item->pub_date > 0 ? item->pub_date : channel->pub_date),
161 					     item->duration,
162 					     item->filesize);
163 	}
164 
165 	rhythmdb_commit (dialog->priv->db);
166 }
167 
168 static void
169 image_file_read_cb (GObject *file, GAsyncResult *result, RBPodcastAddDialog *dialog)
170 {
171 	GFileInputStream *stream;
172 	GdkPixbuf *pixbuf;
173 	GError *error = NULL;
174 
175 	stream = g_file_read_finish (G_FILE (file), result, &error);
176 	if (error != NULL) {
177 		rb_debug ("podcast image read failed: %s", error->message);
178 		g_clear_error (&error);
179 		g_object_unref (dialog);
180 		return;
181 	}
182 
183 	pixbuf = gdk_pixbuf_new_from_stream_at_scale (G_INPUT_STREAM (stream), PODCAST_IMAGE_SIZE, PODCAST_IMAGE_SIZE, TRUE, NULL, &error);
184 	if (error != NULL) {
185 		rb_debug ("podcast image load failed: %s", error->message);
186 		g_clear_error (&error);
187 	} else {
188 		GtkTreeIter iter;
189 
190 		if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (dialog->priv->feed_model), &iter)) {
191 			do {
192 				GFile *feedfile;
193 				gtk_tree_model_get (GTK_TREE_MODEL (dialog->priv->feed_model), &iter,
194 						    FEED_COLUMN_IMAGE_FILE, &feedfile,
195 						    -1);
196 				if (feedfile == G_FILE (file)) {
197 					gtk_list_store_set (dialog->priv->feed_model,
198 							    &iter,
199 							    FEED_COLUMN_IMAGE, g_object_ref (pixbuf),
200 							    -1);
201 					break;
202 				}
203 			} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (dialog->priv->feed_model), &iter));
204 		}
205 		g_object_unref (pixbuf);
206 	}
207 
208 	g_object_unref (dialog);
209 	g_object_unref (stream);
210 }
211 
212 static void
213 insert_search_result (RBPodcastAddDialog *dialog, RBPodcastChannel *channel, gboolean select)
214 {
215 	GtkTreeIter iter;
216 	GFile *image_file;
217 	int episodes;
218 
219 	if (channel->posts) {
220 		episodes = g_list_length (channel->posts);
221 	} else {
222 		episodes = channel->num_posts;
223 	}
224 
225 	/* if there's an image to load, fetch it */
226 	if (channel->img) {
227 		rb_debug ("fetching image %s", channel->img);
228 		image_file = g_file_new_for_uri (channel->img);
229 	} else {
230 		image_file = NULL;
231 	}
232 
233 	gtk_list_store_insert_with_values (dialog->priv->feed_model,
234 					   &iter,
235 					   G_MAXINT,
236 					   FEED_COLUMN_TITLE, channel->title,
237 					   FEED_COLUMN_AUTHOR, channel->author,
238 					   FEED_COLUMN_EPISODE_COUNT, episodes,
239 					   FEED_COLUMN_IMAGE, NULL,
240 					   FEED_COLUMN_IMAGE_FILE, image_file,
241 					   FEED_COLUMN_PARSED_FEED, channel,
242 					   -1);
243 
244 	if (image_file != NULL) {
245 		g_file_read_async (image_file,
246 				   G_PRIORITY_DEFAULT,
247 				   NULL,
248 				   (GAsyncReadyCallback) image_file_read_cb,
249 				   g_object_ref (dialog));
250 	}
251 
252 	if (select) {
253 		GtkTreeSelection *selection;
254 		selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->priv->feed_view));
255 		gtk_tree_selection_select_iter (selection, &iter);
256 	}
257 }
258 
259 typedef struct {
260 	RBPodcastAddDialog *dialog;
261 	char *url;
262 	RBPodcastChannel *channel;
263 	gboolean existing;
264 	gboolean single;
265 	GError *error;
266 } ParseThreadData;
267 
268 static gboolean
269 parse_finished (ParseThreadData *data)
270 {
271 	if (data->error != NULL) {
272 		gtk_label_set_label (GTK_LABEL (data->dialog->priv->info_bar_message),
273 				     _("Unable to load the feed. Check your network connection."));
274 		gtk_widget_show (data->dialog->priv->info_bar);
275 	} else {
276 		gtk_widget_hide (data->dialog->priv->info_bar);
277 	}
278 
279 	if (data->channel->is_opml) {
280 		GList *l;
281 		/* convert each item into its own channel */
282 		for (l = data->channel->posts; l != NULL; l = l->next) {
283 			RBPodcastChannel *channel;
284 			RBPodcastItem *item;
285 
286 			item = l->data;
287 			channel = g_new0 (RBPodcastChannel, 1);
288 			channel->url = g_strdup (item->url);
289 			channel->title = g_strdup (item->title);
290 			/* none of the other fields get populated anyway */
291 			insert_search_result (data->dialog, channel, FALSE);
292 		}
293 		rb_podcast_parse_channel_free (data->channel);
294 	} else if (data->existing) {
295 		/* find the row for the feed, replace the channel */
296 		GtkTreeIter iter;
297 		gboolean found = FALSE;
298 
299 		if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (data->dialog->priv->feed_model), &iter)) {
300 			do {
301 				RBPodcastChannel *channel;
302 				gtk_tree_model_get (GTK_TREE_MODEL (data->dialog->priv->feed_model), &iter,
303 						    FEED_COLUMN_PARSED_FEED, &channel,
304 						    -1);
305 				if (g_strcmp0 (channel->url, data->url) == 0) {
306 					gtk_list_store_set (data->dialog->priv->feed_model,
307 							    &iter,
308 							    FEED_COLUMN_PARSED_FEED, data->channel,
309 							    -1);
310 					found = TRUE;
311 					rb_podcast_parse_channel_free (channel);
312 					break;
313 				}
314 			} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (data->dialog->priv->feed_model), &iter));
315 		}
316 
317 		/* if the row is selected, create entries for the channel contents */
318 		if (data->dialog->priv->have_selection && found) {
319 			GtkTreePath *a;
320 			GtkTreePath *b;
321 
322 			a = gtk_tree_model_get_path (GTK_TREE_MODEL (data->dialog->priv->feed_model), &iter);
323 			b = gtk_tree_model_get_path (GTK_TREE_MODEL (data->dialog->priv->feed_model), &data->dialog->priv->selected_feed);
324 			if (gtk_tree_path_compare (a, b) == 0) {
325 				add_posts_for_feed (data->dialog, data->channel);
326 			}
327 
328 			gtk_tree_path_free (a);
329 			gtk_tree_path_free (b);
330 		} else {
331 			rb_podcast_parse_channel_free (data->channel);
332 		}
333 	} else {
334 		/* model owns data->channel now */
335 		insert_search_result (data->dialog, data->channel, data->single);
336 	}
337 
338 	g_object_unref (data->dialog);
339 	g_clear_error (&data->error);
340 	g_free (data->url);
341 	g_free (data);
342 	return FALSE;
343 }
344 
345 static gpointer
346 parse_thread (ParseThreadData *data)
347 {
348 	if (rb_podcast_parse_load_feed (data->channel, data->url, FALSE, &data->error) == FALSE) {
349 		/* fake up a channel with just the url as the title, allowing the user
350 		 * to subscribe to the podcast anyway.
351 		 */
352 		data->channel->url = g_strdup (data->url);
353 		data->channel->title = g_strdup (data->url);
354 	}
355 
356 	g_idle_add ((GSourceFunc) parse_finished, data);
357 	return NULL;
358 }
359 
360 static void
361 parse_in_thread (RBPodcastAddDialog *dialog, const char *text, gboolean existing, gboolean single)
362 {
363 	ParseThreadData *data;
364 
365 	data = g_new0 (ParseThreadData, 1);
366 	data->dialog = g_object_ref (dialog);
367 	data->url = g_strdup (text);
368 	data->channel = g_new0 (RBPodcastChannel, 1);
369 	data->existing = existing;
370 	data->single = single;
371 
372 	g_thread_new ("podcast parser", (GThreadFunc) parse_thread, data);
373 }
374 
375 static void
376 podcast_search_result_cb (RBPodcastSearch *search, RBPodcastChannel *feed, RBPodcastAddDialog *dialog)
377 {
378 	rb_debug ("got result %s from podcast search %s", feed->url, G_OBJECT_TYPE_NAME (search));
379 	insert_search_result (dialog, rb_podcast_parse_channel_copy (feed), FALSE);
380 }
381 
382 static void
383 podcast_search_finished_cb (RBPodcastSearch *search, gboolean successful, RBPodcastAddDialog *dialog)
384 {
385 	rb_debug ("podcast search %s finished", G_OBJECT_TYPE_NAME (search));
386 	g_object_unref (search);
387 
388 	dialog->priv->search_successful |= successful;
389 
390 	dialog->priv->running_searches--;
391 	if (dialog->priv->running_searches == 0) {
392 		if (dialog->priv->search_successful == FALSE) {
393 			gtk_label_set_label (GTK_LABEL (dialog->priv->info_bar_message),
394 					     _("Unable to search for podcasts. Check your network connection."));
395 			gtk_widget_show (dialog->priv->info_bar);
396 		}
397 	}
398 }
399 
400 static void
401 search_cb (RBSearchEntry *entry, const char *text, RBPodcastAddDialog *dialog)
402 {
403 	GList *searches;
404 	GList *s;
405 	int i;
406 
407 	/* remove previous feeds */
408 	remove_all_feeds (dialog);
409 	rhythmdb_entry_delete_by_type (dialog->priv->db, RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH);
410 	rhythmdb_commit (dialog->priv->db);
411 
412 	gtk_widget_hide (dialog->priv->info_bar);
413 
414 	if (text == NULL || text[0] == '\0') {
415 		return;
416 	}
417 
418 	/* if the entered text looks like a feed URL, parse it directly */
419 	for (i = 0; i < G_N_ELEMENTS (podcast_uri_prefixes); i++) {
420 		if (g_str_has_prefix (text, podcast_uri_prefixes[i])) {
421 			parse_in_thread (dialog, text, FALSE, TRUE);
422 			return;
423 		}
424 	}
425 
426 	/* not really sure about this one */
427 	if (g_path_is_absolute (text)) {
428 		parse_in_thread (dialog, text, FALSE, TRUE);
429 		return;
430 	}
431 
432 	/* otherwise, try podcast searches */
433 	dialog->priv->search_successful = FALSE;
434 	searches = rb_podcast_manager_get_searches (dialog->priv->podcast_mgr);
435 	for (s = searches; s != NULL; s = s->next) {
436 		RBPodcastSearch *search = s->data;
437 
438 		g_signal_connect_object (search, "result", G_CALLBACK (podcast_search_result_cb), dialog, 0);
439 		g_signal_connect_object (search, "finished", G_CALLBACK (podcast_search_finished_cb), dialog, 0);
440 		rb_podcast_search_start (search, text, PODCAST_SEARCH_LIMIT);
441 		dialog->priv->running_searches++;
442 	}
443 }
444 
445 static void
446 subscribe_selected_feed (RBPodcastAddDialog *dialog)
447 {
448 	RBPodcastChannel *channel;
449 
450 	g_assert (dialog->priv->have_selection);
451 
452 	rhythmdb_entry_delete_by_type (dialog->priv->db, RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH);
453 	rhythmdb_commit (dialog->priv->db);
454 
455 	/* subscribe selected feed */
456 	gtk_tree_model_get (GTK_TREE_MODEL (dialog->priv->feed_model),
457 			    &dialog->priv->selected_feed,
458 			    FEED_COLUMN_PARSED_FEED, &channel,
459 			    -1);
460 	if (channel->posts != NULL) {
461 		rb_podcast_manager_add_parsed_feed (dialog->priv->podcast_mgr, channel);
462 	} else {
463 		rb_podcast_manager_subscribe_feed (dialog->priv->podcast_mgr, channel->url, TRUE);
464 	}
465 }
466 
467 static void
468 subscribe_clicked_cb (GtkButton *button, RBPodcastAddDialog *dialog)
469 {
470 	if (dialog->priv->have_selection == FALSE) {
471 		rb_debug ("no selection");
472 		return;
473 	}
474 
475 	subscribe_selected_feed (dialog);
476 
477 	dialog->priv->clearing = TRUE;
478 	gtk_list_store_remove (GTK_LIST_STORE (dialog->priv->feed_model), &dialog->priv->selected_feed);
479 	dialog->priv->clearing = FALSE;
480 
481 	gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->priv->feed_view)));
482 }
483 
484 static void
485 close_clicked_cb (GtkButton *button, RBPodcastAddDialog *dialog)
486 {
487 	g_signal_emit (dialog, signals[CLOSED], 0);
488 }
489 
490 static void
491 feed_activated_cb (GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *column, RBPodcastAddDialog *dialog)
492 {
493 	gtk_tree_model_get_iter (GTK_TREE_MODEL (dialog->priv->feed_model), &dialog->priv->selected_feed, path);
494 	dialog->priv->have_selection = TRUE;
495 
496 	subscribe_selected_feed (dialog);
497 
498 	dialog->priv->have_selection = FALSE;
499 
500 	g_signal_emit (dialog, signals[CLOSED], 0);
501 }
502 
503 static void
504 feed_selection_changed_cb (GtkTreeSelection *selection, RBPodcastAddDialog *dialog)
505 {
506 	GtkTreeModel *model;
507 
508 	if (dialog->priv->clearing)
509 		return;
510 
511 	dialog->priv->have_selection =
512 		gtk_tree_selection_get_selected (selection, &model, &dialog->priv->selected_feed);
513 	gtk_widget_set_sensitive (dialog->priv->subscribe_button, dialog->priv->have_selection);
514 
515 	rhythmdb_entry_delete_by_type (dialog->priv->db, RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH);
516 	rhythmdb_commit (dialog->priv->db);
517 
518 	if (dialog->priv->have_selection) {
519 		RBPodcastChannel *channel = NULL;
520 
521 		gtk_tree_model_get (model,
522 				    &dialog->priv->selected_feed,
523 				    FEED_COLUMN_PARSED_FEED, &channel,
524 				    -1);
525 
526 		if (channel->posts == NULL) {
527 			rb_debug ("parsing feed %s to get posts", channel->url);
528 			parse_in_thread (dialog, channel->url, TRUE, FALSE);
529 		} else {
530 			add_posts_for_feed (dialog, channel);
531 		}
532 	}
533 }
534 
535 static void
536 episode_count_column_cell_data_func (GtkTreeViewColumn *column,
537 				     GtkCellRenderer *renderer,
538 				     GtkTreeModel *model,
539 				     GtkTreeIter *iter,
540 				     gpointer data)
541 {
542 	GtkTreeIter parent;
543 	if (gtk_tree_model_iter_parent (model, &parent, iter)) {
544 		g_object_set (renderer, "visible", FALSE, NULL);
545 	} else {
546 		int count;
547 		char *text;
548 		gtk_tree_model_get (model, iter, FEED_COLUMN_EPISODE_COUNT, &count, -1);
549 		text = g_strdup_printf ("%d", count);
550 		g_object_set (renderer, "visible", TRUE, "text", text, NULL);
551 		g_free (text);
552 	}
553 }
554 
555 static void
556 podcast_post_date_cell_data_func (GtkTreeViewColumn *column,
557 				  GtkCellRenderer *renderer,
558 				  GtkTreeModel *tree_model,
559 				  GtkTreeIter *iter,
560 				  gpointer data)
561 {
562 	RhythmDBEntry *entry;
563 	gulong value;
564 	char *str;
565 
566 	gtk_tree_model_get (tree_model, iter, 0, &entry, -1);
567 
568 	value = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_POST_TIME);
569         if (value == 0) {
570 		str = g_strdup (_("Unknown"));
571 	} else {
572 		str = rb_utf_friendly_time (value);
573 	}
574 
575 	g_object_set (G_OBJECT (renderer), "text", str, NULL);
576 	g_free (str);
577 
578 	rhythmdb_entry_unref (entry);
579 }
580 
581 static gint
582 podcast_post_date_sort_func (RhythmDBEntry *a,
583 			     RhythmDBEntry *b,
584 			     RhythmDBQueryModel *model)
585 {
586 	gulong a_val, b_val;
587 	gint ret;
588 
589 	a_val = rhythmdb_entry_get_ulong (a, RHYTHMDB_PROP_POST_TIME);
590 	b_val = rhythmdb_entry_get_ulong (b, RHYTHMDB_PROP_POST_TIME);
591 
592 	if (a_val != b_val)
593 		ret = (a_val > b_val) ? 1 : -1;
594 	else
595 		ret = rhythmdb_query_model_title_sort_func (a, b, model);
596 
597         return ret;
598 }
599 
600 static void
601 episodes_sort_changed_cb (GObject *object, GParamSpec *pspec, RBPodcastAddDialog *dialog)
602 {
603 	rb_entry_view_resort_model (RB_ENTRY_VIEW (object));
604 }
605 
606 static void
607 impl_close (RBPodcastAddDialog *dialog)
608 {
609 	g_signal_emit (dialog, signals[CLOSED], 0);
610 }
611 
612 static gboolean
613 set_paned_position (GtkWidget *paned)
614 {
615 	gtk_paned_set_position (GTK_PANED (paned), gtk_widget_get_allocated_height (paned) / 2);
616 	g_object_unref (paned);
617 	return FALSE;
618 }
619 
620 static void
621 paned_size_allocate_cb (GtkWidget *widget, GdkRectangle *allocation, RBPodcastAddDialog *dialog)
622 {
623 	if (dialog->priv->paned_size_set == FALSE) {
624 		dialog->priv->paned_size_set = TRUE;
625 		g_idle_add ((GSourceFunc) set_paned_position, g_object_ref (widget));
626 	}
627 }
628 
629 static void
630 episode_entry_activated_cb (RBEntryView *entry_view, RhythmDBEntry *entry, RBPodcastAddDialog *dialog)
631 {
632 	rb_debug ("search result podcast entry %s activated", rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION));
633 	rb_shell_load_uri (dialog->priv->shell,
634 			   rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION),
635 			   TRUE,
636 			   NULL);
637 }
638 
639 static void
640 impl_constructed (GObject *object)
641 {
642 	RBPodcastAddDialog *dialog;
643 	GtkBuilder *builder;
644 	GtkWidget *widget;
645 	GtkWidget *paned;
646 	GtkTreeViewColumn *column;
647 	GtkCellRenderer *renderer;
648 	RBEntryView *episodes;
649 	RBShellPlayer *shell_player;
650 	RhythmDBQuery *query;
651 	RhythmDBQueryModel *query_model;
652 	const char *episode_strings[3];
653 
654 	RB_CHAIN_GOBJECT_METHOD (rb_podcast_add_dialog_parent_class, constructed, object);
655 	dialog = RB_PODCAST_ADD_DIALOG (object);
656 
657 	g_object_get (dialog->priv->podcast_mgr, "db", &dialog->priv->db, NULL);
658 
659 	builder = rb_builder_load ("podcast-add-dialog.ui", NULL);
660 
661 	dialog->priv->info_bar_message = gtk_label_new ("");
662 	dialog->priv->info_bar = gtk_info_bar_new ();
663 	g_object_set (dialog->priv->info_bar, "spacing", 0, NULL);
664 	gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (GTK_INFO_BAR (dialog->priv->info_bar))),
665 			   dialog->priv->info_bar_message);
666 	gtk_widget_set_no_show_all (dialog->priv->info_bar, TRUE);
667 	gtk_box_pack_start (GTK_BOX (dialog), dialog->priv->info_bar, FALSE, FALSE, 0);
668 	gtk_widget_show (dialog->priv->info_bar_message);
669 
670 	dialog->priv->subscribe_button = GTK_WIDGET (gtk_builder_get_object (builder, "subscribe-button"));
671 	g_signal_connect_object (dialog->priv->subscribe_button, "clicked", G_CALLBACK (subscribe_clicked_cb), dialog, 0);
672 	gtk_widget_set_sensitive (dialog->priv->subscribe_button, FALSE);
673 
674 	dialog->priv->feed_view = GTK_WIDGET (gtk_builder_get_object (builder, "feed-view"));
675 	g_signal_connect (dialog->priv->feed_view, "row-activated", G_CALLBACK (feed_activated_cb), dialog);
676 	g_signal_connect (gtk_tree_view_get_selection (GTK_TREE_VIEW (dialog->priv->feed_view)),
677 			  "changed",
678 			  G_CALLBACK (feed_selection_changed_cb),
679 			  dialog);
680 
681 
682 	dialog->priv->search_entry = rb_search_entry_new (FALSE);
683 	gtk_widget_set_size_request (GTK_WIDGET (dialog->priv->search_entry), 400, -1);
684 	g_object_set (dialog->priv->search_entry,"explicit-mode", TRUE, NULL);
685 	g_signal_connect (dialog->priv->search_entry, "search", G_CALLBACK (search_cb), dialog);
686 	g_signal_connect (dialog->priv->search_entry, "activate", G_CALLBACK (search_cb), dialog);
687 	gtk_container_add (GTK_CONTAINER (gtk_builder_get_object (builder, "search-entry-box")),
688 			   GTK_WIDGET (dialog->priv->search_entry));
689 
690 	g_signal_connect (gtk_builder_get_object (builder, "close-button"),
691 			  "clicked",
692 			  G_CALLBACK (close_clicked_cb),
693 			  dialog);
694 
695 	dialog->priv->feed_model = gtk_list_store_new (7,
696 						       G_TYPE_STRING,	/* name */
697 						       G_TYPE_STRING,	/* author */
698 						       GDK_TYPE_PIXBUF, /* image */
699 						       G_TYPE_FILE,	/* image file */
700 						       G_TYPE_INT,	/* episode count */
701 						       G_TYPE_POINTER,	/* RBPodcastChannel */
702 						       G_TYPE_ULONG);	/* date */
703 	gtk_tree_view_set_model (GTK_TREE_VIEW (dialog->priv->feed_view), GTK_TREE_MODEL (dialog->priv->feed_model));
704 
705 	column = gtk_tree_view_column_new_with_attributes (_("Title"), gtk_cell_renderer_pixbuf_new (), "pixbuf", FEED_COLUMN_IMAGE, NULL);
706 	renderer = gtk_cell_renderer_text_new ();
707 	g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
708 	gtk_tree_view_column_pack_start (column, renderer, TRUE);
709 	gtk_tree_view_column_set_attributes (column, renderer, "text", FEED_COLUMN_TITLE, NULL);
710 
711 	gtk_tree_view_column_set_expand (column, TRUE);
712 	gtk_tree_view_append_column (GTK_TREE_VIEW (dialog->priv->feed_view), column);
713 
714 	renderer = gtk_cell_renderer_text_new ();
715 	g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
716 	column = gtk_tree_view_column_new_with_attributes (_("Author"), renderer, "text", FEED_COLUMN_AUTHOR, NULL);
717 	gtk_tree_view_column_set_expand (column, TRUE);
718 	gtk_tree_view_append_column (GTK_TREE_VIEW (dialog->priv->feed_view), column);
719 
720 	renderer = gtk_cell_renderer_text_new ();
721 	column = gtk_tree_view_column_new_with_attributes (_("Episodes"), renderer, NULL);
722 	gtk_tree_view_column_set_cell_data_func (column, renderer, episode_count_column_cell_data_func, NULL, NULL);
723 	episode_strings[0] = "0000";
724 	episode_strings[1] = _("Episodes");
725 	episode_strings[2] = NULL;
726 	rb_set_tree_view_column_fixed_width (dialog->priv->feed_view, column, renderer, episode_strings, 6);
727 	gtk_tree_view_append_column (GTK_TREE_VIEW (dialog->priv->feed_view), column);
728 
729 	widget = GTK_WIDGET (gtk_builder_get_object (builder, "podcast-add-dialog"));
730 	gtk_box_pack_start (GTK_BOX (dialog), widget, TRUE, TRUE, 12);
731 
732 	gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (dialog->priv->feed_view), TRUE);
733 
734 	/* set up episode view */
735 	g_object_get (dialog->priv->shell, "shell-player", &shell_player, NULL);
736 	episodes = rb_entry_view_new (dialog->priv->db, G_OBJECT (shell_player), TRUE, FALSE);
737 	g_object_unref (shell_player);
738 
739 	g_signal_connect (episodes, "entry-activated", G_CALLBACK (episode_entry_activated_cb), dialog);
740 
741 	/* date column */
742 	column = gtk_tree_view_column_new ();
743 	renderer = gtk_cell_renderer_text_new();
744 
745 	gtk_tree_view_column_pack_start (column, renderer, TRUE);
746 
747 	gtk_tree_view_column_set_clickable (column, TRUE);
748 	gtk_tree_view_column_set_resizable (column, TRUE);
749 	gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
750 	{
751 		const char *sample_strings[3];
752 		sample_strings[0] = _("Date");
753 		sample_strings[1] = rb_entry_view_get_time_date_column_sample ();
754 		sample_strings[2] = NULL;
755 		rb_entry_view_set_fixed_column_width (episodes, column, renderer, sample_strings);
756 	}
757 
758 	gtk_tree_view_column_set_cell_data_func (column, renderer,
759 						 (GtkTreeCellDataFunc) podcast_post_date_cell_data_func,
760 						 dialog, NULL);
761 
762 	rb_entry_view_append_column_custom (episodes, column,
763 					    _("Date"), "Date",
764 					    (GCompareDataFunc) podcast_post_date_sort_func,
765 					    0, NULL);
766 	rb_entry_view_append_column (episodes, RB_ENTRY_VIEW_COL_TITLE, TRUE);
767 	rb_entry_view_append_column (episodes, RB_ENTRY_VIEW_COL_DURATION, TRUE);
768 	rb_entry_view_set_sorting_order (RB_ENTRY_VIEW (episodes), "Date", GTK_SORT_DESCENDING);
769 	g_signal_connect (episodes,
770 			  "notify::sort-order",
771 			  G_CALLBACK (episodes_sort_changed_cb),
772 			  dialog);
773 
774 	query = rhythmdb_query_parse (dialog->priv->db,
775 				      RHYTHMDB_QUERY_PROP_EQUALS,
776 				      RHYTHMDB_PROP_TYPE,
777 				      RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH,
778 				      RHYTHMDB_QUERY_END);
779 	query_model = rhythmdb_query_model_new_empty (dialog->priv->db);
780 	rb_entry_view_set_model (episodes, query_model);
781 
782 	rhythmdb_do_full_query_async_parsed (dialog->priv->db, RHYTHMDB_QUERY_RESULTS (query_model), query);
783 	rhythmdb_query_free (query);
784 
785 	g_object_unref (query_model);
786 
787 	paned = GTK_WIDGET (gtk_builder_get_object (builder, "paned"));
788 	g_signal_connect (paned, "size-allocate", G_CALLBACK (paned_size_allocate_cb), dialog);
789 	gtk_paned_pack2 (GTK_PANED (paned),
790 			 GTK_WIDGET (episodes),
791 			 TRUE,
792 			 FALSE);
793 
794 	gtk_widget_show_all (GTK_WIDGET (dialog));
795 	g_object_unref (builder);
796 }
797 
798 static void
799 impl_dispose (GObject *object)
800 {
801 	RBPodcastAddDialog *dialog = RB_PODCAST_ADD_DIALOG (object);
802 
803 	if (dialog->priv->podcast_mgr != NULL) {
804 		g_object_unref (dialog->priv->podcast_mgr);
805 		dialog->priv->podcast_mgr = NULL;
806 	}
807 	if (dialog->priv->db != NULL) {
808 		g_object_unref (dialog->priv->db);
809 		dialog->priv->db = NULL;
810 	}
811 
812 	G_OBJECT_CLASS (rb_podcast_add_dialog_parent_class)->dispose (object);
813 }
814 
815 static void
816 impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
817 {
818 	RBPodcastAddDialog *dialog = RB_PODCAST_ADD_DIALOG (object);
819 
820 	switch (prop_id) {
821 	case PROP_PODCAST_MANAGER:
822 		dialog->priv->podcast_mgr = g_value_dup_object (value);
823 		break;
824 	case PROP_SHELL:
825 		dialog->priv->shell = g_value_dup_object (value);
826 		break;
827 	default:
828 		g_assert_not_reached ();
829 		break;
830 	}
831 }
832 
833 static void
834 impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
835 {
836 	RBPodcastAddDialog *dialog = RB_PODCAST_ADD_DIALOG (object);
837 
838 	switch (prop_id) {
839 	case PROP_PODCAST_MANAGER:
840 		g_value_set_object (value, dialog->priv->podcast_mgr);
841 		break;
842 	case PROP_SHELL:
843 		g_value_set_object (value, dialog->priv->shell);
844 		break;
845 	default:
846 		g_assert_not_reached ();
847 		break;
848 	}
849 }
850 
851 static void
852 rb_podcast_add_dialog_init (RBPodcastAddDialog *dialog)
853 {
854 	dialog->priv = G_TYPE_INSTANCE_GET_PRIVATE (dialog,
855 						    RB_TYPE_PODCAST_ADD_DIALOG,
856 						    RBPodcastAddDialogPrivate);
857 }
858 
859 static void
860 rb_podcast_add_dialog_class_init (RBPodcastAddDialogClass *klass)
861 {
862 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
863 
864 	object_class->constructed = impl_constructed;
865 	object_class->dispose = impl_dispose;
866 	object_class->set_property = impl_set_property;
867 	object_class->get_property = impl_get_property;
868 
869 	klass->close = impl_close;
870 
871 	g_object_class_install_property (object_class,
872 					 PROP_PODCAST_MANAGER,
873 					 g_param_spec_object ("podcast-manager",
874 							      "podcast-manager",
875 							      "RBPodcastManager instance",
876 							      RB_TYPE_PODCAST_MANAGER,
877 							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
878 	g_object_class_install_property (object_class,
879 					 PROP_SHELL,
880 					 g_param_spec_object ("shell",
881 							      "shell",
882 							      "RBShell instance",
883 							      RB_TYPE_SHELL,
884 							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
885 
886 	signals[CLOSE] = g_signal_new ("close",
887 				       RB_TYPE_PODCAST_ADD_DIALOG,
888 				       G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
889 				       G_STRUCT_OFFSET (RBPodcastAddDialogClass, close),
890 				       NULL, NULL,
891 				       g_cclosure_marshal_VOID__VOID,
892 				       G_TYPE_NONE,
893 				       0);
894 	signals[CLOSED] = g_signal_new ("closed",
895 					RB_TYPE_PODCAST_ADD_DIALOG,
896 					G_SIGNAL_RUN_LAST,
897 					G_STRUCT_OFFSET (RBPodcastAddDialogClass, closed),
898 					NULL, NULL,
899 					g_cclosure_marshal_VOID__VOID,
900 					G_TYPE_NONE,
901 					0);
902 
903 	g_type_class_add_private (object_class, sizeof (RBPodcastAddDialogPrivate));
904 
905 	gtk_binding_entry_add_signal (gtk_binding_set_by_class (klass),
906 				      GDK_KEY_Escape,
907 				      0,
908 				      "close",
909 				      0);
910 }
911 
912 void
913 rb_podcast_add_dialog_reset (RBPodcastAddDialog *dialog, const char *text, gboolean load)
914 {
915 	remove_all_feeds (dialog);
916 	rhythmdb_entry_delete_by_type (dialog->priv->db, RHYTHMDB_ENTRY_TYPE_PODCAST_SEARCH);
917 	rhythmdb_commit (dialog->priv->db);
918 
919 	rb_search_entry_set_text (dialog->priv->search_entry, text);
920 
921 	if (load) {
922 		search_cb (dialog->priv->search_entry, text, dialog);
923 	} else {
924 		rb_search_entry_grab_focus (dialog->priv->search_entry);
925 	}
926 }
927 
928 GtkWidget *
929 rb_podcast_add_dialog_new (RBShell *shell, RBPodcastManager *podcast_mgr)
930 {
931 	return GTK_WIDGET (g_object_new (RB_TYPE_PODCAST_ADD_DIALOG,
932 					 "shell", shell,
933 					 "podcast-manager", podcast_mgr,
934 					 "orientation", GTK_ORIENTATION_VERTICAL,
935 					 NULL));
936 }