1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
3 * arch-tag: Implementation of last.fm station source object
5 * Copyright (C) 2006 Matt Novenstern <fisxoj@gmail.com>
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
23 /* The author would like to extend thanks to Ian Holmes, author of Last Exit,
24 * an alternative last.fm player written in C#, the code of which was
25 * extraordinarily useful in the creation of this code
29 * - if subscriber, make user radio (low priority)
30 * - "recommendation radio" with percentage setting (0=obscure, 100=popular)
31 * - watch username gconf entries, create/update neighbour station
39 #include <glib/gi18n.h>
41 #include <glade/glade.h>
43 #include <gconf/gconf-value.h>
45 #include <libsoup/soup.h>
46 #include <libsoup/soup-uri.h>
50 #include "eel-gconf-extensions.h"
52 #include "rb-proxy-config.h"
53 #include "rb-preferences.h"
55 #include "rb-audioscrobbler.h"
56 #include "rb-lastfm-source.h"
58 #include "rhythmdb-query-model.h"
59 #include "rb-glade-helpers.h"
60 #include "rb-stock-icons.h"
61 #include "rb-entry-view.h"
62 #include "rb-property-view.h"
64 #include "rb-file-helpers.h"
65 #include "rb-preferences.h"
66 #include "rb-dialog.h"
67 #include "rb-station-properties-dialog.h"
68 #include "rb-new-station-dialog.h"
70 #include "eel-gconf-extensions.h"
71 #include "rb-shell-player.h"
73 #define LASTFM_URL "http://ws.audioscrobbler.com"
74 #define PLATFORM_STRING "linux"
75 #define RB_LASTFM_VERSION "1.1.1"
76 #define EXTRA_URI_ENCODE_CHARS "&+"
79 static void rb_lastfm_source_class_init (RBLastfmSourceClass
*klass
);
80 static void rb_lastfm_source_init (RBLastfmSource
*source
);
81 static GObject
*rb_lastfm_source_constructor (GType type
, guint n_construct_properties
,
82 GObjectConstructParam
*construct_properties
);
83 static void rb_lastfm_source_finalize (GObject
*object
);
84 static void rb_lastfm_source_set_property (GObject
*object
,
88 static void rb_lastfm_source_get_property (GObject
*object
,
93 static void rb_lastfm_source_songs_view_sort_order_changed_cb (RBEntryView
*view
,
94 RBLastfmSource
*source
);
95 static void rb_lastfm_source_do_query (RBLastfmSource
*source
);
97 /* source-specific methods */
98 static void rb_lastfm_source_do_handshake (RBLastfmSource
*source
);
99 static char* rb_lastfm_source_get_playback_uri (RhythmDBEntry
*entry
, gpointer data
);
100 static void rb_lastfm_perform (RBLastfmSource
*lastfm
,
102 char *post_data
, /* this takes ownership */
103 SoupMessageCallbackFn response_handler
);
104 static void rb_lastfm_message_cb (SoupMessage
*req
, gpointer user_data
);
105 static void rb_lastfm_change_station (RBLastfmSource
*source
, const char *station
);
107 static void rb_lastfm_proxy_config_changed_cb (RBProxyConfig
*config
,
108 RBLastfmSource
*source
);
109 static void rb_lastfm_source_drag_cb (GtkWidget
*widget
,
112 GtkSelectionData
*selection_data
,
113 guint info
, guint time
,
114 RBLastfmSource
*source
);
116 static void rb_lastfm_source_dispose (GObject
*object
);
118 /* RBSource implementation methods */
119 static void impl_delete (RBSource
*asource
);
120 static GList
*impl_get_ui_actions (RBSource
*source
);
121 static RBEntryView
*impl_get_entry_view (RBSource
*asource
);
122 static void impl_get_status (RBSource
*asource
, char **text
, char **progress_text
, float *progress
);
123 static RBSourceEOFType
impl_handle_eos (RBSource
*asource
);
124 static gboolean
impl_receive_drag (RBSource
*source
, GtkSelectionData
*data
);
125 static void impl_activate (RBSource
*source
);
127 static void rb_lastfm_source_new_station (char *uri
, char *title
, RBLastfmSource
*source
);
128 static void rb_lastfm_source_skip_track (GtkAction
*action
, RBLastfmSource
*source
);
129 static void rb_lastfm_source_love_track (GtkAction
*action
, RBLastfmSource
*source
);
130 static void rb_lastfm_source_ban_track (GtkAction
*action
, RBLastfmSource
*source
);
131 static char *rb_lastfm_source_title_from_uri (char *uri
);
132 static void rb_lastfm_source_add_station_cb (GtkButton
*button
, gpointer
*data
);
134 static void rb_lastfm_source_new_song_cb (GObject
*player_backend
, gpointer data
, RBLastfmSource
*source
);
135 static void rb_lastfm_song_changed_cb (RBShellPlayer
*player
, RhythmDBEntry
*entry
, RBLastfmSource
*source
);
137 static GValue
*streaming_title_request_cb (RhythmDB
*db
,
138 RhythmDBEntry
*entry
,
139 RBLastfmSource
*source
);
140 static GValue
*streaming_album_request_cb (RhythmDB
*db
,
141 RhythmDBEntry
*entry
,
142 RBLastfmSource
*source
);
143 static GValue
*streaming_artist_request_cb (RhythmDB
*db
,
144 RhythmDBEntry
*entry
,
145 RBLastfmSource
*source
);
146 static void extra_metadata_gather_cb (RhythmDB
*db
,
147 RhythmDBEntry
*entry
,
149 RBLastfmSource
*source
);
150 static void playing_source_changed_cb (RBShellPlayer
*player
,
152 RBLastfmSource
*lastfm_source
);
153 #ifdef HAVE_GSTREAMER_0_10
154 /* can't be bothered creating a whole header file just for this: */
155 GType
rb_lastfm_src_get_type (void);
158 struct RBLastfmSourcePrivate
164 /*GtkWidget *tuner;*/
167 GtkWidget
*typecombo
;
171 GtkActionGroup
*action_group
;
173 RBEntryView
*stations
;
175 RBShellPlayer
*shell_player
;
176 RhythmDBEntryType entry_type
;
188 /*RhythmDBEntry *pending_entry;*/
189 char *streaming_title
;
190 char *streaming_artist
;
191 char *streaming_album
;
201 SoupSession
*soup_session
;
202 RBProxyConfig
*proxy_config
;
208 G_DEFINE_TYPE (RBLastfmSource
, rb_lastfm_source
, RB_TYPE_SOURCE
);
209 #define RB_LASTFM_SOURCE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_LASTFM_SOURCE, RBLastfmSourcePrivate))
218 static GtkActionEntry rb_lastfm_source_actions
[] =
220 { "LastfmSkipSong", GTK_STOCK_MEDIA_FORWARD
, N_("Next Song"), NULL
,
221 N_("Skip the current track"),
222 G_CALLBACK (rb_lastfm_source_skip_track
) },
223 { "LastfmLoveSong", GTK_STOCK_ADD
, N_("Love Song"), NULL
,
224 N_("Mark this song as loved"),
225 G_CALLBACK (rb_lastfm_source_love_track
) },
226 { "LastfmBanSong", GTK_STOCK_CANCEL
, N_("Ban Song"), NULL
,
227 N_("Ban the current track from being played again"),
228 G_CALLBACK (rb_lastfm_source_ban_track
) }
231 static const GtkTargetEntry lastfm_drag_types
[] = {
232 { "text/plain", 0, 0 },
233 { "_NETSCAPE_URL", 0, 1 }
237 rb_lastfm_source_class_init (RBLastfmSourceClass
*klass
)
239 GObjectClass
*object_class
= G_OBJECT_CLASS (klass
);
240 RBSourceClass
*source_class
= RB_SOURCE_CLASS (klass
);
242 object_class
->finalize
= rb_lastfm_source_finalize
;
243 object_class
->dispose
= rb_lastfm_source_dispose
;
244 object_class
->constructor
= rb_lastfm_source_constructor
;
246 object_class
->set_property
= rb_lastfm_source_set_property
;
247 object_class
->get_property
= rb_lastfm_source_get_property
;
249 source_class
->impl_can_copy
= (RBSourceFeatureFunc
) rb_false_function
;
250 source_class
->impl_can_delete
= (RBSourceFeatureFunc
) rb_true_function
;
251 source_class
->impl_can_pause
= (RBSourceFeatureFunc
) rb_false_function
;
252 source_class
->impl_delete
= impl_delete
;
253 source_class
->impl_get_entry_view
= impl_get_entry_view
;
254 source_class
->impl_get_status
= impl_get_status
;
255 source_class
->impl_get_ui_actions
= impl_get_ui_actions
;
256 source_class
->impl_handle_eos
= impl_handle_eos
;
257 source_class
->impl_receive_drag
= impl_receive_drag
;
258 source_class
->impl_activate
= impl_activate
;
259 source_class
->impl_try_playlist
= (RBSourceFeatureFunc
) rb_true_function
;
261 g_object_class_install_property (object_class
,
263 g_param_spec_boxed ("entry-type",
265 "Type of the entries which should be displayed by this source",
266 RHYTHMDB_TYPE_ENTRY_TYPE
,
267 G_PARAM_READWRITE
| G_PARAM_CONSTRUCT_ONLY
));
268 g_object_class_install_property (object_class
,
270 g_param_spec_object ("proxy-config",
272 "RBProxyConfig object",
273 RB_TYPE_PROXY_CONFIG
,
274 G_PARAM_WRITABLE
| G_PARAM_CONSTRUCT_ONLY
));
275 g_type_class_add_private (klass
, sizeof (RBLastfmSourcePrivate
));
277 #ifdef HAVE_GSTREAMER_0_10
278 rb_lastfm_src_get_type ();
283 rb_lastfm_source_init (RBLastfmSource
*source
)
288 source
->priv
= RB_LASTFM_SOURCE_GET_PRIVATE (source
);
290 source
->priv
->vbox
= gtk_vbox_new (FALSE
, 5);
292 gtk_container_add (GTK_CONTAINER (source
), source
->priv
->vbox
);
294 gtk_icon_size_lookup (GTK_ICON_SIZE_LARGE_TOOLBAR
, &size
, NULL
);
295 pixbuf
= gtk_icon_theme_load_icon (gtk_icon_theme_get_default (),
299 rb_source_set_pixbuf (RB_SOURCE (source
), pixbuf
);
300 if (pixbuf
!= NULL
) {
301 g_object_unref (pixbuf
);
306 rb_lastfm_source_finalize (GObject
*object
)
308 RBLastfmSource
*source
;
310 g_return_if_fail (object
!= NULL
);
311 g_return_if_fail (RB_IS_LASTFM_SOURCE (object
));
313 source
= RB_LASTFM_SOURCE (object
);
315 g_return_if_fail (source
->priv
!= NULL
);
317 rb_debug ("finalizing lastfm source");
319 if (source
->priv
->db
) {
320 g_object_unref (source
->priv
->db
);
321 source
->priv
->db
= NULL
;
324 g_free (source
->priv
->streaming_title
);
325 g_free (source
->priv
->streaming_artist
);
326 g_free (source
->priv
->streaming_album
);
328 g_object_unref (G_OBJECT (source
->priv
->proxy_config
));
330 G_OBJECT_CLASS (rb_lastfm_source_parent_class
)->finalize (object
);
334 rb_lastfm_source_constructor (GType type
, guint n_construct_properties
,
335 GObjectConstructParam
*construct_properties
)
337 RBLastfmSource
*source
;
338 RBLastfmSourceClass
*klass
;
340 GObject
*player_backend
;
342 klass
= RB_LASTFM_SOURCE_CLASS (g_type_class_peek (RB_TYPE_LASTFM_SOURCE
));
344 source
= RB_LASTFM_SOURCE (G_OBJECT_CLASS (rb_lastfm_source_parent_class
)
345 ->constructor (type
, n_construct_properties
, construct_properties
));
347 g_object_get (G_OBJECT (source
), "shell", &shell
, NULL
);
348 g_object_get (G_OBJECT (shell
),
349 "db", &source
->priv
->db
,
350 "shell-player", &source
->priv
->shell_player
,
352 g_object_unref (G_OBJECT (shell
));
354 /* Set up station tuner */
355 /*source->priv->tuner = gtk_vbox_new (FALSE, 5); */
356 source
->priv
->vbox2
= gtk_vbox_new (FALSE
, 5);
357 source
->priv
->hbox
= gtk_hbox_new (FALSE
, 5);
359 source
->priv
->label
= gtk_label_new (_("Enter the artist or global tag to build a radio station out of:"));
360 g_object_set (source
->priv
->label
, "xalign", 0.0, NULL
);
362 source
->priv
->gobutton
= gtk_button_new_with_label (_("Add"));
363 g_signal_connect_object (G_OBJECT (source
->priv
->gobutton
),
365 G_CALLBACK (rb_lastfm_source_add_station_cb
),
367 source
->priv
->typecombo
= gtk_combo_box_new_text ();
368 gtk_combo_box_append_text (GTK_COMBO_BOX (source
->priv
->typecombo
), _("Artist"));
369 gtk_combo_box_append_text (GTK_COMBO_BOX (source
->priv
->typecombo
), _("Tag"));
370 gtk_combo_box_set_active (GTK_COMBO_BOX (source
->priv
->typecombo
), 0);
372 source
->priv
->txtbox
= gtk_entry_new ();
374 gtk_box_pack_end_defaults (GTK_BOX (source
->priv
->hbox
),
375 GTK_WIDGET (source
->priv
->gobutton
));
377 gtk_box_pack_end_defaults (GTK_BOX (source
->priv
->hbox
),
378 GTK_WIDGET (source
->priv
->txtbox
));
380 gtk_box_pack_start_defaults (GTK_BOX (source
->priv
->hbox
),
381 GTK_WIDGET (source
->priv
->typecombo
));
383 gtk_box_pack_end_defaults (GTK_BOX (source
->priv
->vbox2
),
384 GTK_WIDGET (source
->priv
->hbox
));
386 gtk_box_pack_end_defaults (GTK_BOX (source
->priv
->vbox2
),
387 GTK_WIDGET (source
->priv
->label
));
389 /* set up stations view */
390 source
->priv
->stations
= rb_entry_view_new (source
->priv
->db
,
391 G_OBJECT (source
->priv
->shell_player
),
394 /*rb_entry_view_append_column (source->priv->stations, RB_ENTRY_VIEW_COL_TITLE, TRUE);*/
395 rb_entry_view_append_column (source
->priv
->stations
, RB_ENTRY_VIEW_COL_GENRE
, TRUE
);
396 rb_entry_view_append_column (source
->priv
->stations
, RB_ENTRY_VIEW_COL_RATING
, TRUE
);
397 rb_entry_view_append_column (source
->priv
->stations
, RB_ENTRY_VIEW_COL_LAST_PLAYED
, TRUE
);
398 g_signal_connect_object (G_OBJECT (source
->priv
->stations
),
399 "sort-order-changed",
400 G_CALLBACK (rb_lastfm_source_songs_view_sort_order_changed_cb
),
403 /* Drag and drop URIs */
404 g_signal_connect_object (G_OBJECT (source
->priv
->stations
),
405 "drag_data_received",
406 G_CALLBACK (rb_lastfm_source_drag_cb
),
408 g_signal_connect_object (G_OBJECT (source
->priv
->shell_player
),
409 "playing-song-changed",
410 G_CALLBACK (rb_lastfm_song_changed_cb
),
413 gtk_drag_dest_set (GTK_WIDGET (source
->priv
->stations
),
414 GTK_DEST_DEFAULT_ALL
,
415 lastfm_drag_types
, 2,
416 GDK_ACTION_COPY
| GDK_ACTION_MOVE
);
419 /*gtk_paned_pack1 (GTK_PANED (source->priv->paned),
420 GTK_WIDGET (source->priv->tuner), FALSE, FALSE); */
422 /*source->priv->paned = gtk_vpaned_new ();
423 gtk_paned_pack2 (GTK_PANED (source->priv->paned),
424 GTK_WIDGET (source->priv->stations), TRUE, FALSE); */
426 gtk_box_pack_start (GTK_BOX (source
->priv
->vbox
), GTK_WIDGET (source
->priv
->vbox2
), FALSE
, FALSE
, 5);
427 gtk_box_pack_start_defaults (GTK_BOX (source
->priv
->vbox
), GTK_WIDGET (source
->priv
->stations
));
430 gtk_widget_show_all (GTK_WIDGET (source
));
433 source
->priv
->action_group
= _rb_source_register_action_group (RB_SOURCE (source
),
435 rb_lastfm_source_actions
,
436 G_N_ELEMENTS (rb_lastfm_source_actions
),
439 rb_lastfm_source_do_query (source
);
441 g_signal_connect_object (G_OBJECT (source
->priv
->db
),
442 "entry-extra-metadata-request::" RHYTHMDB_PROP_STREAM_SONG_TITLE
,
443 G_CALLBACK (streaming_title_request_cb
),
446 g_signal_connect_object (G_OBJECT (source
->priv
->db
),
447 "entry-extra-metadata-request::" RHYTHMDB_PROP_STREAM_SONG_ARTIST
,
448 G_CALLBACK (streaming_artist_request_cb
),
451 g_signal_connect_object (G_OBJECT (source
->priv
->db
),
452 "entry-extra-metadata-request::" RHYTHMDB_PROP_STREAM_SONG_ALBUM
,
453 G_CALLBACK (streaming_album_request_cb
),
456 g_signal_connect_object (G_OBJECT (source
->priv
->db
),
457 "entry-extra-metadata-gather",
458 G_CALLBACK (extra_metadata_gather_cb
),
461 g_object_get (source
->priv
->shell_player
, "player", &player_backend
, NULL
);
462 g_signal_connect_object (player_backend
,
463 "event::rb-lastfm-new-song",
464 G_CALLBACK (rb_lastfm_source_new_song_cb
),
467 source
->priv
->buffering
= -1;
468 g_signal_connect_object (source
->priv
->shell_player
, "playing-source-changed",
469 G_CALLBACK (playing_source_changed_cb
),
472 return G_OBJECT (source
);
476 rb_lastfm_source_set_property (GObject
*object
,
481 RBLastfmSource
*source
= RB_LASTFM_SOURCE (object
);
484 case PROP_ENTRY_TYPE
:
485 source
->priv
->entry_type
= g_value_get_boxed (value
);
487 case PROP_PROXY_CONFIG
:
488 source
->priv
->proxy_config
= g_value_get_object (value
);
489 g_object_ref (G_OBJECT (source
->priv
->proxy_config
));
490 g_signal_connect_object (G_OBJECT (source
->priv
->proxy_config
),
492 G_CALLBACK (rb_lastfm_proxy_config_changed_cb
),
496 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, prop_id
, pspec
);
502 rb_lastfm_source_get_property (GObject
*object
,
507 RBLastfmSource
*source
= RB_LASTFM_SOURCE (object
);
510 case PROP_ENTRY_TYPE
:
511 g_value_set_boxed (value
, source
->priv
->entry_type
);
514 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, prop_id
, pspec
);
522 md5_state_t md5state
;
524 gchar md5_response
[33];
528 memset (md5_response
, 0, sizeof (md5_response
));
530 md5_init (&md5state
);
531 md5_append (&md5state
, (unsigned char*)string
, strlen (string
));
532 md5_finish (&md5state
, md5pword
);
534 for (j
= 0; j
< 16; j
++) {
536 sprintf (a
, "%02x", md5pword
[j
]);
537 md5_response
[2*j
] = a
[0];
538 md5_response
[2*j
+1] = a
[1];
541 return (g_strdup (md5_response
));
545 rb_lastfm_source_new (RBShell
*shell
)
548 RBProxyConfig
*proxy_config
;
549 RhythmDBEntryType entry_type
;
554 g_object_get (G_OBJECT (shell
), "db", &db
, NULL
);
556 /* register entry type if it's not already registered */
557 entry_type
= rhythmdb_entry_type_get_by_name (db
, "lastfm-station");
558 if (entry_type
== RHYTHMDB_ENTRY_TYPE_INVALID
) {
559 entry_type
= rhythmdb_entry_register_type (db
, "lastfm-station");
560 entry_type
->save_to_disk
= TRUE
;
561 entry_type
->can_sync_metadata
= (RhythmDBEntryCanSyncFunc
) rb_true_function
;
562 entry_type
->sync_metadata
= (RhythmDBEntrySyncFunc
) rb_null_function
;
563 entry_type
->get_playback_uri
= (RhythmDBEntryStringFunc
) rb_lastfm_source_get_playback_uri
;
566 g_object_get (G_OBJECT (shell
), "proxy-config", &proxy_config
, NULL
);
568 source
= RB_SOURCE (g_object_new (RB_TYPE_LASTFM_SOURCE
,
569 "name", _("Last.fm"),
571 "entry-type", entry_type
,
572 "proxy-config", proxy_config
,
574 rb_shell_register_entry_type_for_source (shell
, source
, entry_type
);
576 entry_type
->get_playback_uri_data
= source
;
578 /* create default neighbour radio station */
579 username
= eel_gconf_get_string (CONF_AUDIOSCROBBLER_USERNAME
);
580 if (username
!= NULL
) {
581 RhythmDBEntry
*entry
;
583 uri
= g_strdup_printf ("lastfm://user/%s/neighbours", username
);
584 entry
= rhythmdb_entry_lookup_by_location (db
, uri
);
586 rb_lastfm_source_new_station (uri
, _("Neighbour Radio"), RB_LASTFM_SOURCE (source
));
588 rhythmdb_entry_unref (entry
);
595 g_object_unref (proxy_config
);
600 impl_get_ui_actions (RBSource
*source
)
602 GList
*actions
= NULL
;
604 actions
= g_list_prepend (actions
, g_strdup ("LastfmLoveSong"));
605 actions
= g_list_prepend (actions
, g_strdup ("LastfmBanSong"));
606 actions
= g_list_prepend (actions
, g_strdup ("LastfmSkipSong"));
612 impl_get_entry_view (RBSource
*asource
)
614 RBLastfmSource
*source
= RB_LASTFM_SOURCE (asource
);
616 return source
->priv
->stations
;
619 static RBSourceEOFType
620 impl_handle_eos (RBSource
*asource
)
622 return RB_SOURCE_EOF_RETRY
;
626 impl_get_status (RBSource
*asource
, char **text
, char **progress_text
, float *progress
)
628 RBLastfmSource
*source
= RB_LASTFM_SOURCE (asource
);
630 if (source
->priv
->buffering
!= -1) {
631 *progress
= ((float)source
->priv
->buffering
)/100;
632 *text
= g_strdup (_("Buffering"));
634 switch (source
->priv
->status
) {
636 *text
= g_strdup (_("No such artist. Check your spelling"));
640 *text
= g_strdup (_("Handshake failed"));
644 *text
= g_strdup (_("The server marked you as banned"));
650 RhythmDBQueryModel
*model
;
653 g_object_get (asource
, "query-model", &model
, NULL
);
654 num_entries
= gtk_tree_model_iter_n_children (GTK_TREE_MODEL (model
), NULL
);
655 g_object_unref (model
);
657 *text
= g_strdup_printf (ngettext ("%d station", "%d stations", num_entries
), num_entries
);
665 impl_delete (RBSource
*asource
)
667 RBLastfmSource
*source
= RB_LASTFM_SOURCE (asource
);
670 for (l
= rb_entry_view_get_selected_entries (source
->priv
->stations
); l
!= NULL
; l
= g_list_next (l
)) {
671 rhythmdb_entry_delete (source
->priv
->db
, l
->data
);
674 rhythmdb_commit (source
->priv
->db
);
678 rb_lastfm_source_songs_view_sort_order_changed_cb (RBEntryView
*view
,
679 RBLastfmSource
*source
)
681 rb_debug ("sort order changed");
683 rb_entry_view_resort_model (view
);
687 rb_lastfm_source_do_query (RBLastfmSource
*source
)
689 RhythmDBQueryModel
*station_query_model
;
692 query
= rhythmdb_query_parse (source
->priv
->db
,
693 RHYTHMDB_QUERY_PROP_EQUALS
,
695 source
->priv
->entry_type
,
697 station_query_model
= rhythmdb_query_model_new_empty (source
->priv
->db
);
698 rhythmdb_do_full_query_parsed (source
->priv
->db
,
699 RHYTHMDB_QUERY_RESULTS (station_query_model
),
702 rhythmdb_query_free (query
);
705 rb_entry_view_set_model (source
->priv
->stations
, station_query_model
);
706 g_object_set (G_OBJECT (source
), "query-model", station_query_model
, NULL
);
708 g_object_unref (G_OBJECT (station_query_model
));
712 rb_lastfm_source_do_handshake (RBLastfmSource
*source
)
719 if (source
->priv
->connected
) {
723 username
= eel_gconf_get_string (CONF_AUDIOSCROBBLER_USERNAME
);
724 if (username
== NULL
) {
725 rb_debug ("no last.fm username");
729 password
= eel_gconf_get_string (CONF_AUDIOSCROBBLER_PASSWORD
);
730 if (password
== NULL
) {
731 rb_debug ("no last.fm password");
735 md5password
= mkmd5 (password
);
738 handshake_url
= g_strdup_printf ("%s/radio/handshake.php?version=1.1.1&platform=linux&"
739 "username=%s&passwordmd5=%s&debug=0&partner=",
743 rb_debug ("Last.fm sending handshake");
744 g_object_ref (source
);
745 rb_lastfm_perform (source
, handshake_url
, NULL
, rb_lastfm_message_cb
);
746 g_free (handshake_url
);
748 g_free (md5password
);
752 rb_lastfm_source_get_playback_uri (RhythmDBEntry
*entry
, gpointer data
)
755 RBLastfmSource
*source
;
758 rb_debug ("NULL entry");
762 source
= RB_LASTFM_SOURCE (data
);
763 if (source
== NULL
) {
764 rb_debug ("NULL source pointer");
769 if (!source
->priv
->connected
) {
770 rb_debug ("not connected");
773 source
= RB_LASTFM_SOURCE (data
);
775 location
= g_strdup_printf ("xrblastfm://%s", source
->priv
->stream_url
+ strlen("http://"));
776 rb_debug ("playback uri: %s", location
);
781 rb_lastfm_perform (RBLastfmSource
*source
,
784 SoupMessageCallbackFn response_handler
)
787 msg
= soup_message_new ("GET", url
);
792 soup_message_set_http_version (msg
, SOUP_HTTP_1_1
);
794 rb_debug ("Last.fm communicating with %s", url
);
796 if (post_data
!= NULL
) {
797 rb_debug ("POST data: %s", post_data
);
798 soup_message_set_request (msg
,
799 "application/x-www-form-urlencoded",
800 SOUP_BUFFER_SYSTEM_OWNED
,
805 /* create soup session, if we haven't got one yet */
806 if (!source
->priv
->soup_session
) {
809 uri
= rb_proxy_config_get_libsoup_uri (source
->priv
->proxy_config
);
810 source
->priv
->soup_session
= soup_session_async_new_with_options (
817 soup_session_queue_message (source
->priv
->soup_session
,
819 (SoupMessageCallbackFn
) response_handler
,
821 source
->priv
->status
= COMMUNICATING
;
822 rb_source_notify_status_changed (RB_SOURCE(source
));
826 rb_lastfm_message_cb (SoupMessage
*req
, gpointer user_data
)
828 RBLastfmSource
*source
= RB_LASTFM_SOURCE (user_data
);
833 if ((req
->response
).body
== NULL
) {
834 rb_debug ("Lastfm: Server failed to respond");
838 body
= g_malloc0 ((req
->response
).length
+ 1);
839 memcpy (body
, (req
->response
).body
, (req
->response
).length
);
841 rb_debug ("response body: %s", body
);
843 if (strstr (body
, "ERROR - no such artist") != NULL
) {
844 source
->priv
->status
= NO_ARTIST
;
848 pieces
= g_strsplit (body
, "\n", 6);
849 for (i
= 0; pieces
[i
] != NULL
; i
++) {
850 gchar
**values
= g_strsplit (pieces
[i
], "=", 2);
851 if (strcmp (values
[0], "session") == 0) {
852 if (strcmp (values
[1], "FAILED") == 0) {
853 source
->priv
->status
= FAILED
;
854 rb_debug ("Lastfm failed to connect to the server");
857 source
->priv
->status
= OK
;
858 source
->priv
->session
= g_strdup (values
[1]);
859 rb_debug ("session ID: %s", source
->priv
->session
);
860 source
->priv
->connected
= TRUE
;
861 } else if (strcmp (values
[0], "stream_url") == 0) {
862 source
->priv
->stream_url
= g_strdup (values
[1]);
863 rb_debug ("stream url: %s", source
->priv
->stream_url
);
864 } else if (strcmp (values
[0], "subscriber") == 0) {
865 if (strcmp (values
[1], "0") == 0) {
866 source
->priv
->subscriber
= FALSE
;
868 source
->priv
->subscriber
= TRUE
;
870 } else if (strcmp (values
[0], "framehack") ==0 ) {
871 if (strcmp (values
[1], "0") == 0) {
872 source
->priv
->framehack
= FALSE
;
874 source
->priv
->framehack
= TRUE
;
876 } else if (strcmp (values
[0], "base_url") ==0) {
877 source
->priv
->base_url
= g_strdup (values
[1]);
878 } else if (strcmp (values
[0], "base_path") ==0) {
879 source
->priv
->base_path
= g_strdup (values
[1]);
880 } else if (strcmp (values
[0], "update_url") ==0) {
881 source
->priv
->update_url
= g_strdup (values
[1]);
882 } else if (strcmp (values
[0], "banned") ==0) {
883 if (strcmp (values
[1], "0") ==0) {
884 source
->priv
->banned
= FALSE
;
886 source
->priv
->status
= BANNED
;
887 source
->priv
->banned
= TRUE
;
888 source
->priv
->connected
= FALSE
;
890 } else if (strcmp (values
[0], "response") == 0) {
891 if (strcmp (values
[1], "OK") == 0) {
892 source
->priv
->status
= OK
;
893 rb_debug ("Successfully communicated");
894 source
->priv
->connected
= TRUE
;
896 source
->priv
->connected
= FALSE
;
898 } else if (strcmp (values
[0], "stationname") == 0) {
899 gchar
**data
= g_strsplit (g_strdown(pieces
[i
- 1]), "=",2);
900 RhythmDBEntry
*entry
;
901 GValue titlestring
= {0,};
903 rb_debug ("Received station name from server: %s", values
[1]);
904 entry
= rhythmdb_entry_lookup_by_location (source
->priv
->db
, data
[1]);
905 g_value_init (&titlestring
, G_TYPE_STRING
);
906 g_value_set_string (&titlestring
, values
[1]);
909 entry
= rhythmdb_entry_new (source
->priv
->db
, source
->priv
->entry_type
, data
[1]);
910 rhythmdb_entry_set (source
->priv
->db
, entry
, RHYTHMDB_PROP_GENRE
, &titlestring
);
912 rhythmdb_entry_set (source
->priv
->db
, entry
, RHYTHMDB_PROP_GENRE
, &titlestring
);
914 g_value_unset (&titlestring
);
915 rhythmdb_commit (source
->priv
->db
);
924 if (source->priv->pending_entry) {
925 rb_shell_player_play_entry (source->priv->shell_player,
926 source->priv->pending_entry,
928 rhythmdb_entry_unref (source->priv->pending_entry);
929 source->priv->pending_entry = NULL;
933 rb_source_notify_status_changed (RB_SOURCE (source
));
934 g_object_unref (source
);
938 rb_lastfm_change_station (RBLastfmSource
*source
, const char *station
)
941 if (!source
->priv
->connected
) {
942 rb_lastfm_source_do_handshake (source
);
946 url
= g_strdup_printf("%s/radio/adjust.php?session=%s&url=%s&debug=0",
948 source
->priv
->session
,
951 g_object_ref (source
);
952 rb_lastfm_perform (source
, url
, NULL
, rb_lastfm_message_cb
);
957 rb_lastfm_proxy_config_changed_cb (RBProxyConfig
*config
,
958 RBLastfmSource
*source
)
962 if (source
->priv
->soup_session
) {
963 uri
= rb_proxy_config_get_libsoup_uri (config
);
964 g_object_set (G_OBJECT (source
->priv
->soup_session
),
973 rb_lastfm_source_new_station (char *uri
, char *title
, RBLastfmSource
*source
)
975 RhythmDBEntry
*entry
;
978 rb_debug ("adding lastfm: %s, %s",uri
, title
);
980 entry
= rhythmdb_entry_lookup_by_location (source
->priv
->db
, uri
);
982 rb_debug ("uri %s already in db", uri
);
986 entry
= rhythmdb_entry_new (source
->priv
->db
, source
->priv
->entry_type
, uri
);
987 g_value_init (&v
, G_TYPE_STRING
);
988 g_value_set_string (&v
, title
);
989 rhythmdb_entry_set (source
->priv
->db
, entry
, RHYTHMDB_PROP_GENRE
, &v
);
992 g_value_init (&v
, G_TYPE_DOUBLE
);
993 g_value_set_double (&v
, 0.0);
994 rhythmdb_entry_set (source
->priv
->db
, entry
, RHYTHMDB_PROP_RATING
, &v
);
996 rhythmdb_commit (source
->priv
->db
);
1000 rb_lastfm_source_dispose (GObject
*object
)
1002 RBLastfmSource
*source
;
1004 source
= RB_LASTFM_SOURCE (object
);
1006 if (source
->priv
->db
) {
1007 g_object_unref (source
->priv
->db
);
1008 source
->priv
->db
= NULL
;
1011 G_OBJECT_CLASS (rb_lastfm_source_parent_class
)->dispose (object
);
1015 rb_lastfm_source_command (RBLastfmSource
*source
, const char *query_string
, const char *status
)
1018 if (!source
->priv
->connected
) {
1019 rb_lastfm_source_do_handshake (source
);
1023 url
= g_strdup_printf ("%s/radio/control.php?session=%s&debug=0&%s",
1025 source
->priv
->session
,
1027 g_object_ref (source
);
1028 rb_lastfm_perform (source
, url
, NULL
, rb_lastfm_message_cb
);
1031 rb_source_notify_status_changed (RB_SOURCE (source
));
1035 rb_lastfm_source_love_track (GtkAction
*action
, RBLastfmSource
*source
)
1037 rb_lastfm_source_command (source
, "command=love", _("Marking song loved..."));
1041 rb_lastfm_source_skip_track (GtkAction
*action
, RBLastfmSource
*source
)
1043 rb_lastfm_source_command (source
, "command=skip", _("Skipping song..."));
1047 rb_lastfm_source_ban_track (GtkAction
*action
, RBLastfmSource
*source
)
1049 rb_lastfm_source_command (source
, "command=ban", _("Banning song..."));
1054 rb_lastfm_source_drag_cb (GtkWidget
*widget
,
1057 GtkSelectionData
*selection_data
,
1058 guint info
, guint time
,
1059 RBLastfmSource
*source
)
1061 impl_receive_drag (RB_SOURCE (source
) , selection_data
);
1065 impl_receive_drag (RBSource
*asource
, GtkSelectionData
*selection_data
)
1069 RBLastfmSource
*source
= RB_LASTFM_SOURCE (asource
);
1071 uri
= (char *)selection_data
->data
;
1072 rb_debug ("parsing uri %s", uri
);
1074 if (strstr (uri
, "lastfm://") == NULL
)
1077 title
= rb_lastfm_source_title_from_uri (uri
);
1079 rb_lastfm_source_new_station (uri
, title
, source
);
1085 rb_lastfm_source_title_from_uri (char *uri
)
1089 gchar
**data
= g_strsplit (uri
, "/", 0);
1091 if (strstr (uri
, "globaltags") != NULL
)
1092 title
= g_strdup_printf (_("Global Tag %s"), data
[3]);
1094 if (title
== NULL
&& strcmp (data
[2], "artist") == 0) {
1095 /* Check if the station is from an artist page, if not, it is a similar
1096 * artist station, and the server should return a name that change_station
1097 * will handle for us.
1099 if (data
[4] != NULL
) {
1100 if (strcmp (data
[4], "similarartists") == 0)
1101 title
= g_strdup_printf (_("Artists similar to %s"), data
[3]);
1102 if (strcmp (data
[4], "fans") == 0)
1103 title
= g_strdup_printf (_("Artists Liked by fans of %s"), data
[3]);
1108 if (title
== NULL
&& strcmp (data
[2], "user") == 0) {
1109 if (strcmp(data
[4], "neighbours") == 0)
1110 title
= g_strdup_printf (_("%s's Neighbour Radio"), data
[3]);
1111 if (strcmp(data
[4], "recommended") == 0)
1112 title
= g_strdup_printf (_("%s's Recommended Radio: %s percent"), data
[3], data
[5]);
1116 if (title
== NULL
) {
1117 title
= g_strstrip (uri
);
1121 unesc_title
= gnome_vfs_unescape_string (title
, NULL
);
1127 rb_lastfm_source_add_station_cb (GtkButton
*button
, gpointer
*data
)
1129 RBLastfmSource
*source
= RB_LASTFM_SOURCE (data
);
1134 add
= gtk_entry_get_text (GTK_ENTRY (source
->priv
->txtbox
));
1136 if (strcmp (gtk_combo_box_get_active_text (GTK_COMBO_BOX (source
->priv
->typecombo
)), "Artist") == 0) {
1137 uri
= g_strdup_printf ("lastfm://artist/%s/similarartists", add
);
1139 uri
= g_strdup_printf ("lastfm://globaltags/%s", add
);
1142 gtk_entry_set_text (GTK_ENTRY (source
->priv
->txtbox
), "");
1143 title
= rb_lastfm_source_title_from_uri (uri
);
1144 rb_lastfm_source_new_station (uri
, title
, source
);
1151 rb_lastfm_source_metadata_cb (SoupMessage
*req
, RBLastfmSource
*source
)
1156 RhythmDBEntry
*entry
;
1158 entry
= rb_shell_player_get_playing_entry (source
->priv
->shell_player
);
1159 if (entry
== NULL
|| rhythmdb_entry_get_entry_type (entry
) != source
->priv
->entry_type
) {
1160 rb_debug ("got response to metadata request, but not playing from this source");
1164 rb_debug ("got response to metadata request");
1165 body
= g_malloc0 ((req
->response
).length
+ 1);
1166 memcpy (body
, (req
->response
).body
, (req
->response
).length
);
1169 pieces
= g_strsplit (body
, "\n", 0);
1171 for (p
= 0; pieces
[p
] != NULL
; p
++) {
1175 values
= g_strsplit (pieces
[p
], "=", 2);
1176 if (strcmp (values
[0], "station") == 0) {
1177 } else if (strcmp (values
[0], "station_url") == 0) {
1178 } else if (strcmp (values
[0], "stationfeed") == 0) {
1179 } else if (strcmp (values
[0], "stationfeed_url") == 0) {
1180 } else if (strcmp (values
[0], "artist") == 0) {
1181 rb_debug ("artist -> %s", values
[1]);
1182 g_free (source
->priv
->streaming_artist
);
1183 source
->priv
->streaming_artist
= g_strdup (values
[1]);
1185 g_value_init (&v
, G_TYPE_STRING
);
1186 g_value_set_string (&v
, values
[1]);
1187 rhythmdb_emit_entry_extra_metadata_notify (source
->priv
->db
,
1189 RHYTHMDB_PROP_STREAM_SONG_ARTIST
,
1192 } else if (strcmp (values
[0], "album") == 0) {
1193 rb_debug ("album -> %s", values
[1]);
1194 g_free (source
->priv
->streaming_album
);
1195 source
->priv
->streaming_album
= g_strdup (values
[1]);
1197 g_value_init (&v
, G_TYPE_STRING
);
1198 g_value_set_string (&v
, values
[1]);
1199 rhythmdb_emit_entry_extra_metadata_notify (source
->priv
->db
,
1201 RHYTHMDB_PROP_STREAM_SONG_ALBUM
,
1204 } else if (strcmp (values
[0], "track") == 0) {
1205 rb_debug ("track -> %s", values
[1]);
1207 g_free (source
->priv
->streaming_title
);
1208 source
->priv
->streaming_title
= g_strdup (values
[1]);
1210 g_value_init (&v
, G_TYPE_STRING
);
1211 g_value_set_string (&v
, values
[1]);
1212 rhythmdb_emit_entry_extra_metadata_notify (source
->priv
->db
,
1214 RHYTHMDB_PROP_STREAM_SONG_TITLE
,
1217 } else if (strcmp (values
[0], "albumcover_small") == 0) {
1218 } else if (strcmp (values
[0], "albumcover_medium") == 0) {
1219 } else if (strcmp (values
[0], "albumcover_large") == 0) {
1220 } else if (strcmp (values
[0], "trackprogress") == 0) {
1221 } else if (strcmp (values
[0], "trackduration") == 0) {
1222 } else if (strcmp (values
[0], "artist_url") == 0) {
1223 } else if (strcmp (values
[0], "album_url") == 0) {
1224 } else if (strcmp (values
[0], "track_url") == 0) {
1225 } else if (strcmp (values
[0], "discovery") == 0) {
1227 rb_debug ("got unknown value: %s", values
[0]);
1230 g_strfreev (values
);
1233 g_strfreev (pieces
);
1236 source
->priv
->status
= OK
;
1237 rb_source_notify_status_changed (RB_SOURCE (source
));
1241 rb_lastfm_source_new_song_cb (GObject
*player_backend
,
1243 RBLastfmSource
*source
)
1246 rb_debug ("got new song");
1248 uri
= g_strdup_printf ("http://%s%s/np.php?session=%s&debug=0",
1249 source
->priv
->base_url
,
1250 source
->priv
->base_path
,
1251 source
->priv
->session
);
1252 rb_lastfm_perform (source
, uri
, NULL
, (SoupMessageCallbackFn
) rb_lastfm_source_metadata_cb
);
1257 check_entry_type (RBLastfmSource
*source
, RhythmDBEntry
*entry
)
1259 RhythmDBEntryType entry_type
;
1260 gboolean matches
= FALSE
;
1262 g_object_get (source
, "entry-type", &entry_type
, NULL
);
1263 if (entry
!= NULL
&& rhythmdb_entry_get_entry_type (entry
) == entry_type
)
1265 g_boxed_free (RHYTHMDB_TYPE_ENTRY_TYPE
, entry_type
);
1271 rb_lastfm_song_changed_cb (RBShellPlayer
*player
,
1272 RhythmDBEntry
*entry
,
1273 RBLastfmSource
*source
)
1275 const char *location
;
1277 g_free (source
->priv
->streaming_title
);
1278 g_free (source
->priv
->streaming_album
);
1279 g_free (source
->priv
->streaming_artist
);
1280 source
->priv
->streaming_title
= NULL
;
1281 source
->priv
->streaming_album
= NULL
;
1282 source
->priv
->streaming_artist
= NULL
;
1284 if (check_entry_type (source
, entry
)) {
1285 location
= rhythmdb_entry_get_string (entry
, RHYTHMDB_PROP_LOCATION
);
1286 /* this bit doesn't work */
1288 if (!source->priv->connected) {
1289 rb_lastfm_source_do_handshake (source);
1290 source->priv->pending_entry = rhythmdb_entry_ref (entry);
1291 rb_debug ("will play station %s once connected", location);
1293 rb_debug ("switching to station %s", location);
1294 rb_lastfm_change_station (source, location);
1297 rb_lastfm_change_station (source
, location
);
1299 rb_debug ("non-lastfm entry being played");
1304 streaming_title_request_cb (RhythmDB
*db
,
1305 RhythmDBEntry
*entry
,
1306 RBLastfmSource
*source
)
1309 if (check_entry_type (source
, entry
) == FALSE
||
1310 entry
!= rb_shell_player_get_playing_entry (source
->priv
->shell_player
) ||
1311 source
->priv
->streaming_title
== NULL
)
1314 rb_debug ("returning streaming title \"%s\" to extra metadata request", source
->priv
->streaming_title
);
1315 value
= g_new0 (GValue
, 1);
1316 g_value_init (value
, G_TYPE_STRING
);
1317 g_value_set_string (value
, source
->priv
->streaming_title
);
1322 streaming_artist_request_cb (RhythmDB
*db
,
1323 RhythmDBEntry
*entry
,
1324 RBLastfmSource
*source
)
1328 if (check_entry_type (source
, entry
) == FALSE
||
1329 entry
!= rb_shell_player_get_playing_entry (source
->priv
->shell_player
) ||
1330 source
->priv
->streaming_artist
== NULL
)
1333 rb_debug ("returning streaming artist \"%s\" to extra metadata request", source
->priv
->streaming_artist
);
1334 value
= g_new0 (GValue
, 1);
1335 g_value_init (value
, G_TYPE_STRING
);
1336 g_value_set_string (value
, source
->priv
->streaming_artist
);
1341 streaming_album_request_cb (RhythmDB
*db
,
1342 RhythmDBEntry
*entry
,
1343 RBLastfmSource
*source
)
1347 if (check_entry_type (source
, entry
) == FALSE
||
1348 entry
!= rb_shell_player_get_playing_entry (source
->priv
->shell_player
) ||
1349 source
->priv
->streaming_artist
== NULL
)
1352 rb_debug ("returning streaming album \"%s\" to extra metadata request", source
->priv
->streaming_album
);
1353 value
= g_new0 (GValue
, 1);
1354 g_value_init (value
, G_TYPE_STRING
);
1355 g_value_set_string (value
, source
->priv
->streaming_album
);
1360 extra_metadata_gather_cb (RhythmDB
*db
,
1361 RhythmDBEntry
*entry
,
1363 RBLastfmSource
*source
)
1365 /* our extra metadata only applies to the playing entry */
1366 if (entry
!= rb_shell_player_get_playing_entry (source
->priv
->shell_player
) ||
1367 check_entry_type (source
, entry
) == FALSE
)
1370 if (source
->priv
->streaming_title
!= NULL
) {
1373 value
= g_new0 (GValue
, 1);
1374 g_value_init (value
, G_TYPE_STRING
);
1375 g_value_set_string (value
, source
->priv
->streaming_title
);
1376 g_hash_table_insert (data
, g_strdup (RHYTHMDB_PROP_STREAM_SONG_TITLE
), value
);
1379 if (source
->priv
->streaming_artist
!= NULL
) {
1382 value
= g_new0 (GValue
, 1);
1383 g_value_init (value
, G_TYPE_STRING
);
1384 g_value_set_string (value
, source
->priv
->streaming_artist
);
1385 g_hash_table_insert (data
, g_strdup (RHYTHMDB_PROP_STREAM_SONG_ARTIST
), value
);
1388 if (source
->priv
->streaming_album
!= NULL
) {
1391 value
= g_new0 (GValue
, 1);
1392 g_value_init (value
, G_TYPE_STRING
);
1393 g_value_set_string (value
, source
->priv
->streaming_album
);
1394 g_hash_table_insert (data
, g_strdup (RHYTHMDB_PROP_STREAM_SONG_ALBUM
), value
);
1399 impl_activate (RBSource
*source
)
1401 rb_lastfm_source_do_handshake (RB_LASTFM_SOURCE (source
));
1405 buffering_cb (GObject
*backend
, guint progress
, RBLastfmSource
*source
)
1410 if (progress
== 100)
1413 rb_debug ("buffer at %d%%", progress
);
1415 GDK_THREADS_ENTER ();
1416 source
->priv
->buffering
= progress
;
1417 rb_source_notify_status_changed (RB_SOURCE (source
));
1418 GDK_THREADS_LEAVE ();
1422 playing_source_changed_cb (RBShellPlayer
*player
,
1424 RBLastfmSource
*lastfm_source
)
1428 g_object_get (player
, "player", &backend
, NULL
);
1430 if (source
== RB_SOURCE (lastfm_source
)) {
1431 rb_debug ("connecting buffering signal handler");
1432 if (lastfm_source
->priv
->buffering_id
== 0) {
1433 lastfm_source
->priv
->buffering_id
=
1434 g_signal_connect_object (backend
, "buffering",
1435 G_CALLBACK (buffering_cb
),
1438 /* display 'connecting' status until we get a buffering message */
1439 lastfm_source
->priv
->buffering
= -1;
1440 rb_source_notify_status_changed (RB_SOURCE (lastfm_source
));
1441 } else if (lastfm_source
->priv
->buffering_id
> 0) {
1442 rb_debug ("disconnecting buffering signal handler");
1443 g_signal_handler_disconnect (backend
, lastfm_source
->priv
->buffering_id
);
1444 lastfm_source
->priv
->buffering_id
= 0;
1446 lastfm_source
->priv
->buffering
= -1;
1447 rb_source_notify_status_changed (RB_SOURCE (lastfm_source
));
1450 g_object_unref (backend
);