[rhythmbox] podcast: add podcast search infrastructure



commit 539e378a51ed4af0fe0955b531af3e2d615d55ad
Author: Jonathan Matthew <jonathan d14n org>
Date:   Sat May 26 13:45:23 2012 +1000

    podcast: add podcast search infrastructure
    
    This searches iTunes and Miroguide for podcast feeds.  Not hooked
    up to any UI yet, but will be soon.
    
    This also makes json-glib a required dependency, which means the
    last.fm plugin can always be built.

 .gitignore                            |    1 +
 configure.ac                          |   33 +-----
 plugins/Makefile.am                   |    5 +-
 podcast/Makefile.am                   |   26 ++++-
 podcast/rb-podcast-manager.c          |   31 +++++
 podcast/rb-podcast-manager.h          |    2 +
 podcast/rb-podcast-parse.c            |   47 +++++++
 podcast/rb-podcast-parse.h            |    4 +
 podcast/rb-podcast-search-itunes.c    |  207 +++++++++++++++++++++++++++++++
 podcast/rb-podcast-search-miroguide.c |  219 +++++++++++++++++++++++++++++++++
 podcast/rb-podcast-search.c           |   96 ++++++++++++++
 podcast/rb-podcast-search.h           |   74 +++++++++++
 podcast/test-podcast-search.c         |  152 +++++++++++++++++++++++
 13 files changed, 859 insertions(+), 38 deletions(-)
---
diff --git a/.gitignore b/.gitignore
index 57d9c5c..73c80e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,6 +83,7 @@ po/.intltool-merge-cache
 
 #
 podcast/test-podcast-parse
+podcast/test-podcast-search
 
 #
 remote/dbus/rhythmbox-client
diff --git a/configure.ac b/configure.ac
index 3644d32..b3a4f31 100644
--- a/configure.ac
+++ b/configure.ac
@@ -101,7 +101,8 @@ PKG_CHECK_MODULES(RHYTHMBOX,				\
 		  libpeas-1.0 >= $LIBPEAS_REQS		\
 		  libpeas-gtk-1.0 >= $LIBPEAS_REQS	\
 		  libxml-2.0 >= $LIBXML2_REQS		\
-		  tdb >= 1.2.6)
+		  tdb >= 1.2.6				\
+		  json-glib-1.0)
 
 PKG_CHECK_MODULES(TOTEM_PLPARSER, totem-plparser >= $TOTEM_PLPARSER_REQS, have_totem_plparser=yes, have_totem_plparser=no)
 if test x$have_totem_plparser != xyes; then
@@ -768,30 +769,6 @@ AC_SUBST(CLUTTER_CFLAGS)
 AC_SUBST(CLUTTER_LIBS)
 
 dnl ================================================================
-dnl Dependencies for Last.fm plugin
-dnl ================================================================
-AC_ARG_ENABLE(lastfm,
-              AC_HELP_STRING([--disable-lastfm],
-                             [Disable Last.fm support]),,
-              enable_lastfm=auto)
-if test "x$enable_lastfm" != "xno"; then
-	PKG_CHECK_MODULES(JSON_GLIB, json-glib-1.0,
-			  have_json_glib=yes,
-			  have_json_glib=no)
-	if test "x$have_json_glib" = "xno" -a "x$enable_lastfm" = "xyes"; then
-		AC_MSG_ERROR([Last.fm support explicitly requested, but json-glib couldn't be found])
-	fi
-	if test x"$have_json_glib" = "xyes"; then
-		AC_DEFINE(HAVE_JSON_GLIB, 1, [Define if json-glib support is enabled])
-	fi
-fi
-
-AM_CONDITIONAL(ENABLE_LASTFM, test x"$have_json_glib" = "xyes")
-
-AC_SUBST(JSON_GLIB_CFLAGS)
-AC_SUBST(JSON_GLIB_LIBS)
-
-dnl ================================================================
 dnl grilo plugin
 dnl ================================================================
 AC_ARG_ENABLE(grilo,
@@ -999,10 +976,4 @@ else
 	AC_MSG_NOTICE([   Visualizer plugin disabled])
 fi
 
-if test "x$have_json_glib" = xyes; then
-	AC_MSG_NOTICE([** Last.fm support enabled])
-else
-	AC_MSG_NOTICE([   Last.fm support disabled])
-fi
-
 AC_MSG_NOTICE([End options])
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index dcc449e..bbea476 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -3,6 +3,7 @@ plugininclude_HEADERS = rb-plugin-macros.h
 
 SUBDIRS =						\
 	audiocd						\
+	audioscrobbler					\
 	dbus-media-server				\
 	generic-player					\
 	iradio						\
@@ -60,10 +61,6 @@ if USE_CLUTTER
 SUBDIRS += visualizer
 endif
 
-if ENABLE_LASTFM
-SUBDIRS += audioscrobbler
-endif
-
 if USE_NOTIFY
 SUBDIRS += notification
 endif
diff --git a/podcast/Makefile.am b/podcast/Makefile.am
index 5a9fe99..18cf01f 100644
--- a/podcast/Makefile.am
+++ b/podcast/Makefile.am
@@ -4,12 +4,16 @@ podcastincludedir = $(includedir)/rhythmbox/podcast
 podcastinclude_HEADERS =				\
 	rb-podcast-entry-types.h			\
 	rb-podcast-parse.h				\
-	rb-podcast-manager.h
+	rb-podcast-manager.h				\
+	rb-podcast-search.h
 
 
 librbpodcast_parse_la_SOURCES = 			\
 	rb-podcast-parse.c				\
-	rb-podcast-parse.h
+	rb-podcast-parse.h				\
+	rb-podcast-search.c				\
+	rb-podcast-search-itunes.c			\
+	rb-podcast-search-miroguide.c
 
 librbpodcast_la_SOURCES =				\
 	$(podcastinclude_HEADERS)			\
@@ -19,6 +23,9 @@ librbpodcast_la_SOURCES =				\
 	rb-podcast-properties-dialog.h			\
 	rb-podcast-main-source.c			\
 	rb-podcast-main-source.h			\
+	rb-podcast-search.c				\
+	rb-podcast-search-itunes.c			\
+	rb-podcast-search-miroguide.c			\
 	rb-podcast-settings.h				\
 	rb-podcast-source.c				\
 	rb-podcast-source.h				\
@@ -26,7 +33,18 @@ librbpodcast_la_SOURCES =				\
 	rb-podcast-manager.c				\
 	rb-podcast-entry-types.c
 
-noinst_PROGRAMS = test-podcast-parse
+noinst_PROGRAMS = test-podcast-parse test-podcast-search
+
+test_podcast_search_SOURCES =				\
+	test-podcast-search.c
+test_podcast_search_LDADD =				\
+	librbpodcast_parse.la				\
+	$(top_builddir)/lib/librb.la			\
+	$(RHYTHMBOX_LIBS)				\
+	$(JSON_GLIB_LIBS)				\
+	$(TOTEM_PLPARSER_LIBS)
+
+
 test_podcast_parse_SOURCES =				\
 	test-podcast-parse.c
 test_podcast_parse_LDADD =				\
@@ -48,9 +66,11 @@ AM_CFLAGS =						\
 	-I$(top_builddir)/lib 				\
 	$(RHYTHMBOX_CFLAGS)				\
 	$(WEBKIT_CFLAGS)				\
+	$(JSON_GLIB_CFLAGS)				\
 	$(TOTEM_PLPARSER_CFLAGS)
 
 librbpodcast_la_LDFLAGS = -export-dynamic
+librbpodcast_la_LIBADD = $(JSON_GLIB_LIBS)
 
 PLUGIN_FILES = rhythmbox-itms-plugin.c npapi.h npupp.h npruntime.h
 
diff --git a/podcast/rb-podcast-manager.c b/podcast/rb-podcast-manager.c
index 88df223..72b8b87 100644
--- a/podcast/rb-podcast-manager.c
+++ b/podcast/rb-podcast-manager.c
@@ -40,6 +40,7 @@
 #include "rb-podcast-settings.h"
 #include "rb-podcast-manager.h"
 #include "rb-podcast-entry-types.h"
+#include "rb-podcast-search.h"
 #include "rb-file-helpers.h"
 #include "rb-debug.h"
 #include "rb-marshal.h"
@@ -115,6 +116,7 @@ struct RBPodcastManagerPrivate
 	gboolean shutdown;
 	RBExtDB *art_store;
 
+	GList *searches;
 	GSettings *settings;
 	GFile *timestamp_file;
 };
@@ -273,6 +275,10 @@ rb_podcast_manager_constructed (GObject *object)
 
 	RB_CHAIN_GOBJECT_METHOD (rb_podcast_manager_parent_class, constructed, object);
 
+	/* add built in search types */
+	rb_podcast_manager_add_search (pd, rb_podcast_search_itunes_get_type ());
+	rb_podcast_manager_add_search (pd, rb_podcast_search_miroguide_get_type ());
+
 	pd->priv->settings = g_settings_new (PODCAST_SETTINGS_SCHEMA);
 	g_signal_connect_object (pd->priv->settings,
 				 "changed",
@@ -354,6 +360,8 @@ rb_podcast_manager_finalize (GObject *object)
 		g_list_free (pd->priv->download_list);
 	}
 
+	g_list_free (pd->priv->searches);
+
 	G_OBJECT_CLASS (rb_podcast_manager_parent_class)->finalize (object);
 }
 
@@ -2232,3 +2240,26 @@ rb_podcast_manager_get_podcast_dir (RBPodcastManager *pd)
 	return conf_dir_uri;
 }
 
+void
+rb_podcast_manager_add_search (RBPodcastManager *pd, GType search_type)
+{
+	pd->priv->searches = g_list_append (pd->priv->searches, GUINT_TO_POINTER (search_type));
+}
+
+GList *
+rb_podcast_manager_get_searches (RBPodcastManager *pd)
+{
+	GList *searches = NULL;
+	GList *i;
+
+	for (i = pd->priv->searches; i != NULL; i = i->next) {
+		RBPodcastSearch *search;
+		GType search_type;
+
+		search_type = GPOINTER_TO_UINT (i->data);
+		search = RB_PODCAST_SEARCH (g_object_new (search_type, NULL));
+		searches = g_list_append (searches, search);
+	}
+
+	return searches;
+}
diff --git a/podcast/rb-podcast-manager.h b/podcast/rb-podcast-manager.h
index cc2320f..5fcf8ef 100644
--- a/podcast/rb-podcast-manager.h
+++ b/podcast/rb-podcast-manager.h
@@ -93,6 +93,8 @@ RhythmDBEntry *         rb_podcast_manager_add_post  	  	(RhythmDB *db,
 gboolean		rb_podcast_manager_entry_downloaded	(RhythmDBEntry *entry);
 gboolean		rb_podcast_manager_entry_in_download_queue (RBPodcastManager *pd, RhythmDBEntry *entry);
 
+void			rb_podcast_manager_add_search		(RBPodcastManager *pd, GType search_type);
+GList *			rb_podcast_manager_get_searches		(RBPodcastManager *pd);
 
 G_END_DECLS
 
diff --git a/podcast/rb-podcast-parse.c b/podcast/rb-podcast-parse.c
index 77c7141..e81e513 100644
--- a/podcast/rb-podcast-parse.c
+++ b/podcast/rb-podcast-parse.c
@@ -242,6 +242,37 @@ rb_podcast_parse_load_feed (RBPodcastChannel *data,
 	return TRUE;
 }
 
+RBPodcastChannel *
+rb_podcast_parse_channel_copy (RBPodcastChannel *data)
+{
+	RBPodcastChannel *copy;
+	copy = g_new0 (RBPodcastChannel, 1);
+	copy->url = g_strdup (data->url);
+	copy->title = g_strdup (data->title);
+	copy->lang = g_strdup (data->lang);
+	copy->description = g_strdup (data->description);
+	copy->author = g_strdup (data->author);
+	copy->contact = g_strdup (data->contact);
+	copy->img = g_strdup (data->img);
+	copy->pub_date = data->pub_date;
+	copy->copyright = g_strdup (data->copyright);
+	copy->is_opml = data->is_opml;
+
+	if (data->posts != NULL) {
+		GList *l;
+		for (l = data->posts; l != NULL; l = l->next) {
+			RBPodcastItem *copyitem;
+			copyitem = rb_podcast_parse_item_copy (l->data);
+			data->posts = g_list_prepend (data->posts, copyitem);
+		}
+		data->posts = g_list_reverse (data->posts);
+	} else {
+		copy->num_posts = data->num_posts;
+	}
+
+	return copy;
+}
+
 void
 rb_podcast_parse_channel_free (RBPodcastChannel *data)
 {
@@ -264,6 +295,21 @@ rb_podcast_parse_channel_free (RBPodcastChannel *data)
 	data = NULL;
 }
 
+RBPodcastItem *
+rb_podcast_parse_item_copy (RBPodcastItem *item)
+{
+	RBPodcastItem *copy;
+	copy = g_new0 (RBPodcastItem, 1);
+	copy->title = g_strdup (item->title);
+	copy->url = g_strdup (item->url);
+	copy->description = g_strdup (item->description);
+	copy->author = g_strdup (item->author);
+	copy->pub_date = item->pub_date;
+	copy->duration = item->duration;
+	copy->filesize = item->filesize;
+	return copy;
+}
+
 void
 rb_podcast_parse_item_free (RBPodcastItem *item)
 {
@@ -272,6 +318,7 @@ rb_podcast_parse_item_free (RBPodcastItem *item)
 	g_free (item->title);
 	g_free (item->url);
 	g_free (item->description);
+	g_free (item->author);
 
 	g_free (item);
 }
diff --git a/podcast/rb-podcast-parse.h b/podcast/rb-podcast-parse.h
index 26eaafb..136dea4 100644
--- a/podcast/rb-podcast-parse.h
+++ b/podcast/rb-podcast-parse.h
@@ -67,12 +67,16 @@ typedef struct
     	gboolean is_opml;
 
 	GList *posts;
+	int num_posts;
 } RBPodcastChannel;
 
 gboolean rb_podcast_parse_load_feed	(RBPodcastChannel *data,
 					 const char *url,
 					 gboolean existing_feed,
 					 GError **error);
+
+RBPodcastChannel *rb_podcast_parse_channel_copy (RBPodcastChannel *data);
+RBPodcastItem *rb_podcast_parse_item_copy (RBPodcastItem *data);
 void rb_podcast_parse_channel_free 	(RBPodcastChannel *data);
 void rb_podcast_parse_item_free 	(RBPodcastItem *data);
 
diff --git a/podcast/rb-podcast-search-itunes.c b/podcast/rb-podcast-search-itunes.c
new file mode 100644
index 0000000..421a92d
--- /dev/null
+++ b/podcast/rb-podcast-search-itunes.c
@@ -0,0 +1,207 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2010 Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#include "config.h"
+
+#include "rb-podcast-search.h"
+#include "rb-debug.h"
+
+#include <libsoup/soup.h>
+#include <libsoup/soup-gnome.h>
+#include <json-glib/json-glib.h>
+
+#define RB_TYPE_PODCAST_SEARCH_ITUNES         (rb_podcast_search_itunes_get_type ())
+#define RB_PODCAST_SEARCH_ITUNES(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_PODCAST_SEARCH_ITUNES, RBPodcastSearchITunes))
+#define RB_PODCAST_SEARCH_ITUNES_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_PODCAST_SEARCH_ITUNES, RBPodcastSearchITunesClass))
+#define RB_IS_PODCAST_SEARCH_ITUNES(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_PODCAST_SEARCH_ITUNES))
+#define RB_IS_PODCAST_SEARCH_ITUNES_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_PODCAST_SEARCH_ITUNES))
+#define RB_PODCAST_SEARCH_ITUNES_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_PODCAST_SEARCH_ITUNES, RBPodcastSearchITunesClass))
+
+typedef struct _RBPodcastSearchITunes RBPodcastSearchITunes;
+typedef struct _RBPodcastSearchITunesClass RBPodcastSearchITunesClass;
+
+struct _RBPodcastSearchITunes
+{
+	RBPodcastSearch parent;
+
+	SoupSession *session;
+};
+
+struct _RBPodcastSearchITunesClass
+{
+	RBPodcastSearchClass parent;
+};
+
+static void rb_podcast_search_itunes_class_init (RBPodcastSearchITunesClass *klass);
+static void rb_podcast_search_itunes_init (RBPodcastSearchITunes *search);
+
+G_DEFINE_TYPE (RBPodcastSearchITunes, rb_podcast_search_itunes, RB_TYPE_PODCAST_SEARCH);
+
+#define ITUNES_SEARCH_URI	"http://itunes.apple.com/WebObjects/MZStoreServices.woa/ws/wsSearch";
+
+static void
+process_results (RBPodcastSearchITunes *search, JsonParser *parser)
+{
+	JsonObject *container;
+	JsonArray *results;
+	guint i;
+
+	container = json_node_get_object (json_parser_get_root (parser));
+	results = json_node_get_array (json_object_get_member (container, "results"));
+
+	for (i = 0; i < json_array_get_length (results); i++) {
+		JsonObject *feed;
+		RBPodcastChannel *channel;
+
+		feed = json_array_get_object_element (results, i);
+
+		/* check wrapperType==track, kind==podcast ? */
+
+		channel = g_new0 (RBPodcastChannel, 1);
+
+		channel->url = g_strdup (json_object_get_string_member (feed, "collectionViewUrl"));
+		channel->title = g_strdup (json_object_get_string_member (feed, "collectionName"));
+		channel->author = g_strdup (json_object_get_string_member (feed, "artistName"));
+		channel->img = g_strdup (json_object_get_string_member (feed, "artworkUrl100"));	/* 100? */
+		channel->is_opml = FALSE;
+
+		channel->num_posts = json_object_get_int_member (feed, "trackCount");
+
+		rb_debug ("got result %s (%s)", channel->title, channel->url);
+		rb_podcast_search_result (RB_PODCAST_SEARCH (search), channel);
+		rb_podcast_parse_channel_free (channel);
+	}
+}
+
+static void
+search_response_cb (SoupSession *session, SoupMessage *msg, RBPodcastSearchITunes *search)
+{
+	JsonParser *parser;
+	GError *error = NULL;
+	int code;
+
+	g_object_get (msg, SOUP_MESSAGE_STATUS_CODE, &code, NULL);
+	if (code != 200) {
+		char *reason;
+
+		g_object_get (msg, SOUP_MESSAGE_REASON_PHRASE, &reason, NULL);
+		rb_debug ("search request failed: %s", reason);
+		g_free (reason);
+		rb_podcast_search_finished (RB_PODCAST_SEARCH (search), FALSE);
+		return;
+	}
+
+	if (msg->response_body->data == NULL) {
+		rb_debug ("no response data");
+		rb_podcast_search_finished (RB_PODCAST_SEARCH (search), TRUE);
+		return;
+	}
+
+	parser = json_parser_new ();
+	if (json_parser_load_from_data (parser, msg->response_body->data, msg->response_body->length, &error)) {
+		process_results (search, parser);
+	} else {
+		rb_debug ("unable to parse response data: %s", error->message);
+		g_clear_error (&error);
+	}
+
+	g_object_unref (parser);
+	rb_podcast_search_finished (RB_PODCAST_SEARCH (search), TRUE);
+}
+
+static void
+impl_start (RBPodcastSearch *bsearch, const char *text, int max_results)
+{
+	SoupURI *uri;
+	SoupMessage *message;
+	char *limit;
+	RBPodcastSearchITunes *search = RB_PODCAST_SEARCH_ITUNES (bsearch);
+
+	search->session = soup_session_async_new_with_options (SOUP_SESSION_ADD_FEATURE_BY_TYPE,
+							       SOUP_TYPE_GNOME_FEATURES_2_26,
+							       NULL);
+
+	uri = soup_uri_new (ITUNES_SEARCH_URI);
+	limit = g_strdup_printf ("%d", max_results);
+	soup_uri_set_query_from_fields (uri,
+					"term", text,
+					"media", "podcast",
+					"entity", "podcast",
+					"limit", limit,
+					"version", "2",
+					"output", "json",
+					NULL);
+	g_free (limit);
+
+	message = soup_message_new_from_uri (SOUP_METHOD_GET, uri);
+	soup_uri_free (uri);
+
+	soup_session_queue_message (search->session, message, (SoupSessionCallback) search_response_cb, search);
+}
+
+static void
+impl_cancel (RBPodcastSearch *bsearch)
+{
+	RBPodcastSearchITunes *search = RB_PODCAST_SEARCH_ITUNES (bsearch);
+
+	if (search->session != NULL) {
+		soup_session_abort (search->session);
+	}
+}
+
+static void
+impl_dispose (GObject *object)
+{
+	RBPodcastSearchITunes *search = RB_PODCAST_SEARCH_ITUNES (object);
+
+	if (search->session != NULL) {
+		soup_session_abort (search->session);
+		g_object_unref (search->session);
+		search->session = NULL;
+	}
+
+	G_OBJECT_CLASS (rb_podcast_search_itunes_parent_class)->dispose (object);
+}
+
+static void
+rb_podcast_search_itunes_init (RBPodcastSearchITunes *search)
+{
+	/* do nothing? */
+}
+
+static void
+rb_podcast_search_itunes_class_init (RBPodcastSearchITunesClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+	RBPodcastSearchClass *search_class = RB_PODCAST_SEARCH_CLASS (klass);
+
+	object_class->dispose = impl_dispose;
+
+	search_class->cancel = impl_cancel;
+	search_class->start = impl_start;
+}
diff --git a/podcast/rb-podcast-search-miroguide.c b/podcast/rb-podcast-search-miroguide.c
new file mode 100644
index 0000000..d7fd519
--- /dev/null
+++ b/podcast/rb-podcast-search-miroguide.c
@@ -0,0 +1,219 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2010 Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#include "config.h"
+
+#include "rb-podcast-search.h"
+#include "rb-debug.h"
+
+#include <libsoup/soup.h>
+#include <libsoup/soup-gnome.h>
+#include <json-glib/json-glib.h>
+#include <totem-pl-parser.h>
+
+#define RB_TYPE_PODCAST_SEARCH_MIROGUIDE         (rb_podcast_search_miroguide_get_type ())
+#define RB_PODCAST_SEARCH_MIROGUIDE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_PODCAST_SEARCH_MIROGUIDE, RBPodcastSearchMiroGuide))
+#define RB_PODCAST_SEARCH_MIROGUIDE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_PODCAST_SEARCH_MIROGUIDE, RBPodcastSearchMiroGuideClass))
+#define RB_IS_PODCAST_SEARCH_MIROGUIDE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_PODCAST_SEARCH_MIROGUIDE))
+#define RB_IS_PODCAST_SEARCH_MIROGUIDE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_PODCAST_SEARCH_MIROGUIDE))
+#define RB_PODCAST_SEARCH_MIROGUIDE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_PODCAST_SEARCH_MIROGUIDE, RBPodcastSearchMiroGuideClass))
+
+typedef struct _RBPodcastSearchMiroGuide RBPodcastSearchMiroGuide;
+typedef struct _RBPodcastSearchMiroGuideClass RBPodcastSearchMiroGuideClass;
+
+struct _RBPodcastSearchMiroGuide
+{
+	RBPodcastSearch parent;
+
+	SoupSession *session;
+};
+
+struct _RBPodcastSearchMiroGuideClass
+{
+	RBPodcastSearchClass parent;
+};
+
+static void rb_podcast_search_miroguide_class_init (RBPodcastSearchMiroGuideClass *klass);
+static void rb_podcast_search_miroguide_init (RBPodcastSearchMiroGuide *search);
+
+G_DEFINE_TYPE (RBPodcastSearchMiroGuide, rb_podcast_search_miroguide, RB_TYPE_PODCAST_SEARCH);
+
+#define MIROGUIDE_SEARCH_URI	"http://www.miroguide.com/api/get_feeds";
+
+static void
+process_results (RBPodcastSearchMiroGuide *search, JsonParser *parser)
+{
+	JsonArray *results;
+	guint i;
+
+	results = json_node_get_array (json_parser_get_root (parser));
+
+	for (i = 0; i < json_array_get_length (results); i++) {
+		JsonObject *feed;
+		JsonArray *items;
+		RBPodcastChannel *channel;
+		int j;
+
+		feed = json_array_get_object_element (results, i);
+
+		channel = g_new0 (RBPodcastChannel, 1);
+		channel->url = g_strdup (json_object_get_string_member (feed, "url"));
+		channel->title = g_strdup (json_object_get_string_member (feed, "name"));
+		channel->author = g_strdup (json_object_get_string_member (feed, "publisher"));		/* hrm */
+		channel->img = g_strdup (json_object_get_string_member (feed, "thumbnail_url"));
+		channel->is_opml = FALSE;
+		rb_debug ("feed %d: url %s, name \"%s\"", i, channel->url, channel->title);
+
+		items = json_object_get_array_member (feed, "item");
+		for (j = 0; j < json_array_get_length (items); j++) {
+			JsonObject *episode = json_array_get_object_element (items, j);
+			RBPodcastItem *item;
+
+			item = g_new0 (RBPodcastItem, 1);
+			item->title = g_strdup (json_object_get_string_member (episode, "name"));
+			item->url = g_strdup (json_object_get_string_member (episode, "url"));
+			item->description = g_strdup (json_object_get_string_member (episode, "description"));
+			item->pub_date = totem_pl_parser_parse_date (json_object_get_string_member (episode, "date"), FALSE);
+			item->filesize = json_object_get_int_member (episode, "size");
+			rb_debug ("item %d: title \"%s\", url %s", j, item->title, item->url);
+
+			channel->posts = g_list_prepend (channel->posts, item);
+		}
+		channel->posts = g_list_reverse (channel->posts);
+		rb_debug ("finished parsing items");
+
+		rb_podcast_search_result (RB_PODCAST_SEARCH (search), channel);
+		rb_podcast_parse_channel_free (channel);
+	}
+}
+
+static void
+search_response_cb (SoupSession *session, SoupMessage *msg, RBPodcastSearchMiroGuide *search)
+{
+	JsonParser *parser;
+	int code;
+
+	g_object_get (msg, SOUP_MESSAGE_STATUS_CODE, &code, NULL);
+	if (code != 200) {
+		char *reason;
+
+		g_object_get (msg, SOUP_MESSAGE_REASON_PHRASE, &reason, NULL);
+		rb_debug ("search request failed: %s", reason);
+		g_free (reason);
+		rb_podcast_search_finished (RB_PODCAST_SEARCH (search), FALSE);
+		return;
+	}
+
+	if (msg->response_body->data == NULL) {
+		rb_debug ("no response data");
+		rb_podcast_search_finished (RB_PODCAST_SEARCH (search), TRUE);
+		return;
+	}
+
+	parser = json_parser_new ();
+	if (json_parser_load_from_data (parser, msg->response_body->data, msg->response_body->length, NULL)) {
+		process_results (search, parser);
+	} else {
+		rb_debug ("unable to parse response data");
+	}
+
+	g_object_unref (parser);
+	rb_podcast_search_finished (RB_PODCAST_SEARCH (search), TRUE);
+}
+
+static void
+impl_start (RBPodcastSearch *bsearch, const char *text, int max_results)
+{
+	SoupURI *uri;
+	SoupMessage *message;
+	char *limit;
+	RBPodcastSearchMiroGuide *search = RB_PODCAST_SEARCH_MIROGUIDE (bsearch);
+
+	search->session = soup_session_async_new_with_options (SOUP_SESSION_ADD_FEATURE_BY_TYPE,
+							       SOUP_TYPE_GNOME_FEATURES_2_26,
+							       NULL);
+
+	uri = soup_uri_new (MIROGUIDE_SEARCH_URI);
+	limit = g_strdup_printf ("%d", max_results);
+	soup_uri_set_query_from_fields (uri,
+					"filter", "audio",
+					"filter_value", "1",
+					"filter", "name",
+					"filter_value", text,
+					"sort", "popular",	/* hmm */
+					"limit", limit,
+					"datatype", "json",
+					NULL);
+	g_free (limit);
+
+	message = soup_message_new_from_uri (SOUP_METHOD_GET, uri);
+	soup_uri_free (uri);
+
+	soup_session_queue_message (search->session, message, (SoupSessionCallback) search_response_cb, search);
+}
+
+static void
+impl_cancel (RBPodcastSearch *bsearch)
+{
+	RBPodcastSearchMiroGuide *search = RB_PODCAST_SEARCH_MIROGUIDE (bsearch);
+	if (search->session != NULL) {
+		soup_session_abort (search->session);
+	}
+}
+
+static void
+impl_dispose (GObject *object)
+{
+	RBPodcastSearchMiroGuide *search = RB_PODCAST_SEARCH_MIROGUIDE (object);
+
+	if (search->session != NULL) {
+		soup_session_abort (search->session);
+		g_object_unref (search->session);
+		search->session = NULL;
+	}
+
+	G_OBJECT_CLASS (rb_podcast_search_miroguide_parent_class)->dispose (object);
+}
+
+static void
+rb_podcast_search_miroguide_init (RBPodcastSearchMiroGuide *search)
+{
+	/* do nothing? */
+}
+
+static void
+rb_podcast_search_miroguide_class_init (RBPodcastSearchMiroGuideClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+	RBPodcastSearchClass *search_class = RB_PODCAST_SEARCH_CLASS (klass);
+
+	object_class->dispose = impl_dispose;
+
+	search_class->start = impl_start;
+	search_class->cancel = impl_cancel;
+}
diff --git a/podcast/rb-podcast-search.c b/podcast/rb-podcast-search.c
new file mode 100644
index 0000000..0a963f1
--- /dev/null
+++ b/podcast/rb-podcast-search.c
@@ -0,0 +1,96 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2010 Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#include "config.h"
+
+#include "rb-podcast-search.h"
+
+static void rb_podcast_search_class_init (RBPodcastSearchClass *klass);
+static void rb_podcast_search_init (RBPodcastSearch *search);
+
+enum {
+	RESULT,
+	FINISHED,
+	LAST_SIGNAL
+};
+
+G_DEFINE_TYPE (RBPodcastSearch, rb_podcast_search, G_TYPE_OBJECT);
+
+static guint signals[LAST_SIGNAL];
+
+void
+rb_podcast_search_start (RBPodcastSearch *search, const char *text, int max_results)
+{
+	RBPodcastSearchClass *klass = RB_PODCAST_SEARCH_GET_CLASS (search);
+	klass->start (search, text, max_results);
+}
+
+void
+rb_podcast_search_cancel (RBPodcastSearch *search)
+{
+	RBPodcastSearchClass *klass = RB_PODCAST_SEARCH_GET_CLASS (search);
+	klass->cancel (search);
+}
+
+void
+rb_podcast_search_result (RBPodcastSearch *search, RBPodcastChannel *data)
+{
+	g_signal_emit (search, signals[RESULT], 0, data);
+}
+
+void
+rb_podcast_search_finished (RBPodcastSearch *search, gboolean successful)
+{
+	g_signal_emit (search, signals[FINISHED], 0, successful);
+}
+
+static void
+rb_podcast_search_init (RBPodcastSearch *search)
+{
+}
+
+static void
+rb_podcast_search_class_init (RBPodcastSearchClass *klass)
+{
+	signals[RESULT] = g_signal_new ("result",
+					RB_TYPE_PODCAST_SEARCH,
+					G_SIGNAL_RUN_LAST,
+					0,
+					NULL, NULL,
+					g_cclosure_marshal_VOID__POINTER,
+					G_TYPE_NONE,
+					1, G_TYPE_POINTER);
+	signals[FINISHED] = g_signal_new ("finished",
+					  RB_TYPE_PODCAST_SEARCH,
+					  G_SIGNAL_RUN_LAST,
+					  0,
+					  NULL, NULL,
+					  g_cclosure_marshal_VOID__BOOLEAN,
+					  G_TYPE_NONE,
+					  1, G_TYPE_BOOLEAN);
+}
diff --git a/podcast/rb-podcast-search.h b/podcast/rb-podcast-search.h
new file mode 100644
index 0000000..91e09e2
--- /dev/null
+++ b/podcast/rb-podcast-search.h
@@ -0,0 +1,74 @@
+/*
+ *  Copyright (C) 2010 Jonathan Matthew  <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#ifndef RB_PODCAST_SEARCH_H
+#define RB_PODCAST_SEARCH_H
+
+#include <glib-object.h>
+
+#include "rb-podcast-parse.h"
+
+G_BEGIN_DECLS
+
+#define RB_TYPE_PODCAST_SEARCH         (rb_podcast_search_get_type ())
+#define RB_PODCAST_SEARCH(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_PODCAST_SEARCH, RBPodcastSearch))
+#define RB_PODCAST_SEARCH_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_PODCAST_SEARCH, RBPodcastSearchClass))
+#define RB_IS_PODCAST_SEARCH(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_PODCAST_SEARCH))
+#define RB_IS_PODCAST_SEARCH_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_PODCAST_SEARCH))
+#define RB_PODCAST_SEARCH_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_PODCAST_SEARCH, RBPodcastSearchClass))
+
+typedef struct _RBPodcastSearch RBPodcastSearch;
+typedef struct _RBPodcastSearchClass RBPodcastSearchClass;
+
+struct _RBPodcastSearch
+{
+	GObject parent;
+};
+
+struct _RBPodcastSearchClass
+{
+	GObjectClass parent;
+
+	/* methods */
+	void	(*start)	(RBPodcastSearch *search, const char *text, int max_results);
+	void	(*cancel)	(RBPodcastSearch *search);
+};
+
+GType		rb_podcast_search_get_type		(void);
+
+void		rb_podcast_search_start			(RBPodcastSearch *search, const char *text, int max_results);
+void		rb_podcast_search_cancel		(RBPodcastSearch *search);
+
+void		rb_podcast_search_result		(RBPodcastSearch *search, RBPodcastChannel *data);
+void		rb_podcast_search_finished		(RBPodcastSearch *search, gboolean successful);
+
+/* built in search types */
+
+GType		rb_podcast_search_itunes_get_type	(void);
+GType		rb_podcast_search_miroguide_get_type	(void);
+
+#endif /* RB_PODCAST_SEARCH_H */
diff --git a/podcast/test-podcast-search.c b/podcast/test-podcast-search.c
new file mode 100644
index 0000000..bd0ca43
--- /dev/null
+++ b/podcast/test-podcast-search.c
@@ -0,0 +1,152 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+
+#include "config.h"
+
+#include <locale.h>
+#include <glib.h>
+#include <glib-object.h>
+#include <glib/gi18n.h>
+
+#include "rb-podcast-search.h"
+
+#include <string.h>
+
+static gboolean debug = FALSE;
+static int done = 0;
+
+void rb_debug_realf (const char *func,
+		     const char *file,
+		     int line,
+		     gboolean newline,
+		     const char *format, ...) G_GNUC_PRINTF (5, 6);
+
+/* For the benefit of the podcast parsing code */
+void
+rb_debug_realf (const char *func,
+	        const char *file,
+	        int line,
+	        gboolean newline,
+	        const char *format, ...)
+{
+	va_list args;
+	char buffer[1025];
+
+	if (debug == FALSE)
+		return;
+
+	va_start (args, format);
+	g_vsnprintf (buffer, 1024, format, args);
+	va_end (args);
+
+	g_printerr (newline ? "%s:%d [%s] %s\n" : "%s:%d [%s] %s",
+		    file, line, func, buffer);
+}
+
+static void
+result_cb (RBPodcastSearch *search, RBPodcastChannel *data)
+{
+	char datebuf[1025];
+	GDate date;
+
+	g_date_set_time_t (&date, data->pub_date);
+	g_date_strftime (datebuf, 1024, "%F %T", &date);
+
+	g_print ("Result from %s\n", G_OBJECT_TYPE_NAME (search));
+
+	g_print ("Podcast title: %s\n", data->title);
+	g_print ("Description: %s\n", data->description);
+	g_print ("Author: %s\n", data->author);
+	g_print ("Date: %s\n", datebuf);
+
+	if (data->num_posts > 0) {
+		g_print ("Number of episodes: %d\n", data->num_posts);
+		g_print ("\n");
+	} else {
+		GList *l;
+		g_print ("Number of episodes: %d\n", g_list_length (data->posts));
+		g_print ("\n");
+		for (l = data->posts; l != NULL; l = l->next) {
+			RBPodcastItem *item = l->data;
+
+			g_date_set_time_t (&date, item->pub_date);
+			g_date_strftime (datebuf, 1024, "%F %T", &date);
+
+			g_print ("\tItem title: %s\n", item->title);
+			g_print ("\tURL: %s\n", item->url);
+			g_print ("\tAuthor: %s\n", item->author);
+			g_print ("\tDate: %s\n", datebuf);
+			g_print ("\tDescription: %s\n", item->description);
+			g_print ("\n");
+		}
+	}
+}
+
+static void
+finished_cb (RBPodcastSearch *search, GMainLoop *loop)
+{
+	g_print ("Search %s finished\n", G_OBJECT_TYPE_NAME (search));
+	done++;
+	if (done == 2) {
+		g_main_loop_quit (loop);
+	}
+}
+
+int main (int argc, char **argv)
+{
+	GMainLoop *loop;
+	RBPodcastSearch *itunes;
+	RBPodcastSearch *miroguide;
+	char *text;
+
+	g_type_init ();
+	setlocale (LC_ALL, "");
+	bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
+	bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+	text = argv[1];
+	if (argv[2] != NULL && strcmp (argv[2], "--debug") == 0) {
+		debug = TRUE;
+	}
+
+	loop = g_main_loop_new (NULL, FALSE);
+
+	itunes = RB_PODCAST_SEARCH (g_object_new (rb_podcast_search_itunes_get_type (), NULL));
+	miroguide = RB_PODCAST_SEARCH (g_object_new (rb_podcast_search_miroguide_get_type (), NULL));
+
+	g_signal_connect (itunes, "result", G_CALLBACK (result_cb), NULL);
+	g_signal_connect (miroguide, "result", G_CALLBACK (result_cb), NULL);
+	g_signal_connect (itunes, "finished", G_CALLBACK (finished_cb), loop);
+	g_signal_connect (miroguide, "finished", G_CALLBACK (finished_cb), loop);
+
+	rb_podcast_search_start (itunes, text, 10);
+	rb_podcast_search_start (miroguide, text, 10);
+
+	g_main_loop_run (loop);
+
+	return 0;
+}



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]