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