1 /*
2  * rb-audioscrobbler-radio-source.c
3  *
4  * Copyright (C) 2010 Jamie Nicol <jamie@thenicols.net>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2, or (at your option)
9  * any later version.
10  *
11  * The Rhythmbox authors hereby grant permission for non-GPL compatible
12  * GStreamer plugins to be used and distributed together with GStreamer
13  * and Rhythmbox. This permission is above and beyond the permissions granted
14  * by the GPL license by which Rhythmbox is covered. If you modify this code
15  * you may extend this exception to your version of the code, but you are not
16  * obligated to do so. If you do not wish to do so, delete this exception
17  * statement from your version.
18  *
19  * This program is distributed in the hope that it will be useful,
20  * but WITHOUT ANY WARRANTY; without even the implied warranty of
21  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22  * GNU General Public License for more details.
23  *
24  * You should have received a copy of the GNU General Public License
25  * along with this program; if not, write to the Free Software
26  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
27  */
28 
29 #include "config.h"
30 #include <string.h>
31 #include <unistd.h>
32 #include <libsoup/soup.h>
33 #include <json-glib/json-glib.h>
34 #include <glib/gi18n.h>
35 #include <glib/gstdio.h>
36 
37 #include <totem-pl-parser.h>
38 
39 #include "rb-audioscrobbler-radio-source.h"
40 #include "rb-audioscrobbler-radio-track-entry-type.h"
41 #include "rb-audioscrobbler-play-order.h"
42 #include "rb-debug.h"
43 #include "rb-display-page-tree.h"
44 #include "rb-util.h"
45 #include "rb-file-helpers.h"
46 #include "rb-source-toolbar.h"
47 #include "rb-ext-db.h"
48 
49 
50 /* radio type stuff */
51 static const char* radio_types[] = {
52 	/* Translators: describes a radio stream playing tracks similar to those by an artist.
53 	 * Followed by a text entry box for the artist name.
54 	 */
55 	N_("Similar to Artist:"),
56 	/* Translators: describes a radio stream playing tracks listened to by the top fans of
57 	 * a particular artist.  Followed by a text entry box for the artist name.
58 	 */
59 	N_("Top Fans of Artist:"),
60 	/* Translators: describes a radio stream playing tracks from the library of a particular
61 	 * user.  Followed by a text entry box for the user name.
62 	 */
63 	N_("Library of User:"),
64 	/* Translators: describes a radio stream playing tracks played by users similar to a
65 	 * particular user.  Followed by a text entry box for the user name.
66 	 */
67 	N_("Neighbourhood of User:"),
68 	/* Translators: describes a radio stream playing tracks that a particular user has marked
69 	 * as loved.  Followed by a text entry box for the user name.
70 	 */
71 	N_("Tracks Loved by User:"),
72 	/* Translators: describes a radio stream playing tracks recommended to a particular user.
73 	 * Followed by a text entry box for the user name.
74 	 */
75 	N_("Recommendations for User:"),
76 	/* Translators: a type of station named "Mix Radio" by Last.fm.
77 	 * See http://blog.last.fm/2010/10/29/mix-radio-a-new-radio-station for a description of it.
78 	 * Followed by a text entry box for the user name.
79 	 */
80 	N_("Mix Radio for User:"),
81 	/* Translators: describes a radio stream playing tracks tagged with a particular tag.
82 	 * Followed by a text entry box for the tag.
83 	 */
84 	N_("Tracks Tagged with:"),
85 	/* Translators: describes a radio stream playing tracks often listened to by members of
86 	 * a particular group. Followed by a text entry box for the group name.
87 	 */
88 	N_("Listened by Group:"),
89 	NULL
90 };
91 
92 const char *
rb_audioscrobbler_radio_type_get_text(RBAudioscrobblerRadioType type)93 rb_audioscrobbler_radio_type_get_text (RBAudioscrobblerRadioType type)
94 {
95 	return _(radio_types[type]);
96 }
97 
98 static const char* radio_urls[] = {
99 	"lastfm://artist/%s/similarartists",
100 	"lastfm://artist/%s/fans",
101 	"lastfm://user/%s/library",
102 	"lastfm://user/%s/neighbours",
103 	"lastfm://user/%s/loved",
104 	"lastfm://user/%s/recommended",
105 	"lastfm://user/%s/mix",
106 	"lastfm://globaltags/%s",
107 	"lastfm://group/%s",
108 	NULL
109 };
110 
111 const char *
rb_audioscrobbler_radio_type_get_url(RBAudioscrobblerRadioType type)112 rb_audioscrobbler_radio_type_get_url (RBAudioscrobblerRadioType type)
113 {
114 	return radio_urls[type];
115 }
116 
117 static const char* radio_names[] = {
118 
119 	/* Translators: I have chosen these names for the radio stations based upon
120 	 * what last.fm's website uses or what I thought to be sensible.
121 	 */
122 	/* Translators: station is built from artists similar to the artist %s */
123 	N_("%s Radio"),
124 	/* Translators: station is built from the artist %s's top fans */
125 	N_("%s Fan Radio"),
126 	/* Translators: station is built from the library of the user %s */
127 	N_("%s's Library"),
128 	/* Translators: station is built from the "neighbourhood" of the user %s.
129 	 * Last.fm uses "neighbourhood" to mean other users with similar music tastes */
130 	N_("%s's Neighbourhood"),
131 	/* Translators: station is built from the tracks which have been "loved" by the user %s */
132 	N_("%s's Loved Tracks"),
133 	/* Translators: station is built from the tracks which are recommended to the user %s */
134 	N_("%s's Recommended Radio"),
135 	/* Translators: station is the "Mix Radio" for the user %s.
136 	 * See http://blog.last.fm/2010/10/29/mix-radio-a-new-radio-station for description. */
137 	N_("%s's Mix Radio"),
138 	/* Translators: station is built from the tracks which have been "tagged" with %s.
139 	 * Last.fm lets users "tag" songs with any string they wish. Tags are usually genres,
140 	 * but nationalities, record labels, decades and very random words are also common */
141 	N_("%s Tag Radio"),
142 	/* Translators: station is built from the library of the group %s */
143 	N_("%s Group Radio"),
144 	NULL
145 };
146 
147 const char *
rb_audioscrobbler_radio_type_get_default_name(RBAudioscrobblerRadioType type)148 rb_audioscrobbler_radio_type_get_default_name (RBAudioscrobblerRadioType type)
149 {
150 	return _(radio_names[type]);
151 }
152 
153 /* source declarations */
154 struct _RBAudioscrobblerRadioSourcePrivate
155 {
156 	RBAudioscrobblerProfilePage *parent;
157 
158 	RBAudioscrobblerService *service;
159 	char *username;
160 	char *session_key;
161 	char *station_url;
162 
163 	SoupSession *soup_session;
164 
165 	GtkWidget *error_info_bar;
166 	GtkWidget *error_info_bar_label;
167 
168 	RBEntryView *track_view;
169 	RhythmDBQueryModel *track_model;
170 
171 	gboolean is_busy;
172 
173 	RBPlayOrder *play_order;
174 
175 	/* the currently playing entry from this source, if there is one */
176 	RhythmDBEntry *playing_entry;
177 
178 	RBExtDB *art_store;
179 };
180 
181 #define RB_AUDIOSCROBBLER_RADIO_SOURCE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_AUDIOSCROBBLER_RADIO_SOURCE, RBAudioscrobblerRadioSourcePrivate))
182 
183 static void rb_audioscrobbler_radio_source_constructed (GObject *object);
184 static void rb_audioscrobbler_radio_source_dispose (GObject *object);
185 static void rb_audioscrobbler_radio_source_finalize (GObject *object);
186 static void rb_audioscrobbler_radio_source_get_property (GObject *object,
187                                                          guint prop_id,
188                                                          GValue *value,
189                                                          GParamSpec *pspec);
190 static void rb_audioscrobbler_radio_source_set_property (GObject *object,
191                                                          guint prop_id,
192                                                          const GValue *value,
193                                                          GParamSpec *pspec);
194 
195 static void playing_song_changed_cb (RBShellPlayer *player,
196                                      RhythmDBEntry *entry,
197                                      RBAudioscrobblerRadioSource *source);
198 
199 /* last.fm api requests */
200 static void tune (RBAudioscrobblerRadioSource *source);
201 static void tune_response_cb (SoupSession *session,
202                               SoupMessage *msg,
203                               gpointer user_data);
204 static void fetch_playlist (RBAudioscrobblerRadioSource *source);
205 static void fetch_playlist_response_cb (SoupSession *session,
206                                         SoupMessage *msg,
207                                         gpointer user_data);
208 static void xspf_entry_parsed (TotemPlParser *parser,
209                                const char *uri,
210                                GHashTable *metadata,
211                                RBAudioscrobblerRadioSource *source);
212 
213 /* info bar related things */
214 static void display_error_info_bar (RBAudioscrobblerRadioSource *source,
215                                     const char *message);
216 
217 /* RBDisplayPage implementations */
218 static void impl_selected (RBDisplayPage *page);
219 static void impl_delete_thyself (RBDisplayPage *page);
220 static gboolean impl_can_remove (RBDisplayPage *page);
221 static void impl_remove (RBDisplayPage *page);
222 
223 /* RBSource implementations */
224 static RBEntryView *impl_get_entry_view (RBSource *asource);
225 static RBSourceEOFType impl_handle_eos (RBSource *asource);
226 static void impl_get_playback_status (RBSource *source, char **text, float *progress);
227 
228 enum {
229 	PROP_0,
230 	PROP_PARENT,
231 	PROP_SERVICE,
232 	PROP_USERNAME,
233 	PROP_SESSION_KEY,
234 	PROP_STATION_URL,
235 	PROP_PLAY_ORDER
236 };
237 
G_DEFINE_DYNAMIC_TYPE(RBAudioscrobblerRadioSource,rb_audioscrobbler_radio_source,RB_TYPE_STREAMING_SOURCE)238 G_DEFINE_DYNAMIC_TYPE (RBAudioscrobblerRadioSource, rb_audioscrobbler_radio_source, RB_TYPE_STREAMING_SOURCE)
239 
240 RBSource *
241 rb_audioscrobbler_radio_source_new (RBAudioscrobblerProfilePage *parent,
242                                     RBAudioscrobblerService *service,
243                                     const char *username,
244                                     const char *session_key,
245                                     const char *station_name,
246                                     const char *station_url)
247 {
248 	RBSource *source;
249 	RBShell *shell;
250 	GObject *plugin;
251 	RhythmDB *db;
252 	GMenu *toolbar_menu;
253 
254 	g_object_get (parent, "shell", &shell, "plugin", &plugin, NULL);
255 	g_object_get (shell, "db", &db, NULL);
256 
257 	if (RHYTHMDB_ENTRY_TYPE_AUDIOSCROBBLER_RADIO_TRACK == NULL) {
258 		rb_audioscrobbler_radio_track_register_entry_type (db);
259 	}
260 
261 	g_object_get (parent, "toolbar-menu", &toolbar_menu, NULL);
262 
263 	source = g_object_new (RB_TYPE_AUDIOSCROBBLER_RADIO_SOURCE,
264 	                       "shell", shell,
265 	                       "plugin", plugin,
266 	                       "name", station_name,
267 	                       "entry-type", RHYTHMDB_ENTRY_TYPE_AUDIOSCROBBLER_RADIO_TRACK,
268 	                       "parent", parent,
269 	                       "service", service,
270                                "username", username,
271 	                       "session-key", session_key,
272 	                       "station-url", station_url,
273 			       "toolbar-menu", toolbar_menu,
274 	                       NULL);
275 
276 	g_object_unref (shell);
277 	g_object_unref (plugin);
278 	g_object_unref (db);
279 	g_object_unref (toolbar_menu);
280 
281 	return source;
282 }
283 
284 static void
rb_audioscrobbler_radio_source_class_init(RBAudioscrobblerRadioSourceClass * klass)285 rb_audioscrobbler_radio_source_class_init (RBAudioscrobblerRadioSourceClass *klass)
286 {
287 	GObjectClass *object_class;
288 	RBDisplayPageClass *page_class;
289 	RBSourceClass *source_class;
290 
291 	object_class = G_OBJECT_CLASS (klass);
292 	object_class->constructed = rb_audioscrobbler_radio_source_constructed;
293 	object_class->dispose = rb_audioscrobbler_radio_source_dispose;
294 	object_class->finalize = rb_audioscrobbler_radio_source_finalize;
295 	object_class->get_property = rb_audioscrobbler_radio_source_get_property;
296 	object_class->set_property = rb_audioscrobbler_radio_source_set_property;
297 
298 	page_class = RB_DISPLAY_PAGE_CLASS (klass);
299 	page_class->selected = impl_selected;
300 	page_class->delete_thyself = impl_delete_thyself;
301 	page_class->can_remove = impl_can_remove;
302 	page_class->remove = impl_remove;
303 
304 	source_class = RB_SOURCE_CLASS (klass);
305 	source_class->can_rename = (RBSourceFeatureFunc) rb_true_function;
306 	source_class->can_copy = (RBSourceFeatureFunc) rb_false_function;
307 	source_class->can_delete = (RBSourceFeatureFunc) rb_false_function;
308 	source_class->can_pause = (RBSourceFeatureFunc) rb_false_function;
309 	source_class->try_playlist = (RBSourceFeatureFunc) rb_false_function;
310 	source_class->get_entry_view = impl_get_entry_view;
311 	source_class->handle_eos = impl_handle_eos;
312 	source_class->get_playback_status = impl_get_playback_status;
313 
314 	g_object_class_install_property (object_class,
315 	                                 PROP_PARENT,
316 	                                 g_param_spec_object ("parent",
317 	                                                      "Parent",
318 	                                                      "Profile page that created this radio source",
319 	                                                      RB_TYPE_AUDIOSCROBBLER_PROFILE_PAGE,
320                                                               G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
321 
322 	g_object_class_install_property (object_class,
323 	                                 PROP_SERVICE,
324 	                                 g_param_spec_object ("service",
325 	                                                      "Service",
326 	                                                      "Service to stream radio from",
327 	                                                      RB_TYPE_AUDIOSCROBBLER_SERVICE,
328                                                               G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
329 
330 	g_object_class_install_property (object_class,
331 	                                 PROP_USERNAME,
332 	                                 g_param_spec_string ("username",
333 	                                                      "Username",
334 	                                                      "Username of the user who is streaming radio",
335 	                                                      NULL,
336                                                               G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
337 
338 	g_object_class_install_property (object_class,
339 	                                 PROP_SESSION_KEY,
340 	                                 g_param_spec_string ("session-key",
341 	                                                      "Session Key",
342 	                                                      "Session key used to authenticate the user",
343 	                                                      NULL,
344                                                               G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
345 
346 	g_object_class_install_property (object_class,
347 	                                 PROP_STATION_URL,
348 	                                 g_param_spec_string ("station-url",
349 	                                                      "Station URL",
350 	                                                      "Last.fm radio URL of the station this source will stream",
351 	                                                      NULL,
352                                                               G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
353 
354 	g_object_class_override_property (object_class,
355 					  PROP_PLAY_ORDER,
356 					  "play-order");
357 
358 	g_type_class_add_private (klass, sizeof (RBAudioscrobblerRadioSourcePrivate));
359 }
360 
361 static void
rb_audioscrobbler_radio_source_class_finalize(RBAudioscrobblerRadioSourceClass * klass)362 rb_audioscrobbler_radio_source_class_finalize (RBAudioscrobblerRadioSourceClass *klass)
363 {
364 }
365 
366 static void
rb_audioscrobbler_radio_source_init(RBAudioscrobblerRadioSource * source)367 rb_audioscrobbler_radio_source_init (RBAudioscrobblerRadioSource *source)
368 {
369 	source->priv = RB_AUDIOSCROBBLER_RADIO_SOURCE_GET_PRIVATE (source);
370 
371 	source->priv->soup_session =
372 		soup_session_new_with_options (SOUP_SESSION_ADD_FEATURE_BY_TYPE,
373 					       SOUP_TYPE_PROXY_RESOLVER_DEFAULT,
374 					       NULL);
375 }
376 
377 static void
rb_audioscrobbler_radio_source_constructed(GObject * object)378 rb_audioscrobbler_radio_source_constructed (GObject *object)
379 {
380 	RBAudioscrobblerRadioSource *source;
381 	RBShell *shell;
382 	RBShellPlayer *shell_player;
383 	RhythmDB *db;
384 	GtkWidget *main_vbox;
385 	GtkWidget *error_info_bar_content_area;
386 	GtkAccelGroup *accel_group;
387 	RBSourceToolbar *toolbar;
388 
389 	RB_CHAIN_GOBJECT_METHOD (rb_audioscrobbler_radio_source_parent_class, constructed, object);
390 
391 	source = RB_AUDIOSCROBBLER_RADIO_SOURCE (object);
392 	g_object_get (source, "shell", &shell, NULL);
393 	g_object_get (shell,
394 		      "db", &db,
395 		      "shell-player", &shell_player,
396 		      "accel-group", &accel_group,
397 		      NULL);
398 
399 	source->priv->art_store = rb_ext_db_new ("album-art");
400 
401 	main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 4);
402 	gtk_widget_show (main_vbox);
403 	gtk_container_add (GTK_CONTAINER (source), main_vbox);
404 
405 	/* toolbar */
406 	toolbar = rb_source_toolbar_new (RB_DISPLAY_PAGE (source), accel_group);
407 	gtk_box_pack_start (GTK_BOX (main_vbox), GTK_WIDGET (toolbar), FALSE, FALSE, 0);
408 	gtk_widget_show_all (GTK_WIDGET (toolbar));
409 
410 	/* error info bar */
411 	source->priv->error_info_bar = gtk_info_bar_new ();
412 	source->priv->error_info_bar_label = gtk_label_new ("");
413 	error_info_bar_content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (source->priv->error_info_bar));
414 	gtk_container_add (GTK_CONTAINER (error_info_bar_content_area), source->priv->error_info_bar_label);
415 	gtk_box_pack_start (GTK_BOX (main_vbox), source->priv->error_info_bar, FALSE, FALSE, 0);
416 
417 	/* entry view */
418 	source->priv->track_view = rb_entry_view_new (db, G_OBJECT (shell_player), FALSE, FALSE);
419 	rb_entry_view_append_column (source->priv->track_view, RB_ENTRY_VIEW_COL_TITLE, TRUE);
420 	rb_entry_view_append_column (source->priv->track_view, RB_ENTRY_VIEW_COL_ARTIST, FALSE);
421 	rb_entry_view_append_column (source->priv->track_view, RB_ENTRY_VIEW_COL_ALBUM, FALSE);
422 	rb_entry_view_append_column (source->priv->track_view, RB_ENTRY_VIEW_COL_DURATION, FALSE);
423 	rb_entry_view_set_columns_clickable (source->priv->track_view, FALSE);
424 	gtk_widget_show_all (GTK_WIDGET (source->priv->track_view));
425 
426 	gtk_box_pack_start (GTK_BOX (main_vbox), GTK_WIDGET (source->priv->track_view), TRUE, TRUE, 0);
427 
428 	rb_source_bind_settings (RB_SOURCE (source), GTK_WIDGET (source->priv->track_view), NULL, NULL, TRUE);
429 
430 	/* query model */
431 	source->priv->track_model = rhythmdb_query_model_new_empty (db);
432 	rb_entry_view_set_model (source->priv->track_view, source->priv->track_model);
433 	g_object_set (source, "query-model", source->priv->track_model, NULL);
434 
435 	/* play order */
436 	source->priv->play_order = rb_audioscrobbler_play_order_new (shell_player);
437 
438 	/* signals */
439 	g_signal_connect_object (shell_player,
440 				 "playing-song-changed",
441 				 G_CALLBACK (playing_song_changed_cb),
442 				 source, 0);
443 
444 	rb_shell_append_display_page (shell, RB_DISPLAY_PAGE (source), RB_DISPLAY_PAGE (source->priv->parent));
445 
446 	g_object_unref (shell);
447 	g_object_unref (shell_player);
448 	g_object_unref (db);
449 	g_object_unref (accel_group);
450 }
451 
452 static void
rb_audioscrobbler_radio_source_dispose(GObject * object)453 rb_audioscrobbler_radio_source_dispose (GObject *object)
454 {
455 	RBAudioscrobblerRadioSource *source = RB_AUDIOSCROBBLER_RADIO_SOURCE (object);
456 
457 	if (source->priv->soup_session != NULL) {
458 		soup_session_abort (source->priv->soup_session);
459 		g_object_unref (source->priv->soup_session);
460 		source->priv->soup_session = NULL;
461 	}
462 
463 	if (source->priv->service != NULL) {
464 		g_object_unref (source->priv->service);
465 		source->priv->service = NULL;
466 	}
467 
468 	if (source->priv->track_model != NULL) {
469 		g_object_unref (source->priv->track_model);
470 		source->priv->track_model = NULL;
471 	}
472 
473 	if (source->priv->play_order != NULL) {
474 		g_object_unref (source->priv->play_order);
475 		source->priv->play_order = NULL;
476 	}
477 
478 	if (source->priv->art_store != NULL) {
479 		g_object_unref (source->priv->art_store);
480 		source->priv->art_store = NULL;
481 	}
482 
483 	G_OBJECT_CLASS (rb_audioscrobbler_radio_source_parent_class)->dispose (object);
484 }
485 
486 static void
rb_audioscrobbler_radio_source_finalize(GObject * object)487 rb_audioscrobbler_radio_source_finalize (GObject *object)
488 {
489 	RBAudioscrobblerRadioSource *source = RB_AUDIOSCROBBLER_RADIO_SOURCE (object);
490 
491 	g_free (source->priv->username);
492 	g_free (source->priv->session_key);
493 	g_free (source->priv->station_url);
494 
495 	G_OBJECT_CLASS (rb_audioscrobbler_radio_source_parent_class)->finalize (object);
496 }
497 
498 static void
rb_audioscrobbler_radio_source_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)499 rb_audioscrobbler_radio_source_get_property (GObject *object,
500                                              guint prop_id,
501                                              GValue *value,
502                                              GParamSpec *pspec)
503 {
504 	RBAudioscrobblerRadioSource *source = RB_AUDIOSCROBBLER_RADIO_SOURCE (object);
505 	switch (prop_id) {
506 	case PROP_STATION_URL:
507 		g_value_set_string (value, source->priv->station_url);
508 		break;
509 	case PROP_PLAY_ORDER:
510 		g_value_set_object (value, source->priv->play_order);
511 		break;
512 	default:
513 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
514 		break;
515 	}
516 }
517 
518 static void
rb_audioscrobbler_radio_source_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)519 rb_audioscrobbler_radio_source_set_property (GObject *object,
520                                              guint prop_id,
521                                              const GValue *value,
522                                              GParamSpec *pspec)
523 {
524 	RBAudioscrobblerRadioSource *source = RB_AUDIOSCROBBLER_RADIO_SOURCE (object);
525 	switch (prop_id) {
526 	case PROP_PARENT:
527 		source->priv->parent = g_value_get_object (value);
528 		break;
529 	case PROP_SERVICE:
530 		source->priv->service = g_value_dup_object (value);
531 		break;
532 	case PROP_USERNAME:
533 		source->priv->username = g_value_dup_string (value);
534 		break;
535 	case PROP_SESSION_KEY:
536 		source->priv->session_key = g_value_dup_string (value);
537 		break;
538 	case PROP_STATION_URL:
539 		source->priv->station_url = g_value_dup_string (value);
540 		break;
541 	default:
542 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
543 		break;
544 	}
545 }
546 
547 static void
playing_song_changed_cb(RBShellPlayer * player,RhythmDBEntry * entry,RBAudioscrobblerRadioSource * source)548 playing_song_changed_cb (RBShellPlayer *player,
549                          RhythmDBEntry *entry,
550                          RBAudioscrobblerRadioSource *source)
551 {
552 	RhythmDB *db;
553 	GtkTreeIter playing_iter;
554 
555 	g_object_get (player, "db", &db, NULL);
556 
557 	/* delete old entry */
558 	if (source->priv->playing_entry != NULL) {
559 		rhythmdb_query_model_remove_entry (source->priv->track_model, source->priv->playing_entry);
560 		rhythmdb_entry_delete (db, source->priv->playing_entry);
561 		source->priv->playing_entry = NULL;
562 	}
563 
564 	/* check if the new playing entry is from this source */
565 	if (rhythmdb_query_model_entry_to_iter (source->priv->track_model, entry, &playing_iter) == TRUE) {
566 		RBAudioscrobblerRadioTrackData *track_data;
567 		RBExtDBKey *key;
568 		GtkTreeIter iter;
569 		gboolean reached_playing = FALSE;
570 		int entries_after_playing = 0;
571 		GList *remove = NULL;
572 		GList *i;
573 
574 		/* update our playing entry */
575 		source->priv->playing_entry = entry;
576 
577 		/* mark invalidated entries for removal and count remaining */
578 		gtk_tree_model_get_iter_first (GTK_TREE_MODEL (source->priv->track_model), &iter);
579 		do {
580 			RhythmDBEntry *iter_entry;
581 			iter_entry = rhythmdb_query_model_iter_to_entry (source->priv->track_model, &iter);
582 
583 			if (reached_playing == TRUE) {
584 				entries_after_playing++;
585 			} else if (iter_entry == entry) {
586 				reached_playing = TRUE;
587 			} else {
588 				/* add to list of entries marked for removal */
589 				remove = g_list_append (remove, iter_entry);
590 			}
591 
592 			rhythmdb_entry_unref (iter_entry);
593 
594 		} while (gtk_tree_model_iter_next (GTK_TREE_MODEL (source->priv->track_model), &iter));
595 
596 		/* remove invalidated entries */
597 		for (i = remove; i != NULL; i = i->next) {
598 			rhythmdb_query_model_remove_entry (source->priv->track_model, i->data);
599 			rhythmdb_entry_delete (db, i->data);
600 		}
601 
602 		/* request more if needed */
603 		if (entries_after_playing <= 2) {
604 			tune (source);
605 		}
606 
607 		/* provide cover art */
608 		key = rb_ext_db_key_create_storage ("album", rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM));
609 		rb_ext_db_key_add_field (key, "artist", rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST));
610 		track_data = RHYTHMDB_ENTRY_GET_TYPE_DATA(entry, RBAudioscrobblerRadioTrackData);
611 		rb_ext_db_store_uri (source->priv->art_store,
612 				     key,
613 				     RB_EXT_DB_SOURCE_SEARCH,
614 				     track_data->image_url);
615 		rb_ext_db_key_free (key);
616 	}
617 
618 	rhythmdb_commit (db);
619 
620 	g_object_unref (db);
621 }
622 
623 static void
tune(RBAudioscrobblerRadioSource * source)624 tune (RBAudioscrobblerRadioSource *source)
625 {
626 	char *sig_arg;
627 	char *sig;
628 	char *escaped_station_url;
629 	char *request;
630 	char *msg_url;
631 	SoupMessage *msg;
632 
633 	/* only go through the tune + get playlist process once at a time */
634 	if (source->priv->is_busy == TRUE) {
635 		return;
636 	}
637 
638 	source->priv->is_busy = TRUE;
639 	gtk_widget_hide (source->priv->error_info_bar);
640 
641 	sig_arg = g_strdup_printf ("api_key%smethodradio.tunesk%sstation%s%s",
642 	                           rb_audioscrobbler_service_get_api_key (source->priv->service),
643 	                           source->priv->session_key,
644 	                           source->priv->station_url,
645 	                           rb_audioscrobbler_service_get_api_secret (source->priv->service));
646 
647 	sig = g_compute_checksum_for_string (G_CHECKSUM_MD5, sig_arg, -1);
648 
649 	escaped_station_url = g_uri_escape_string (source->priv->station_url, NULL, FALSE);
650 
651 	request = g_strdup_printf ("method=radio.tune&station=%s&api_key=%s&api_sig=%s&sk=%s",
652 	                           escaped_station_url,
653 	                           rb_audioscrobbler_service_get_api_key (source->priv->service),
654 	                           sig,
655 	                           source->priv->session_key);
656 
657 	/* The format parameter needs to go here instead of in the request body */
658 	msg_url = g_strdup_printf ("%s?format=json",
659 	                           rb_audioscrobbler_service_get_api_url (source->priv->service));
660 
661 	rb_debug ("sending tune request: %s", request);
662 	msg = soup_message_new ("POST", msg_url);
663 	soup_message_set_request (msg,
664 	                          "application/x-www-form-urlencoded",
665 	                          SOUP_MEMORY_COPY,
666 	                          request,
667 	                          strlen (request));
668 	soup_session_queue_message (source->priv->soup_session,
669 	                            msg,
670 	                            tune_response_cb,
671 	                            source);
672 
673 	g_free (escaped_station_url);
674 	g_free (sig_arg);
675 	g_free (sig);
676 	g_free (request);
677 	g_free (msg_url);
678 }
679 
680 static void
tune_response_cb(SoupSession * session,SoupMessage * msg,gpointer user_data)681 tune_response_cb (SoupSession *session,
682                   SoupMessage *msg,
683                   gpointer user_data)
684 {
685 	RBAudioscrobblerRadioSource *source;
686 	JsonParser *parser;
687 
688 	source = RB_AUDIOSCROBBLER_RADIO_SOURCE (user_data);
689 	parser = json_parser_new ();
690 
691 	if (msg->response_body->data == NULL) {
692 		rb_debug ("no response from tune request");
693 		display_error_info_bar (source, _("Error tuning station: no response"));
694 		source->priv->is_busy = FALSE;
695 
696 	} else if (json_parser_load_from_data (parser, msg->response_body->data, msg->response_body->length, NULL)) {
697 		JsonObject *root_object;
698 		root_object = json_node_get_object (json_parser_get_root (parser));
699 
700 		/* Noticed on 2010-08-12 that Last.fm now responds with a "{ status:ok }"
701 		 * instead of providing a "station" object with various properties.
702 		 * Checking for a "station" or "status" member ensures compatibility with
703 		 * both Last.fm and Libre.fm.
704 		 */
705 		if (json_object_has_member (root_object, "station") ||
706 		    json_object_has_member (root_object, "status")) {
707 			rb_debug ("tune request was successful");
708 
709 			/* get the playlist */
710 			fetch_playlist (source);
711 		} else if (json_object_has_member (root_object, "error")) {
712 			int code;
713 			const char *message;
714 
715 			code = json_object_get_int_member (root_object, "error");
716 			message = json_object_get_string_member (root_object, "message");
717 
718 			rb_debug ("tune request responded with error: %s", message);
719 
720 			/* show appropriate error message */
721 			char *error_message = NULL;
722 
723 			if (code == 6) {
724 				/* Invalid station url */
725 				error_message = g_strdup (_("Invalid station URL"));
726 			} else if (code == 12) {
727 				/* Subscriber only station */
728 				/* Translators: %s is the name of the audioscrobbler service, for example "Last.fm".
729 				 * This message indicates that to listen to this radio station the user needs to be
730 				 * a paying subscriber to the service. */
731 				error_message = g_strdup_printf (_("This station is only available to %s subscribers"),
732 								 rb_audioscrobbler_service_get_name (source->priv->service));
733 			} else if (code == 20) {
734 				/* Not enough content */
735 				error_message = g_strdup (_("Not enough content to play station"));
736 			} else if (code == 27) {
737 				/* Deprecated station */
738 				/* Translators: %s is the name of the audioscrobbler service, for example "Last.fm".
739 				 * This message indicates that the service has deprecated this type of station. */
740 				error_message = g_strdup_printf (_("%s no longer supports this type of station"),
741 								 rb_audioscrobbler_service_get_name (source->priv->service));
742 			} else {
743 				/* Other error */
744 				error_message = g_strdup_printf (_("Error tuning station: %i - %s"), code, message);
745 			}
746 
747 			display_error_info_bar (source, error_message);
748 
749 			g_free (error_message);
750 
751 			source->priv->is_busy = FALSE;
752 		} else {
753 			rb_debug ("unexpected response from tune request: %s", msg->response_body->data);
754 			display_error_info_bar(source, _("Error tuning station: unexpected response"));
755 			source->priv->is_busy = FALSE;
756 		}
757 	} else {
758 		rb_debug ("invalid response from tune request: %s", msg->response_body->data);
759 		display_error_info_bar(source, _("Error tuning station: invalid response"));
760 		source->priv->is_busy = FALSE;
761 	}
762 }
763 
764 static void
fetch_playlist(RBAudioscrobblerRadioSource * source)765 fetch_playlist (RBAudioscrobblerRadioSource *source)
766 {
767 	char *sig_arg;
768 	char *sig;
769 	char *request;
770 	SoupMessage *msg;
771 
772 	sig_arg = g_strdup_printf ("api_key%smethodradio.getPlaylistrawtruesk%s%s",
773 	                           rb_audioscrobbler_service_get_api_key (source->priv->service),
774 	                           source->priv->session_key,
775 	                           rb_audioscrobbler_service_get_api_secret (source->priv->service));
776 
777 	sig = g_compute_checksum_for_string (G_CHECKSUM_MD5, sig_arg, -1);
778 
779 	request = g_strdup_printf ("method=radio.getPlaylist&api_key=%s&api_sig=%s&sk=%s&raw=true",
780 	                           rb_audioscrobbler_service_get_api_key (source->priv->service),
781 	                           sig,
782 	                           source->priv->session_key);
783 
784 	rb_debug ("sending playlist request: %s", request);
785 	msg = soup_message_new ("POST", rb_audioscrobbler_service_get_api_url (source->priv->service));
786 	soup_message_set_request (msg,
787 	                          "application/x-www-form-urlencoded",
788 	                          SOUP_MEMORY_COPY,
789 	                          request,
790 	                          strlen (request));
791 	soup_session_queue_message (source->priv->soup_session,
792 	                            msg,
793 	                            fetch_playlist_response_cb,
794 	                            source);
795 
796 	g_free (sig_arg);
797 	g_free (sig);
798 	g_free (request);
799 }
800 
801 static void
fetch_playlist_response_cb(SoupSession * session,SoupMessage * msg,gpointer user_data)802 fetch_playlist_response_cb (SoupSession *session,
803                             SoupMessage *msg,
804                             gpointer user_data)
805 {
806 	RBAudioscrobblerRadioSource *source;
807 	int tmp_fd;
808 	char *tmp_name;
809 	char *tmp_uri = NULL;
810 	GIOChannel *channel = NULL;
811 	TotemPlParser *parser = NULL;
812 	TotemPlParserResult result;
813 	GError *error = NULL;
814 
815 	source = RB_AUDIOSCROBBLER_RADIO_SOURCE (user_data);
816 
817 	source->priv->is_busy = FALSE;
818 
819 	if (msg->response_body->data == NULL) {
820 		rb_debug ("no response from get playlist request");
821 		return;
822 	}
823 
824 	/* until totem-pl-parser can parse playlists from in-memory data, we save it to a
825 	 * temporary file.
826 	 */
827 
828 	tmp_fd = g_file_open_tmp ("rb-audioscrobbler-playlist-XXXXXX.xspf", &tmp_name, &error);
829 	if (error != NULL) {
830 		rb_debug ("unable to save playlist: %s", error->message);
831 		goto cleanup;
832 	}
833 
834 	channel = g_io_channel_unix_new (tmp_fd);
835 	g_io_channel_write_chars (channel, msg->response_body->data, msg->response_body->length, NULL, &error);
836 	if (error != NULL) {
837 		rb_debug ("unable to save playlist: %s", error->message);
838 		goto cleanup;
839 	}
840 	g_io_channel_flush (channel, NULL);		/* ignore errors.. */
841 
842 	tmp_uri = g_filename_to_uri (tmp_name, NULL, &error);
843 	if (error != NULL) {
844 		rb_debug ("unable to parse playlist: %s", error->message);
845 		goto cleanup;
846 	}
847 
848 	rb_debug ("parsing playlist %s", tmp_uri);
849 
850 	parser = totem_pl_parser_new ();
851 	g_signal_connect_data (parser, "entry-parsed",
852 	                       G_CALLBACK (xspf_entry_parsed),
853 	                       source, NULL, 0);
854 	result = totem_pl_parser_parse (parser, tmp_uri, FALSE);
855 
856 	switch (result) {
857 	default:
858 	case TOTEM_PL_PARSER_RESULT_UNHANDLED:
859 	case TOTEM_PL_PARSER_RESULT_IGNORED:
860 	case TOTEM_PL_PARSER_RESULT_ERROR:
861 		rb_debug ("playlist didn't parse");
862 		break;
863 
864 	case TOTEM_PL_PARSER_RESULT_SUCCESS:
865 		rb_debug ("playlist parsed successfully");
866 		break;
867 	}
868 
869  cleanup:
870 	if (channel != NULL) {
871 		g_io_channel_unref (channel);
872 	}
873 	if (parser != NULL) {
874 		g_object_unref (parser);
875 	}
876 	if (error != NULL) {
877 		g_error_free (error);
878 	}
879 	close (tmp_fd);
880 	g_unlink (tmp_name);
881 	g_free (tmp_name);
882 	g_free (tmp_uri);
883 }
884 
885 static void
xspf_entry_parsed(TotemPlParser * parser,const char * uri,GHashTable * metadata,RBAudioscrobblerRadioSource * source)886 xspf_entry_parsed (TotemPlParser *parser,
887                    const char *uri,
888                    GHashTable *metadata,
889                    RBAudioscrobblerRadioSource *source)
890 {
891 	RBShell *shell;
892 	RhythmDBEntryType *entry_type;
893 	RhythmDB *db;
894 
895 	RhythmDBEntry *entry;
896 	RBAudioscrobblerRadioTrackData *track_data;
897 	const char *value;
898 	GValue v = {0,};
899 	int i;
900 	struct {
901 		const char *field;
902 		RhythmDBPropType prop;
903 	} field_mapping[] = {
904 		{ TOTEM_PL_PARSER_FIELD_TITLE, RHYTHMDB_PROP_TITLE },
905 		{ TOTEM_PL_PARSER_FIELD_AUTHOR, RHYTHMDB_PROP_ARTIST },
906 		{ TOTEM_PL_PARSER_FIELD_ALBUM, RHYTHMDB_PROP_ALBUM },
907 	};
908 
909 	g_object_get (source, "shell", &shell, "entry-type", &entry_type, NULL);
910 	g_object_get (shell, "db", &db, NULL);
911 
912 	/* create db entry if it doesn't already exist */
913 	entry = rhythmdb_entry_lookup_by_location (db, uri);
914 	if (entry == NULL) {
915 		rb_debug ("creating new track entry for %s", uri);
916 		entry = rhythmdb_entry_new (db, entry_type, uri);
917 	} else {
918 		rb_debug ("track entry %s already exists", uri);
919 	}
920 	track_data = RHYTHMDB_ENTRY_GET_TYPE_DATA (entry, RBAudioscrobblerRadioTrackData);
921 	track_data->service = source->priv->service;
922 
923 	/* straightforward string copying */
924 	for (i = 0; i < G_N_ELEMENTS (field_mapping); i++) {
925 		value = g_hash_table_lookup (metadata, field_mapping[i].field);
926 		if (value != NULL) {
927 			g_value_init (&v, G_TYPE_STRING);
928 			g_value_set_string (&v, value);
929 			rhythmdb_entry_set (db, entry, field_mapping[i].prop, &v);
930 			g_value_unset (&v);
931 		}
932 	}
933 
934 	/* duration needs some conversion */
935 	value = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_DURATION_MS);
936 	if (value != NULL) {
937 		gint64 duration;
938 
939 		duration = totem_pl_parser_parse_duration (value, FALSE);
940 		if (duration > 0) {
941 			g_value_init (&v, G_TYPE_ULONG);
942 			g_value_set_ulong (&v, (gulong) duration / 1000);		/* ms -> s */
943 			rhythmdb_entry_set (db, entry, RHYTHMDB_PROP_DURATION, &v);
944 			g_value_unset (&v);
945 		}
946 	}
947 
948 	/* image URL and track auth ID are stored in entry type specific data */
949 	value = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_IMAGE_URI);
950 	if (value != NULL) {
951 		track_data->image_url = g_strdup (value);
952 	}
953 
954 	value = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_ID);
955 	if (value != NULL) {
956 		track_data->track_auth = g_strdup (value);
957 	}
958 
959 	value = g_hash_table_lookup (metadata, TOTEM_PL_PARSER_FIELD_DOWNLOAD_URI);
960 	if (value != NULL) {
961 		track_data->download_url = g_strdup (value);
962 		rb_debug ("track %s has a download url: %s", uri, track_data->download_url);
963 	}
964 
965 	rhythmdb_query_model_add_entry (source->priv->track_model, entry, -1);
966 
967 	g_object_unref (shell);
968 	g_object_unref (db);
969 }
970 
971 static void
display_error_info_bar(RBAudioscrobblerRadioSource * source,const char * message)972 display_error_info_bar (RBAudioscrobblerRadioSource *source,
973                         const char *message)
974 {
975 	gtk_label_set_label (GTK_LABEL (source->priv->error_info_bar_label), message);
976 	gtk_info_bar_set_message_type (GTK_INFO_BAR (source->priv->error_info_bar), GTK_MESSAGE_WARNING);
977 	gtk_widget_show_all (source->priv->error_info_bar);
978 }
979 
980 static gboolean
impl_can_remove(RBDisplayPage * page)981 impl_can_remove (RBDisplayPage *page)
982 {
983 	return TRUE;
984 }
985 
986 static void
impl_remove(RBDisplayPage * page)987 impl_remove (RBDisplayPage *page)
988 {
989 	RBAudioscrobblerRadioSource *source = RB_AUDIOSCROBBLER_RADIO_SOURCE (page);
990 	rb_audioscrobbler_profile_page_remove_radio_station (source->priv->parent, RB_SOURCE (page));
991 }
992 
993 static void
impl_selected(RBDisplayPage * page)994 impl_selected (RBDisplayPage *page)
995 {
996 	RBAudioscrobblerRadioSource *source = RB_AUDIOSCROBBLER_RADIO_SOURCE (page);
997 
998 	RB_DISPLAY_PAGE_CLASS (rb_audioscrobbler_radio_source_parent_class)->selected (page);
999 
1000 	/* if the query model is empty then attempt to add some tracks to it */
1001 	if (rhythmdb_query_model_get_duration (source->priv->track_model) == 0) {
1002 		tune (source);
1003 	}
1004 }
1005 
1006 static RBEntryView *
impl_get_entry_view(RBSource * asource)1007 impl_get_entry_view (RBSource *asource)
1008 {
1009 	RBAudioscrobblerRadioSource *source = RB_AUDIOSCROBBLER_RADIO_SOURCE (asource);
1010 
1011 	return source->priv->track_view;
1012 }
1013 
1014 static void
impl_get_playback_status(RBSource * source,char ** text,float * progress)1015 impl_get_playback_status (RBSource *source, char **text, float *progress)
1016 {
1017 	rb_streaming_source_get_progress (RB_STREAMING_SOURCE (source), text, progress);
1018 }
1019 
1020 static RBSourceEOFType
impl_handle_eos(RBSource * asource)1021 impl_handle_eos (RBSource *asource)
1022 {
1023 	return RB_SOURCE_EOF_NEXT;
1024 }
1025 
1026 static void
impl_delete_thyself(RBDisplayPage * page)1027 impl_delete_thyself (RBDisplayPage *page)
1028 {
1029 	RBAudioscrobblerRadioSource *source;
1030 	RBShell *shell;
1031 	RhythmDB *db;
1032 	GtkTreeIter iter;
1033 	gboolean loop;
1034 
1035 	rb_debug ("deleting radio source");
1036 
1037 	source = RB_AUDIOSCROBBLER_RADIO_SOURCE (page);
1038 
1039 	g_object_get (source, "shell", &shell, NULL);
1040 	g_object_get (shell, "db", &db, NULL);
1041 
1042 	/* Ensure playing entry isn't deleted twice */
1043 	source->priv->playing_entry = NULL;
1044 
1045 	/* delete all entries */
1046 	loop = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (source->priv->track_model), &iter);
1047 	while (loop) {
1048 		RhythmDBEntry *entry;
1049 
1050 		entry = rhythmdb_query_model_iter_to_entry (source->priv->track_model, &iter);
1051 		rhythmdb_entry_delete (db, entry);
1052 		rhythmdb_entry_unref (entry);
1053 
1054 		loop = gtk_tree_model_iter_next (GTK_TREE_MODEL (source->priv->track_model), &iter);
1055 	}
1056 
1057 	rhythmdb_commit (db);
1058 
1059 	g_object_unref (shell);
1060 	g_object_unref (db);
1061 }
1062 
1063 void
_rb_audioscrobbler_radio_source_register_type(GTypeModule * module)1064 _rb_audioscrobbler_radio_source_register_type (GTypeModule *module)
1065 {
1066 	rb_audioscrobbler_radio_source_register_type (module);
1067 }
1068