[banshee] Migrate to the Last.fm 2.0 API (BGO#541227)



commit 874b0d0a8288a109fcbe5f3b5d2dcec5614dcc64
Author: Bertrand Lorentz <bertrand lorentz gmail com>
Date:   Mon Nov 9 21:16:01 2009 +0100

    Migrate to the Last.fm 2.0 API (BGO#541227)
    
    This commit upgrades the Lastfm to use the 2.0 API, as the older API was
    disabled a while back. Radio streams are accessible again, under
    restrictions imposed by Last.fm : paying subscribers and people
    living the US, UK or Germany.
    
    Major changes are :
    - Web services style API returning JSON, handled in the new
    LastfmRequest class.
    - New authentication scheme. The user's doesn't have to give us
    his password, but has to authorize Banshee to access his account.
    - Scrobbling uses this authentication scheme, moving it to the 1.2.1
    protocol.
    The Lastfm.Data namespace is not impacted.

 .../AudioscrobblerService.cs                       |    9 +-
 .../Banshee.Lastfm.Radio/LastfmSource.cs           |   22 +-
 .../Banshee.Lastfm.Radio/StationEditor.cs          |    2 +-
 src/Libraries/Lastfm.Gui/Lastfm.Gui.csproj         |    4 +
 .../Lastfm.Gui/Lastfm.Gui/AccountLoginDialog.cs    |   11 +-
 .../Lastfm.Gui/Lastfm.Gui/AccountLoginForm.cs      |   80 ++---
 src/Libraries/Lastfm/Lastfm.csproj                 |    1 +
 src/Libraries/Lastfm/Lastfm/Account.cs             |   79 +++-
 .../Lastfm/Lastfm/AudioscrobblerConnection.cs      |   21 +-
 src/Libraries/Lastfm/Lastfm/LastfmCore.cs          |   14 +
 src/Libraries/Lastfm/Lastfm/LastfmRequest.cs       |  261 +++++++++++++
 src/Libraries/Lastfm/Lastfm/RadioConnection.cs     |  412 +++++---------------
 src/Libraries/Lastfm/Makefile.am                   |    1 +
 13 files changed, 517 insertions(+), 400 deletions(-)
---
diff --git a/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs b/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs
index 6813f6d..d3baed9 100644
--- a/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs
+++ b/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs
@@ -81,7 +81,7 @@ namespace Banshee.Lastfm.Audioscrobbler
             
             if (account.UserName == null) {
                 account.UserName = LastUserSchema.Get ();
-                account.CryptedPassword = LastPassSchema.Get ();
+                account.SessionKey = LastSessionKeySchema.Get ();
                 account.ScrobbleUrl = LastScrobbleUrlSchema.Get ();
             }
             
@@ -103,8 +103,7 @@ namespace Banshee.Lastfm.Audioscrobbler
             
             // Update the Visit action menu item if we update our account info
             LastfmCore.Account.Updated += delegate (object o, EventArgs args) {
-                actions["AudioscrobblerVisitAction"].Sensitive = LastfmCore.Account.UserName != null 
-                    && LastfmCore.Account.CryptedPassword != null;
+                actions["AudioscrobblerVisitAction"].Sensitive = String.IsNullOrEmpty (LastfmCore.Account.UserName);
             };
             
             ServiceManager.PlayerEngine.ConnectEvent (OnPlayerEvent, 
@@ -315,8 +314,8 @@ namespace Banshee.Lastfm.Audioscrobbler
             "plugins.lastfm", "username", "", "Last.fm user", "Last.fm username"
         );
 
-        public static readonly SchemaEntry<string> LastPassSchema = new SchemaEntry<string> (
-            "plugins.lastfm", "password_hash", "", "Last.fm password", "Last.fm password (hashed)"
+        public static readonly SchemaEntry<string> LastSessionKeySchema = new SchemaEntry<string> (
+            "plugins.lastfm", "session_key", "", "Last.fm session key", "Last.fm sessions key used in authenticated calls"
         );
    
         public static readonly SchemaEntry<string> LastScrobbleUrlSchema = new SchemaEntry<string> (
diff --git a/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/LastfmSource.cs b/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/LastfmSource.cs
index e8685a4..fa4ab92 100644
--- a/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/LastfmSource.cs
+++ b/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/LastfmSource.cs
@@ -76,7 +76,8 @@ namespace Banshee.Lastfm.Radio
             // username we used so we can load the user's stations.
             if (account.UserName != null) {
                 account.UserName = LastUserSchema.Get ();
-                account.CryptedPassword = LastPassSchema.Get ();
+                account.SessionKey = LastSessionKeySchema.Get ();
+                account.Subscriber = LastIsSubscriberSchema.Get ();
             }
 
             if (LastfmCore.UserAgent == null) {
@@ -169,9 +170,9 @@ namespace Banshee.Lastfm.Radio
         private bool last_was_subscriber = false;
         public void SetUserName (string username)
         {
-            if (username != last_username || last_was_subscriber != Connection.Subscriber) {
+            if (username != last_username || last_was_subscriber != Account.Subscriber) {
                 last_username = username;
-                last_was_subscriber = Connection.Subscriber;
+                last_was_subscriber = Account.Subscriber;
                 LastfmSource.LastUserSchema.Set (last_username);
                 ClearChildSources ();
                 PauseSorting ();
@@ -216,10 +217,11 @@ namespace Banshee.Lastfm.Radio
         private void UpdateUI ()
         {
             bool have_user = Account.UserName != null;
-            bool have_pass = Account.CryptedPassword != null;
+            bool have_session_key = Account.SessionKey != null;
             
-            if (have_pass) {
-                LastPassSchema.Set (Account.CryptedPassword);
+            if (have_session_key) {
+                LastSessionKeySchema.Set (Account.SessionKey);
+                LastIsSubscriberSchema.Set (Account.Subscriber);
             }
             
             if (have_user) {
@@ -269,8 +271,12 @@ namespace Banshee.Lastfm.Radio
             "plugins.lastfm", "username", "", "Last.fm user", "Last.fm username"
         );
 
-        public static readonly SchemaEntry<string> LastPassSchema = new SchemaEntry<string> (
-            "plugins.lastfm", "password_hash", "", "Last.fm password", "Last.fm password (hashed)"
+        public static readonly SchemaEntry<string> LastSessionKeySchema = new SchemaEntry<string> (
+            "plugins.lastfm", "session_key", "", "Last.fm session key", "Last.fm session key used in authenticated calls"
+        );
+
+        public static readonly SchemaEntry<bool> LastIsSubscriberSchema = new SchemaEntry<bool> (
+            "plugins.lastfm", "subscriber", false, "User is Last.fm subscriber", "User is Last.fm subscriber"
         );
 
         public static readonly SchemaEntry<bool> ExpandedSchema = new SchemaEntry<bool> (
diff --git a/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/StationEditor.cs b/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/StationEditor.cs
index 0533a66..d30ed88 100644
--- a/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/StationEditor.cs
+++ b/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/StationEditor.cs
@@ -89,7 +89,7 @@ namespace Banshee.Lastfm.Radio
             int active_type = 0;
             int i = 0;
             foreach (StationType type in StationType.Types) {
-                if (!type.SubscribersOnly || lastfm.Connection.Subscriber) {
+                if (!type.SubscribersOnly || lastfm.Account.Subscriber) {
                     type_combo.AppendText (type.Label);
                     if (source != null && type == source.Type) {
                         active_type = i;
diff --git a/src/Libraries/Lastfm.Gui/Lastfm.Gui.csproj b/src/Libraries/Lastfm.Gui/Lastfm.Gui.csproj
index 8866c70..2d01bf2 100644
--- a/src/Libraries/Lastfm.Gui/Lastfm.Gui.csproj
+++ b/src/Libraries/Lastfm.Gui/Lastfm.Gui.csproj
@@ -45,6 +45,10 @@
       <Project>{C1F63FC5-4B96-48B2-B7F7-5B33FCC4F2A2}</Project>
       <Name>Lastfm</Name>
     </ProjectReference>
+    <ProjectReference Include="..\Hyena\Hyena.csproj">
+      <Project>{95374549-9553-4C1E-9D89-667755F90E12}</Project>
+      <Name>Hyena</Name>
+    </ProjectReference>
   </ItemGroup>
   <ItemGroup>
     <Compile Include="Lastfm.Gui\AccountLoginDialog.cs" />
diff --git a/src/Libraries/Lastfm.Gui/Lastfm.Gui/AccountLoginDialog.cs b/src/Libraries/Lastfm.Gui/Lastfm.Gui/AccountLoginDialog.cs
index 06ea853..d6ef79d 100644
--- a/src/Libraries/Lastfm.Gui/Lastfm.Gui/AccountLoginDialog.cs
+++ b/src/Libraries/Lastfm.Gui/Lastfm.Gui/AccountLoginDialog.cs
@@ -85,6 +85,7 @@ namespace Lastfm.Gui
         
             login_form = new AccountLoginForm (account);
             login_form.AddSignUpButton ();
+            login_form.AddAuthorizeButton ();
             login_form.Show ();
             
             vbox.PackStart (login_form, true, true, 0);
@@ -112,7 +113,6 @@ namespace Lastfm.Gui
                     login_form.Save ();
                 };
                 AddActionWidget (button, ResponseType.Ok);
-                login_form.SaveOnEnter (this);
             }
         }
         
@@ -132,6 +132,11 @@ namespace Lastfm.Gui
             login_form.AddSignUpButton ();
         }
         
+        public void AddAuthorizeButton ()
+        {
+            login_form.AddAuthorizeButton ();
+        }
+        
         public string Message {
             get { return message.Text; }
             set { message.Text = value; }
@@ -145,9 +150,5 @@ namespace Lastfm.Gui
         public string Username {
             get { return login_form.Username; }
         }
-        
-        public string Password {
-            get { return login_form.Password; }
-        }
     }
 }
diff --git a/src/Libraries/Lastfm.Gui/Lastfm.Gui/AccountLoginForm.cs b/src/Libraries/Lastfm.Gui/Lastfm.Gui/AccountLoginForm.cs
index 00c04cc..ddb6e43 100644
--- a/src/Libraries/Lastfm.Gui/Lastfm.Gui/AccountLoginForm.cs
+++ b/src/Libraries/Lastfm.Gui/Lastfm.Gui/AccountLoginForm.cs
@@ -38,15 +38,12 @@ namespace Lastfm.Gui
     {
         private Account account;
         private Entry username_entry;
-        private Entry password_entry;
         private LinkButton signup_button;
-        
+        private Button authorize_button;
+
         private bool save_on_edit = false;
         
-        private bool save_on_enter = false;
-        private Gtk.Dialog parentDialog;
-
-        public AccountLoginForm (Account account) : base (2, 2, false)
+        public AccountLoginForm (Account account) : base (1, 2, false)
         {
             this.account = account;
 
@@ -61,37 +58,13 @@ namespace Lastfm.Gui
             username_entry = new Entry ();
             username_entry.Show ();
             
-            Label password_label = new Label (Catalog.GetString ("Password:"));
-            password_label.Xalign = 1.0f;
-            password_label.Show ();
-            
-            password_entry = new Entry ();
-            password_entry.Visibility = false;
-
-            //When the user presses enter in the password field: save, and then destroy the AcountLoginDialog
-            password_entry.Activated += delegate {
-                    if (save_on_enter) {
-                        this.Save ();
-                        parentDialog.Destroy ();
-                    }
-                };
-            
-            password_entry.Show ();
-            
             Attach (username_label, 0, 1, 0, 1, AttachOptions.Fill, 
                 AttachOptions.Shrink, 0, 0);
             
             Attach (username_entry, 1, 2, 0, 1, AttachOptions.Fill | AttachOptions.Expand, 
                 AttachOptions.Shrink, 0, 0);
             
-            Attach (password_label, 0, 1, 1, 2, AttachOptions.Fill, 
-                AttachOptions.Shrink, 0, 0);
-            
-            Attach (password_entry, 1, 2, 1, 2, AttachOptions.Fill | AttachOptions.Expand, 
-                AttachOptions.Shrink, 0, 0);
-                
             username_entry.Text = account.UserName ?? String.Empty;
-            password_entry.Text = account.Password ?? String.Empty;
         }
         
         protected override void OnDestroyed ()
@@ -109,17 +82,45 @@ namespace Lastfm.Gui
                 return;
             }
             
-            Resize (3, 2);
+            Resize (2, 2);
             signup_button = new LinkButton (account.SignUpUrl, Catalog.GetString ("Sign up for Last.fm"));
             signup_button.Show ();
-            Attach (signup_button, 1, 2, 2, 3, AttachOptions.Shrink, AttachOptions.Shrink, 0, 0);
+            Attach (signup_button, 1, 2, 1, 2, AttachOptions.Shrink, AttachOptions.Shrink, 0, 0);
+        }
+        
+        public void AddAuthorizeButton ()
+        {
+            if (authorize_button != null) {
+                return;
+            }
+            
+            Resize (3, 2);
+            authorize_button = new Button (Catalog.GetString ("Authorize for Last.fm"));
+            authorize_button.Clicked += OnAuthorize;
+            authorize_button.Show ();
+            Attach (authorize_button, 1, 2, 2, 3, AttachOptions.Shrink, AttachOptions.Shrink, 0, 0);
+        }
+
+        private void OnAuthorize (object o, EventArgs args)
+        {
+            account.SessionKey = null;
+            account.RequestAuthorization ();
         }
         
         public void Save ()
         {
-            if (account.UserName != username_entry.Text.Trim () || account.Password != password_entry.Text.Trim ()) {
+            bool is_modified = false;
+            
+            if (account.UserName != username_entry.Text.Trim ()) {
                 account.UserName = username_entry.Text.Trim ();
-                account.Password = password_entry.Text.Trim ();
+                is_modified = true;
+            }
+            if (account.SessionKey == null) {
+                account.FetchSessionKey ();
+                is_modified = true;
+            }
+            
+            if (is_modified) {
                 account.Save ();
             }
         }
@@ -129,20 +130,9 @@ namespace Lastfm.Gui
             set { save_on_edit = value; }
         }
         
-        //enable save on Enter and destruction of the parentDialog.
-        public void SaveOnEnter (Gtk.Dialog parentDialog) 
-        {
-           save_on_enter = true;
-           this.parentDialog = parentDialog;
-        }
-                
         public string Username {
             get { return username_entry.Text; }
         }
-        
-        public string Password {
-            get { return password_entry.Text; }
-        }
     }
 }
 
diff --git a/src/Libraries/Lastfm/Lastfm.csproj b/src/Libraries/Lastfm/Lastfm.csproj
index f33c7ac..b84de28 100644
--- a/src/Libraries/Lastfm/Lastfm.csproj
+++ b/src/Libraries/Lastfm/Lastfm.csproj
@@ -67,6 +67,7 @@
     <Compile Include="Lastfm.Data\LastfmData.cs" />
     <Compile Include="Lastfm.Data\LastfmUserData.cs" />
     <Compile Include="Lastfm.Data\LastfmAlbumData.cs" />
+    <Compile Include="Lastfm\LastfmRequest.cs" />
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <ProjectExtensions>
diff --git a/src/Libraries/Lastfm/Lastfm/Account.cs b/src/Libraries/Lastfm/Lastfm/Account.cs
index de82369..a0f6cfd 100644
--- a/src/Libraries/Lastfm/Lastfm/Account.cs
+++ b/src/Libraries/Lastfm/Lastfm/Account.cs
@@ -31,39 +31,33 @@ using System;
 using System.Collections;
 using System.Text;
 
+using Hyena;
+
 namespace Lastfm
 {
     public class Account
-    {    
+    {
         public event EventHandler Updated;
 
+        // Only used during the authentication process
+        private string authentication_token;
+        
         private string username;
         public string UserName {
             get { return username; }
             set { username = value; }
         }
 
-        private string password; 
-        public string Password {
-            get { return password; }
-            set { password = value; }
+        private string session_key;
+        public string SessionKey {
+            get { return session_key; }
+            set { session_key = value; }
         }
 
-        public string CryptedPassword {
-            get {
-                // Okay, so this will explode if someone has a raw text password 
-                // that matches ^[a-f0-9]{32}$ ... likely? I hope not.
-            
-                if (password == null) {
-                    return null;
-                } else if (Hyena.CryptoUtil.IsMd5Encoded (password)) {
-                    return password;
-                }
-                
-                password = Hyena.CryptoUtil.Md5Encode (password);
-                return password;
-            }
-            set { password = String.IsNullOrEmpty (value) ? null : value; }
+        private bool subscriber;
+        public bool Subscriber {
+            get { return subscriber; }
+            set { subscriber = value; }
         }
 
         private string scrobble_url;
@@ -100,6 +94,51 @@ namespace Lastfm
             OnUpdated ();
         }
 
+        public StationError RequestAuthorization ()
+        {
+            LastfmRequest get_token = new LastfmRequest ("auth.getToken", RequestType.Read, ResponseFormat.Json);
+            get_token.Send ();
+            
+            var response = get_token.GetResponseObject ();
+            object error_code;
+            if (response.TryGetValue ("error", out error_code)) {
+                Log.WarningFormat ("Lastfm error {0} : {1}", (int)error_code, (string)response["message"]);
+                return (StationError) error_code;
+            }
+            
+            authentication_token = (string)response["token"];
+            Browser.Open (String.Format ("http://www.last.fm/api/auth/?api_key={0}&token={1}";, LastfmCore.ApiKey, authentication_token));
+
+            return StationError.None;
+        }
+
+        public StationError FetchSessionKey ()
+        {
+            if (authentication_token == null) {
+                throw new InvalidOperationException ("RequestAuthorization should be called before calling FetchSessionKey");
+            }
+            
+            LastfmRequest get_session = new LastfmRequest ("auth.getSession", RequestType.SessionRequest, ResponseFormat.Json);
+            get_session.AddParameter ("token", authentication_token);
+            get_session.Send ();
+            var response = get_session.GetResponseObject ();
+            object error_code;
+            if (response.TryGetValue ("error", out error_code)) {
+                Log.WarningFormat ("Lastfm error {0} : {1}", (int)error_code, (string)response["message"]);
+                return (StationError) error_code;
+            }
+            
+            var session = (Hyena.Json.JsonObject)response["session"];
+            UserName = (string)session["name"];
+            SessionKey = (string)session["key"];
+            Subscriber = session["subscriber"].ToString ().Equals ("1");
+
+            // The authentication token is only valid once, and for a limited time
+            authentication_token = null;
+            
+            return StationError.None;
+        }
+        
         protected void OnUpdated ()
         {
             EventHandler handler = Updated;
diff --git a/src/Libraries/Lastfm/Lastfm/AudioscrobblerConnection.cs b/src/Libraries/Lastfm/Lastfm/AudioscrobblerConnection.cs
index 3ed2cca..34d0aa3 100644
--- a/src/Libraries/Lastfm/Lastfm/AudioscrobblerConnection.cs
+++ b/src/Libraries/Lastfm/Lastfm/AudioscrobblerConnection.cs
@@ -60,7 +60,7 @@ namespace Lastfm
         private const string CLIENT_ID = "bsh";
         private const string CLIENT_VERSION = "0.1";
         private const string SCROBBLER_URL = "http://post.audioscrobbler.com/";;
-        private const string SCROBBLER_VERSION = "1.2";
+        private const string SCROBBLER_VERSION = "1.2.1";
 
         private string post_url;
         private string session_id = null;
@@ -184,7 +184,7 @@ namespace Lastfm
             switch (state) {
             case State.Idle:
                 if (LastfmCore.Account.UserName != null &&
-                    LastfmCore.Account.CryptedPassword != null && session_id == null) {
+                    LastfmCore.Account.SessionKey != null && session_id == null) {
                     
                     state = State.NeedHandshake;
                 } else {
@@ -383,8 +383,8 @@ namespace Lastfm
         private void Handshake ()
         {
             string timestamp = UnixTime();
-            string security_token = Hyena.CryptoUtil.Md5Encode
-                (LastfmCore.Account.CryptedPassword + timestamp);
+            string authentication_token = Hyena.CryptoUtil.Md5Encode
+                (LastfmCore.ApiSecret + timestamp);
             
             string api_url = LastfmCore.Account.ScrobbleUrl;
             if (String.IsNullOrEmpty (api_url)) {
@@ -393,13 +393,15 @@ namespace Lastfm
                 Log.DebugFormat ("Scrobbling to non-standard API URL: {0}", api_url);
             }
 
-            string uri = String.Format ("{0}?hs=true&p={1}&c={2}&v={3}&u={4}&t={5}&a={6}",
+            string uri = String.Format ("{0}?hs=true&p={1}&c={2}&v={3}&u={4}&t={5}&a={6}&api_key={7}&sk={8}",
                                         api_url,
                                         SCROBBLER_VERSION,
                                         CLIENT_ID, CLIENT_VERSION,
                                         HttpUtility.UrlEncode (LastfmCore.Account.UserName),
                                         timestamp,
-                                        security_token);
+                                        authentication_token,
+                                        LastfmCore.ApiKey,
+                                        LastfmCore.Account.SessionKey);
 
             current_web_req = (HttpWebRequest) WebRequest.Create (uri);
 
@@ -443,8 +445,8 @@ namespace Lastfm
                 Log.Warning ("Audioscrobbler sign-on failed", "Player is banned", false);
                                    
             } else if (line.StartsWith ("BADAUTH")) {
-                Log.Warning ("Audioscrobbler sign-on failed", Catalog.GetString ("Last.fm username or password is invalid."));
-                LastfmCore.Account.CryptedPassword = null;
+                Log.Warning ("Audioscrobbler sign-on failed", Catalog.GetString ("Last.fm username is invalid or Banshee is not authorized to access you account."));
+                LastfmCore.Account.SessionKey = null;
             } else if (line.StartsWith ("BADTIME")) {
                 Log.Warning ("Audioscrobbler sign-on failed", 
                                                   "timestamp provided was not close enough to the current time", false);
@@ -518,7 +520,6 @@ namespace Lastfm
                                         str_track_number,
                                         mbrainzid);
 
-            Console.WriteLine ("Submitting via non-uri handler.");            
             NowPlaying (data);
         }
         
@@ -621,7 +622,7 @@ namespace Lastfm
                     String.Format("Failed to post NowPlaying: {0}", e), false);
             }
             
-            // NowPlaying error/success is non-crutial.
+            // NowPlaying error/success is non-crucial.
             hard_failures++;
             if (hard_failures < 3) {
                 NowPlaying (current_now_playing_data);
diff --git a/src/Libraries/Lastfm/Lastfm/LastfmCore.cs b/src/Libraries/Lastfm/Lastfm/LastfmCore.cs
index 7328210..d7b7f64 100644
--- a/src/Libraries/Lastfm/Lastfm/LastfmCore.cs
+++ b/src/Libraries/Lastfm/Lastfm/LastfmCore.cs
@@ -32,6 +32,20 @@ namespace Lastfm
 {
     public static class LastfmCore
     {
+        // The default API key and secret are for a "Non-commercial Use" API account.
+        // See http://www.last.fm/api/account for more information.
+        private static string api_key = "344e9141fffeb02201e1ae455d92ae9f";
+        public static string ApiKey {
+            get { return api_key; }
+            set { api_key = value; }
+        }
+
+        private static string api_secret = "af3f4459eebbe1bde84fa9f8cf1a75fb";
+        internal static string ApiSecret {
+            get { return api_secret; }
+            set { api_secret = value; }
+        }
+
         private static Account account;
         public static Account Account {
             get {
diff --git a/src/Libraries/Lastfm/Lastfm/LastfmRequest.cs b/src/Libraries/Lastfm/Lastfm/LastfmRequest.cs
new file mode 100644
index 0000000..1f479aa
--- /dev/null
+++ b/src/Libraries/Lastfm/Lastfm/LastfmRequest.cs
@@ -0,0 +1,261 @@
+//
+// LastfmRequest.cs
+//
+// Authors:
+//   Bertrand Lorentz <bertrand lorentz gmail com>
+//
+// Copyright (C) 2009 Bertrand Lorentz
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Web;
+
+using Hyena;
+using Hyena.Json;
+
+namespace Lastfm
+{
+    public enum RequestType {
+        Read,
+        SessionRequest, // Needs the signature, but we don't have the session key yet
+        AuthenticatedRead,
+        Write
+    }
+
+    public enum ResponseFormat {
+        Json,
+        Raw
+    }
+    
+    public class LastfmRequest
+    {
+        private const string API_ROOT = "http://ws.audioscrobbler.com/2.0/";;
+        
+        private Dictionary<string, string> parameters = new Dictionary<string, string> ();
+        private Stream response_stream;
+
+        public LastfmRequest ()
+        {}
+
+        public LastfmRequest (string method) : this (method, RequestType.Read, ResponseFormat.Json)
+        {}
+        
+        public LastfmRequest (string method, RequestType request_type, ResponseFormat response_format)
+        {
+            this.method = method;
+            this.request_type = request_type;
+            this.response_format = response_format;
+        }
+
+        private string method;
+        public string Method { get; set; }
+
+        
+        private RequestType request_type;
+        public RequestType RequestType { get; set; }
+
+        
+        private ResponseFormat response_format;
+        public ResponseFormat ResponseFormat { get; set; }
+
+        
+        public void AddParameter (string param_name, string param_value)
+        {
+            parameters.Add (param_name, param_value);
+        }
+
+        public Stream GetResponseStream ()
+        {
+            return response_stream;
+        }
+
+        public void Send ()
+        {
+            if (method == null) {
+                throw new InvalidOperationException ("The method name should be set");
+            }
+            
+            if (response_format == ResponseFormat.Json) {
+                AddParameter ("format", "json");
+            } else if (response_format == ResponseFormat.Raw) {
+                AddParameter ("raw", "true");
+            }
+
+            if (request_type == RequestType.Write) {
+                response_stream = Post (API_ROOT, BuildPostData ());
+            } else {
+                response_stream = Get (BuildGetUrl ());
+            }
+        }
+        
+        public JsonObject GetResponseObject ()
+        {
+            Deserializer deserializer = new Deserializer (response_stream);
+            object obj = deserializer.Deserialize ();
+            JsonObject json_obj = obj as Hyena.Json.JsonObject;
+
+            if (json_obj == null) {
+                throw new ApplicationException ("Lastfm invalid response : not a JSON object");
+            }
+            
+            return json_obj;
+        }
+
+        public StationError GetError ()
+        {
+            StationError error = StationError.None;
+            
+            string response;
+            using (StreamReader sr = new StreamReader (response_stream)) {
+                response = sr.ReadToEnd ();
+            }
+            
+            if (response.Contains ("<lfm status=\"failed\">")) {
+                // XML reply indicates an error
+                Match match = Regex.Match (response, "<error code=\"(\\d+)\">");
+                if (match.Success) {
+                    error = (StationError) Int32.Parse (match.Value);
+                    Log.WarningFormat ("Lastfm error {0}", error);
+                } else {
+                    error = StationError.Unknown;
+                }
+            }
+            if (response_format == ResponseFormat.Json && response.Contains ("\"error\":")) {
+                // JSON reply indicates an error
+                Deserializer deserializer = new Deserializer (response);
+                JsonObject json = deserializer.Deserialize () as JsonObject;
+                if (json != null && json.ContainsKey ("error")) {
+                    error = (StationError) json["error"];
+                    Log.WarningFormat ("Lastfm error {0} : {1}", error, (string)json["message"]);
+                }
+            }
+
+            return error;
+        }
+        
+        private string BuildGetUrl ()
+        {
+            if (request_type == RequestType.AuthenticatedRead) {
+                parameters.Add ("sk", LastfmCore.Account.SessionKey);
+            }
+            
+            StringBuilder url = new StringBuilder (API_ROOT);
+            url.AppendFormat ("?method={0}", method);
+            url.AppendFormat ("&api_key={0}", LastfmCore.ApiKey);
+            foreach (KeyValuePair<string, string> param in parameters) {
+                url.AppendFormat ("&{0}={1}", param.Key, Uri.EscapeDataString (param.Value));
+            }
+            if (request_type == RequestType.AuthenticatedRead || request_type == RequestType.SessionRequest) {
+                url.AppendFormat ("&api_sig={0}", GetSignature ());
+            }
+            
+            return url.ToString ();
+        }
+
+        private string BuildPostData ()
+        {
+            parameters.Add ("sk", LastfmCore.Account.SessionKey);
+
+            StringBuilder data = new StringBuilder ();
+            data.AppendFormat ("method={0}", method);
+            data.AppendFormat ("&api_key={0}", LastfmCore.ApiKey);
+            foreach (KeyValuePair<string, string> param in parameters) {
+                data.AppendFormat ("&{0}={1}", param.Key, Uri.EscapeDataString (param.Value));
+            }
+            data.AppendFormat ("&api_sig={0}", GetSignature ());
+
+            return data.ToString ();
+        }
+        
+        private string GetSignature ()
+        {
+            SortedDictionary<string, string> sorted_params = new SortedDictionary<string, string> (parameters);
+            
+            if (!sorted_params.ContainsKey ("api_key")) {
+                sorted_params.Add ("api_key", LastfmCore.ApiKey);
+            }
+            if (!sorted_params.ContainsKey ("method")) {
+                sorted_params.Add ("method", method);
+            }
+            StringBuilder signature = new StringBuilder ();
+            foreach (KeyValuePair<string, string> parm in sorted_params) {
+                if (parm.Key.Equals ("format")) {
+                    continue;
+                }
+                signature.Append (parm.Key);
+                signature.Append (parm.Value);
+            }
+            signature.Append (LastfmCore.ApiSecret);
+            
+            return Hyena.CryptoUtil.Md5Encode (signature.ToString (), Encoding.UTF8);
+        }
+
+#region HTTP helpers
+
+        private Stream Get (string uri)
+        {
+            return Get (uri, null);
+        }
+
+        private Stream Get (string uri, string accept)
+        {
+            HttpWebRequest request = (HttpWebRequest) WebRequest.Create (uri);
+            if (accept != null) {
+                request.Accept = accept;
+            }
+            request.UserAgent = LastfmCore.UserAgent;
+            request.Timeout = 10000;
+            request.KeepAlive = false;
+            request.AllowAutoRedirect = true;
+
+            HttpWebResponse response = (HttpWebResponse) request.GetResponse ();
+            return response.GetResponseStream ();
+        }
+       
+        private Stream Post (string uri, string data)
+        {
+            // Do not trust docs : it doesn't work if parameters are in the request body
+            HttpWebRequest request = (HttpWebRequest) WebRequest.Create (String.Concat (uri, "?", data));
+            request.UserAgent = LastfmCore.UserAgent;
+            request.Timeout = 10000;
+            request.Method = "POST";
+            request.KeepAlive = false;
+            request.ContentType = "application/x-www-form-urlencoded";
+
+            HttpWebResponse response = null;
+            try {
+                response = (HttpWebResponse) request.GetResponse ();
+            } catch (WebException e) {
+                Log.DebugException (e);
+                response = (HttpWebResponse)e.Response;
+            }
+            return response.GetResponseStream ();
+        }
+
+#endregion
+    }
+}
diff --git a/src/Libraries/Lastfm/Lastfm/RadioConnection.cs b/src/Libraries/Lastfm/Lastfm/RadioConnection.cs
index 7206305..05a540c 100644
--- a/src/Libraries/Lastfm/Lastfm/RadioConnection.cs
+++ b/src/Libraries/Lastfm/Lastfm/RadioConnection.cs
@@ -38,6 +38,7 @@ using System.Web;
 using System.Threading;
 
 using Hyena;
+using Hyena.Json;
 using Mono.Unix;
 
 using Media.Playlists.Xspf;
@@ -59,21 +60,38 @@ namespace Lastfm
         NoAccount,
         NoNetwork,
         InvalidAccount,
+        NotAuthorized,
         Connecting,
         Connected
     };
 
-    // Error codes returned when trying to adjust.php to a new station
+    // Error codes returned by the API methods
     public enum StationError
     {
         None = 0,
-        NotEnoughContent = 1,
-        FewGroupMembers,
-        FewFans,
-        Unavailable,
-        Subscribe,
-        FewNeighbors,
-        Offline,
+        NotUsed = 1,
+        InvalidService,
+        InvalidMethod,
+        AuthenticationFailed,
+        InvalidFormat,
+        InvalidParameters,
+        InvalidResource,
+        TokenFailure,
+        InvalidSessionKey,
+        InvalidApiKey,
+        ServiceOffline,
+        SubscriptionError,
+        InvalidSignature,
+        TokenNotAuthorized,
+        ExpiredToken,
+
+        SubscriptionRequired = 18,
+
+        NotEnoughContent = 20,
+        NotEnoughMembers,
+        NotEnoughFans,
+        NotEnoughNeighbours,
+        
         Unknown // not an official code, just the fall back
     }
 
@@ -83,19 +101,9 @@ namespace Lastfm
         public event StateChangedHandler StateChanged;
 
         private ConnectionState state;
-        private string session;
-        private string base_url;
-        private string base_path;
         private string info_message;
         private bool network_connected = false;
 
-        private static Regex station_error_regex = new Regex ("error=(\\d+)", RegexOptions.Compiled);
-        
-        private bool subscriber;
-        public bool Subscriber {
-            get { return subscriber; }
-        }
-
         public string InfoMessage {
             get { return info_message; }
         }
@@ -143,26 +151,28 @@ namespace Lastfm
             if (State == ConnectionState.Connecting || State == ConnectionState.Connected)
                 return;
 
-            if (LastfmCore.Account.UserName == null || LastfmCore.Account.CryptedPassword == null) {
+            if (LastfmCore.Account.UserName == null) {
                 State = ConnectionState.NoAccount;
                 return;
             }
+            
+            if (LastfmCore.Account.SessionKey == null) {
+                State = ConnectionState.NotAuthorized;
+                return;
+            }
 
             if (!network_connected) {
                 State = ConnectionState.NoNetwork;
                 return;
             }
 
-            // Otherwise, we're good to try to connect
-            State = ConnectionState.Connecting;
-            Handshake ();
+            // Otherwise, we're good and consider ourselves connected
+            State = ConnectionState.Connected;
         }
 
-        public bool Love    (string artist, string title) { return PostTrackRequest ("loveTrack", artist, title); }
-        public bool UnLove  (string artist, string title) { return PostTrackRequest ("unLoveTrack", artist, title); }
-        public bool Ban     (string artist, string title) { return PostTrackRequest ("banTrack", artist, title); }
-        public bool UnBan   (string artist, string title) { return PostTrackRequest ("unBanTrack", artist, title); }
-
+        public bool Love    (string artist, string title) { return PostTrackRequest ("love", artist, title); }
+        public bool Ban     (string artist, string title) { return PostTrackRequest ("ban", artist, title); }
+        
         public StationError ChangeStationTo (string station)
         {
             lock (this) {
@@ -170,18 +180,13 @@ namespace Lastfm
                     return StationError.None;
 
                 try {
-                    Stream stream = Get (StationUrlFor (station));
-                    using (StreamReader strm = new StreamReader (stream)) {
-                        string body = strm.ReadToEnd ();
-                        if (body.IndexOf ("response=OK") == -1) {
-                            Match match = station_error_regex.Match (body);
-                            if (match.Success) {
-                                int i = Int32.Parse (match.Groups[1].Value);
-                                return (StationError) i;
-                            } else {
-                                return StationError.Unknown;
-                            }
-                        }
+                    
+                    LastfmRequest radio_tune = new LastfmRequest ("radio.tune", RequestType.Write, ResponseFormat.Json);
+                    radio_tune.AddParameter ("station", station);
+                    radio_tune.Send ();
+                    StationError error = radio_tune.GetError ();
+                    if (error != StationError.None) {
+                        return error;
                     }
 
                     this.station = station;
@@ -199,18 +204,19 @@ namespace Lastfm
                 if (station != Station)
                     return null;
 
-                string url = StationRefreshUrl;
                 Playlist pl = new Playlist ();
                 Stream stream = null;
+                LastfmRequest radio_playlist = new LastfmRequest ("radio.getPlaylist", RequestType.AuthenticatedRead, ResponseFormat.Raw);
                 try {
-                    stream = GetXspfStream (url);
+                    radio_playlist.Send ();
+                    stream = radio_playlist.GetResponseStream ();
                     pl.Load (stream);
                     Log.Debug (String.Format ("Adding {0} Tracks to Last.fm Station {1}", pl.TrackCount, station), null);
                 } catch (System.Net.WebException e) {
                     Log.Warning ("Error Loading Last.fm Station", e.Message, false);
                     return null;
                 } catch (Exception e) {
-                    string body = "Unable to get body";
+                    string body = null;
                     try {
                         using (StreamReader strm = new StreamReader (stream)) {
                             body = strm.ReadToEnd ();
@@ -218,7 +224,7 @@ namespace Lastfm
                     } catch {}
                     Log.Warning (
                         "Error loading station",
-                        String.Format ("Exception:\n{0}\n\nResponse Body:\n{1}", e.ToString (), body), false
+                        String.Format ("Exception:\n{0}\n\nResponse:\n{1}", e.ToString (), body ?? "Unable to get response"), false
                     );
                     return null;
                 }
@@ -231,8 +237,7 @@ namespace Lastfm
 
         private void Initialize ()
         {
-            subscriber = false;
-            base_url = base_path = session = station = info_message = null;
+            station = info_message = null;
         }
 
         private void HandleAccountUpdated (object o, EventArgs args)
@@ -256,195 +261,47 @@ namespace Lastfm
             }
         }
 
-        private void Handshake ()
-        {
-            ThreadPool.QueueUserWorkItem (delegate {
-                try {
-                    Stream stream = Get (String.Format (
-                        "http://ws.audioscrobbler.com/radio/handshake.php?version={0}&platform={1}&username={2}&passwordmd5={3}&language={4}&session=324234";,
-                        "1.1.1",
-                        "linux", // FIXME
-                        LastfmCore.Account.UserName, LastfmCore.Account.CryptedPassword,
-                        "en" // FIXME
-                    ));
-
-                    // Set us as connecting, assuming the connection attempt wasn't changed out from under us
-                    if (ParseHandshake (new StreamReader (stream).ReadToEnd ()) && session != null) {
-                        State = ConnectionState.Connected;
-                        Log.Debug (String.Format ("Logged into Last.fm as {0} ({1}subscriber)",
-                            LastfmCore.Account.UserName, subscriber ? null : "not a "), null);
-                        return;
-                    }
-                } catch (Exception e) {
-                    Log.Debug ("Error in Last.fm Handshake", e.ToString ());
-                }
-                
-                // Must not have parsed correctly
-                Initialize ();
-                if (State == ConnectionState.Connecting)
-                    State = ConnectionState.Disconnected;
-            });
-        }
-
-        private bool ParseHandshake (string content) 
-        {
-            string [] lines = content.Split (new Char[] {'\n'});
-            foreach (string line in lines) {
-                string [] opts = line.Split (new Char[] {'='});
-
-                switch (opts[0].Trim ().ToLower ()) {
-                case "session":
-                    if (opts[1].ToLower () == "failed") {
-                        session = null;
-                        State = ConnectionState.InvalidAccount;
-                        Log.Warning (
-                            Catalog.GetString ("Failed to Login to Last.fm"),
-                            Catalog.GetString ("Either your username or password is invalid."),
-                            false
-                        );
-                        LastfmCore.Account.CryptedPassword = null;
-                        return false;
-                    }
-
-                    session = opts[1];
-                    break;
-
-                case "stream_url":
-                    //stream_url = opts[1];
-                    break;
-
-                case "subscriber":
-                    subscriber = (opts[1] != "0");
-                    break;
-
-                case "base_url":
-                    base_url = opts[1];
-                    break;
-
-                case "base_path":
-                    base_path = opts[1];
-                    break;
-                    
-                case "info_message":
-                    info_message = opts[1];
-                    break;
-
-                default:
-                    break;
-                }
-            }
-
-            return true;
-        }
-
-        // Basic HTTP helpers
-
-        private HttpStatusCode Post (string uri, string body)
-        {
-            if (!network_connected) {
-                //throw new NetworkUnavailableException ();
-                return HttpStatusCode.RequestTimeout;
-            }
-        
-            HttpWebRequest request = (HttpWebRequest) WebRequest.Create (uri);
-            request.UserAgent = LastfmCore.UserAgent;
-            request.Timeout = 10000;
-            request.Method = "POST";
-            request.KeepAlive = false;
-            request.ContentLength = body.Length;
-
-            using (StreamWriter writer = new StreamWriter (request.GetRequestStream ())) {
-                writer.Write (body);
-            }
-
-            HttpWebResponse response = (HttpWebResponse) request.GetResponse ();
-            using (Stream stream = response.GetResponseStream ()) {
-                /*using (StreamReader reader = new StreamReader (stream)) {
-                    Console.WriteLine ("Posted {0} got response {1}", body, reader.ReadToEnd ());
-                }*/
-            }
-            return response.StatusCode;
-        }
-
-        private Stream GetXspfStream (string uri)
-        {
-            return Get (uri, "application/xspf+xml");
-        }
-
-        private Stream Get (string uri)
-        {
-            return Get (uri, null);
-        }
-
-        private Stream Get (string uri, string accept)
-        {
-            if (!network_connected) {
-                //throw new NetworkUnavailableException ();
-                return null;
-            }
-        
-            HttpWebRequest request = (HttpWebRequest) WebRequest.Create (uri);
-            if (accept != null) {
-                request.Accept = accept;
-            }
-            request.UserAgent = LastfmCore.UserAgent;
-            request.Timeout = 10000;
-            request.KeepAlive = false;
-            request.AllowAutoRedirect = true;
-
-            HttpWebResponse response = (HttpWebResponse) request.GetResponse ();
-            return response.GetResponseStream ();
-        }
-
-        /* ArtistMetaDataRequest.cpp:    rpc.setMethod( "artistMetadata" );
-         * DeleteFriendRequest.cpp:    xmlrpc.setMethod( "removeFriend" );
-         * RecommendRequest.cpp:    xml_rpc.setMethod( "recommendItem" );
-         * SetTagRequest.cpp:            xml_rpc.setMethod( "tagArtist" );
-         * SetTagRequest.cpp:            xml_rpc.setMethod( "tagAlbum" );
-         * SetTagRequest.cpp:            xml_rpc.setMethod( "tagTrack" );
-         * SimilarTagsRequest.cpp:    xmlrpc.setMethod( "getSimilarTags" );
-         * TrackMetaDataRequest.cpp:    xmlrpc.setMethod( "trackMetadata" );
-         * TrackToIdRequest.cpp:    xmlrpc.setMethod( "trackToId" );
-         * UserPicturesRequest.cpp:    xmlrpc.setMethod( "getAvatarUrls" );
-         */
-
-
-        // URL generators for internal use
- 
-        private string XmlRpcUrl {
-            get { return String.Format ("http://{0}/1.0/rw/xmlrpc.php";, base_url); }
-        }
-
-        private string StationUrlFor (string station) 
-        {
-            return String.Format (
-                "http://{0}{1}/adjust.php?session={2}&url={3}&lang=en";,
-                base_url, base_path, session, HttpUtility.UrlEncode (station)
-            );
-        }
-
-        private string StationRefreshUrl {
-            get {
-                return String.Format (
-                    "http://{0}{1}/xspf.php?sk={2}&discovery=0&desktop=1.3.1.1";,
-                    base_url, base_path, session
-                );
-            }
-        }
-
         // Translated error message strings
 
         public static string ErrorMessageFor (StationError error)
         {
             switch (error) {
-            case StationError.NotEnoughContent:  return Catalog.GetString ("There is not enough content to play this station.");
-            case StationError.FewGroupMembers:   return Catalog.GetString ("This group does not have enough members for radio.");
-            case StationError.FewFans:           return Catalog.GetString ("This artist does not have enough fans for radio.");
-            case StationError.Unavailable:       return Catalog.GetString ("This station is not available.");
-            case StationError.Subscribe:         return Catalog.GetString ("This station is only available to subscribers.");
-            case StationError.FewNeighbors:      return Catalog.GetString ("There are not enough neighbours for this station.");
-            case StationError.Offline:           return Catalog.GetString ("The streaming system is offline for maintenance, please try again later.");
-            case StationError.Unknown:           return Catalog.GetString ("There was an unknown error.");
+                case StationError.InvalidService:
+                case StationError.InvalidMethod:
+                    return Catalog.GetString ("This service does not exist.");
+                case StationError.AuthenticationFailed:
+                case StationError.SubscriptionError:
+                case StationError.SubscriptionRequired:
+                    return Catalog.GetString ("This station is only available to subscribers.");
+                case StationError.InvalidFormat:
+                    return Catalog.GetString ("This station is not available.");
+                case StationError.InvalidParameters:
+                    return Catalog.GetString ("The request is missing a required parameter.");
+                case StationError.InvalidResource:
+                    return Catalog.GetString ("The specified resource is invalid.");
+                case StationError.TokenFailure:
+                    return Catalog.GetString ("Server error, please try again later.");
+                case StationError.InvalidSessionKey:
+                    return Catalog.GetString ("Invalid authentication information, please re-authenticate.");
+                case StationError.InvalidApiKey:
+                    return Catalog.GetString ("The API key used by this application is invalid.");
+                case StationError.ServiceOffline:
+                    return Catalog.GetString ("The streaming system is offline for maintenance, please try again later.");
+                case StationError.InvalidSignature:
+                    return Catalog.GetString ("The method signature is invalid.");
+                case StationError.TokenNotAuthorized:
+                case StationError.ExpiredToken:
+                    return Catalog.GetString ("You need to allow Banshee to access your Last.fm account.");
+                case StationError.NotEnoughContent:
+                    return Catalog.GetString ("There is not enough content to play this station.");
+                case StationError.NotEnoughMembers:
+                    return Catalog.GetString ("This group does not have enough members for radio.");
+                case StationError.NotEnoughFans:
+                    return Catalog.GetString ("This artist does not have enough fans for radio.");
+                case StationError.NotEnoughNeighbours:
+                    return Catalog.GetString ("There are not enough neighbours for this station.");
+                case StationError.Unknown:
+                    return Catalog.GetString ("There was an unknown error.");
             }
             return String.Empty;
         }
@@ -452,93 +309,36 @@ namespace Lastfm
         public static string MessageFor (ConnectionState state)
         {
             switch (state) {
-            case ConnectionState.Disconnected:      return Catalog.GetString ("Not connected to Last.fm.");
-            case ConnectionState.NoAccount:         return Catalog.GetString ("Account details are needed before you can connect to Last.fm");
-            case ConnectionState.NoNetwork:         return Catalog.GetString ("No network connection detected.");
-            case ConnectionState.InvalidAccount:    return Catalog.GetString ("Last.fm username or password is invalid.");
-            case ConnectionState.Connecting:        return Catalog.GetString ("Connecting to Last.fm.");
-            case ConnectionState.Connected:         return Catalog.GetString ("Connected to Last.fm.");
+                case ConnectionState.Disconnected:
+                    return Catalog.GetString ("Not connected to Last.fm.");
+                case ConnectionState.NoAccount:
+                    return Catalog.GetString ("Account details are needed before you can connect to Last.fm");
+                case ConnectionState.NoNetwork:
+                    return Catalog.GetString ("No network connection detected.");
+                case ConnectionState.InvalidAccount:
+                    return Catalog.GetString ("Last.fm username is invalid.");
+                case ConnectionState.NotAuthorized:
+                    return Catalog.GetString ("You need to allow Banshee to access your Last.fm account.");
+                case ConnectionState.Connecting:
+                    return Catalog.GetString ("Connecting to Last.fm.");
+                case ConnectionState.Connected:
+                    return Catalog.GetString ("Connected to Last.fm.");
             }
             return String.Empty;
         }
 
-        // XML-RPC foo
-
         private bool PostTrackRequest (string method, string artist, string title)
         {
-            return PostXmlRpc (LastFMXmlRpcRequest (method).AddStringParams (artist, title));
-        }
-
-        private bool PostXmlRpc (LameXmlRpcRequest request)
-        {
             if (State != ConnectionState.Connected)
                 return false;
 
-            return Post (XmlRpcUrl, request.ToString ()) == HttpStatusCode.OK;
-        }
+            // track.love and track.ban do not return JSON
+            LastfmRequest track_request = new LastfmRequest (String.Concat ("track.", method), RequestType.Write, ResponseFormat.Raw);
+            track_request.AddParameter ("track", title);
+            track_request.AddParameter ("artist", artist);
+            track_request.Send ();
 
-        private string UnixTime ()
-        {
-            return ((int) (DateTime.Now - new DateTime (1970, 1, 1)).TotalSeconds).ToString ();
-        }
-
-        private LameXmlRpcRequest LastFMXmlRpcRequest (string method)
-        {
-            string time = UnixTime ();
-            string auth_hash = Hyena.CryptoUtil.Md5Encode (LastfmCore.Account.CryptedPassword + time);
-            return new LameXmlRpcRequest (method).AddStringParams (LastfmCore.Account.UserName, time, auth_hash);
-        }
-
-        protected class LameXmlRpcRequest
-        {
-            private StringBuilder sb = new StringBuilder ();
-            public LameXmlRpcRequest (string method_name)
-            {
-                sb.Append ("<?xml version=\"1.0\" encoding=\"us-ascii\"?>\n");
-                sb.Append ("<methodCall><methodName>");
-                sb.Append (Encode (method_name));
-                sb.Append ("</methodName>\n");
-                sb.Append ("<params>\n");
-            }
-
-            public LameXmlRpcRequest AddStringParams (params string [] values)
-            {
-                foreach (string value in values)
-                    AddStringParam (value);
-                return this;
-            }
-
-            public LameXmlRpcRequest AddStringParam (string value)
-            {
-                sb.Append ("<param><value><string>");
-                sb.Append (Encode (value));
-                sb.Append ("</string></value></param>\n");
-                return this;
-            }
-
-            public static string Encode (string val)
-            {
-                return HttpUtility.HtmlEncode (val);
-            }
-
-            private bool closed = false;
-            public override string ToString ()
-            {
-                if (!closed) {
-                    sb.Append ("</params>\n</methodCall>");
-                    closed = true;
-                }
-
-                return sb.ToString ();
-            }
-        }
-    }
-
-    public sealed class StringUtils {
-        public static string StringToUTF8 (string s)
-        {
-            byte [] ba = (new UnicodeEncoding ()).GetBytes (s);
-            return System.Text.Encoding.UTF8.GetString (ba);
+            return (track_request.GetError () == StationError.None);
         }
     }
 }
diff --git a/src/Libraries/Lastfm/Makefile.am b/src/Libraries/Lastfm/Makefile.am
index 1044dc4..22df01f 100644
--- a/src/Libraries/Lastfm/Makefile.am
+++ b/src/Libraries/Lastfm/Makefile.am
@@ -15,6 +15,7 @@ SOURCES =  \
 	Lastfm/Browser.cs \
 	Lastfm/IQueue.cs \
 	Lastfm/LastfmCore.cs \
+	Lastfm/LastfmRequest.cs \
 	Lastfm/RadioConnection.cs 
 
 include $(top_srcdir)/build/build.mk



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