banshee r3308 - in trunk/banshee: . build src/Core/Banshee.ThickClient/Banshee.Gui src/Core/Banshee.ThickClient/Resources src/Extensions src/Extensions/Banshee.Lastfm src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler src/Libraries/Lastfm src/Libraries/Lastfm/Lastfm



Author: ahixon
Date: Sat Feb 23 08:35:36 2008
New Revision: 3308
URL: http://svn.gnome.org/viewvc/banshee?rev=3308&view=rev

Log:
2008-02-22  Alexander Hixon <hixon alexander mediati org>
    * src/Extensions/Makefile.am:
    * src/Extensions/Banshee.Audioscrobbler:
    * build/build.environment.mk:
    * configure.ac: Remove reference to the old extension name and paths.
    
    * src/Extensions/Banshee.Lastfm/Makefile.am:
    * src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/Queue.cs:
    * src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs:
    * src/Extensions/Banshee.Lastfm/Banshee.Lastfm.addin.xml: Cleaned version of the
      Audioscrobbler plugin from stable. Glue to hook up player engine and
      AudioscrobblerConnection logic lives here.
    
    * src/Libraries/Lastfm/Lastfm/AudioscrobblerConnection.cs:
    * src/Libraries/Lastfm/Lastfm/IQueue.cs: Seperate out web bits and queue interface
      into our Last.fm library. Uses version 1.2 of the Last.fm protocol.
    
    * src/Core/Banshee.ThickClient/Resources/core-ui-actions-layout.xml:
    * src/Core/Banshee.ThickClient/Banshee.Gui/GlobalActions.cs: Added 'Tools' menu.


Added:
   trunk/banshee/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/
   trunk/banshee/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs
   trunk/banshee/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/Queue.cs
   trunk/banshee/src/Libraries/Lastfm/Lastfm/AudioscrobblerConnection.cs
   trunk/banshee/src/Libraries/Lastfm/Lastfm/IQueue.cs
Modified:
   trunk/banshee/ChangeLog
   trunk/banshee/build/build.environment.mk
   trunk/banshee/configure.ac
   trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui/GlobalActions.cs
   trunk/banshee/src/Core/Banshee.ThickClient/Resources/core-ui-actions-layout.xml
   trunk/banshee/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.addin.xml
   trunk/banshee/src/Extensions/Banshee.Lastfm/Makefile.am
   trunk/banshee/src/Extensions/Makefile.am
   trunk/banshee/src/Libraries/Lastfm/Makefile.am

Modified: trunk/banshee/build/build.environment.mk
==============================================================================
--- trunk/banshee/build/build.environment.mk	(original)
+++ trunk/banshee/build/build.environment.mk	Sat Feb 23 08:35:36 2008
@@ -117,7 +117,6 @@
 REF_BACKEND_UNIX = $(LINK_BANSHEE_CORE_DEPS) $(LINK_MONO_POSIX)
 
 # Extensions
-REF_EXTENSION_AUDIOSCROBBLER = $(LINK_BANSHEE_SERVICES_DEPS)
 REF_EXTENSION_MULTIMEDIAKEYS = $(LINK_BANSHEE_SERVICES_DEPS)
 REF_EXTENSION_NOTIFICATIONAREA = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_PLAYQUEUE = $(LINK_BANSHEE_THICKCLIENT_DEPS)

Modified: trunk/banshee/configure.ac
==============================================================================
--- trunk/banshee/configure.ac	(original)
+++ trunk/banshee/configure.ac	Sat Feb 23 08:35:36 2008
@@ -147,7 +147,6 @@
 src/Libraries/Mono.Media/Makefile
 
 src/Extensions/Makefile
-src/Extensions/Banshee.Audioscrobbler/Makefile
 src/Extensions/Banshee.Lastfm/Makefile
 src/Extensions/Banshee.MultimediaKeys/Makefile
 src/Extensions/Banshee.NotificationArea/Makefile

Modified: trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui/GlobalActions.cs
==============================================================================
--- trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui/GlobalActions.cs	(original)
+++ trunk/banshee/src/Core/Banshee.ThickClient/Banshee.Gui/GlobalActions.cs	Sat Feb 23 08:35:36 2008
@@ -77,6 +77,10 @@
                     Catalog.GetString ("Manage _Extensions"), null,
                     Catalog.GetString ("Manage extensions to add new features to Banshee"), OnExtensions),
                 
+                // Tools menu
+                new ActionEntry ("ToolsMenuAction", null,
+                    Catalog.GetString ("_Tools"), null, null, null),
+                
                 // Help Menu
                 new ActionEntry ("HelpMenuAction", null, 
                     Catalog.GetString ("_Help"), null, null, null),

Modified: trunk/banshee/src/Core/Banshee.ThickClient/Resources/core-ui-actions-layout.xml
==============================================================================
--- trunk/banshee/src/Core/Banshee.ThickClient/Resources/core-ui-actions-layout.xml	(original)
+++ trunk/banshee/src/Core/Banshee.ThickClient/Resources/core-ui-actions-layout.xml	Sat Feb 23 08:35:36 2008
@@ -76,6 +76,9 @@
       <separator/>
       <menuitem name="Shuffle" action="ShuffleAction"/>
     </menu>
+    
+    <menu name="ToolsMenu" action="ToolsMenuAction">
+    </menu>
 
     <menu name="HelpMenu" action="HelpMenuAction">
       <menu name="WebMenu" action="WebMenuAction">

Added: trunk/banshee/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs	Sat Feb 23 08:35:36 2008
@@ -0,0 +1,230 @@
+//
+// AudioscrobblerService.cs
+//
+// Authors:
+//   Alexander Hixon <hixon alexander mediati org>
+//   Chris Toshok <toshok ximian com>
+//   Ruben Vermeersch <ruben savanne be>
+//   Aaron Bockover <aaron abock org>
+//
+// Copyright (C) 2005-2008 Novell, Inc.
+//
+// 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.IO;
+using System.Net;
+using System.Text;
+using System.Security.Cryptography;
+using Gtk;
+using Mono.Unix;
+
+using Hyena;
+
+using Lastfm;
+using Lastfm.Gui;
+
+using Banshee.MediaEngine;
+using Banshee.Base;
+using Banshee.Configuration;
+using Banshee.ServiceStack;
+using Banshee.Gui;
+
+using Banshee.Collection;
+
+namespace Banshee.Lastfm.Audioscrobbler
+{
+    public class AudioscrobblerService : IExtensionService, IDisposable
+    {
+        private AudioscrobblerConnection connection;
+        private ActionGroup actions;
+        private uint ui_manager_id;
+        private InterfaceActionService action_service;
+        private Queue queue;        
+        private Account account;
+        
+        private bool song_started = false; /* if we were watching the current song from the beginning */
+        private bool queued; /* if current_track has been queued */
+        private bool sought; /* if the user has sought in the current playing song */
+    
+        public AudioscrobblerService ()
+        {
+        }
+        
+        void IExtensionService.Initialize ()
+        {
+            account = new Account ();
+            account.UserName = LastUserSchema.Get ();
+            account.CryptedPassword = LastPassSchema.Get ();
+            
+            queue = new Queue ();
+            connection = new AudioscrobblerConnection (account, queue);
+            
+            ServiceManager.PlayerEngine.EventChanged += OnPlayerEngineEventChanged;
+            
+            action_service = ServiceManager.Get<InterfaceActionService> ("InterfaceActionService");
+            InterfaceInitialize ();
+        
+            if (!connection.Started) {
+                connection.Connect ();
+            }
+        }
+        
+        public void InterfaceInitialize ()
+        {
+            actions = new ActionGroup ("Audioscrobbler");
+            
+            actions.Add (new ActionEntry [] {
+                new ActionEntry ("AudioscrobblerAction", null,
+                    Catalog.GetString ("_Audioscrobbler"), null,
+                    Catalog.GetString ("Configure the Audioscrobbler plugin"), null),
+                    
+                new ActionEntry ("AudioscrobblerVisitAction", null,
+                    Catalog.GetString ("Visit _user profile page"), null,
+                    Catalog.GetString ("Visit your Audioscrobbler profile page"), OnVisitOwnProfile),
+                
+                new ActionEntry ("AudioscrobblerConfigureAction", null,
+                    Catalog.GetString ("_Configure..."), null,
+                    Catalog.GetString ("Configure the Audioscrobbler plugin"), OnConfigurePlugin)
+            });
+            
+            actions.Add (new ToggleActionEntry [] { 
+                new ToggleActionEntry ("AudioscrobblerEnableAction", null,
+                    Catalog.GetString ("_Enable song reporting"), "<control>U",
+                    Catalog.GetString ("Enable song reporting"), OnToggleEnabled, Enabled)
+            });
+            
+            action_service.UIManager.InsertActionGroup (actions, 0);
+            ui_manager_id = action_service.UIManager.AddUiFromResource ("AudioscrobblerMenu.xml");
+            
+            actions["AudioscrobblerVisitAction"].Sensitive = account.UserName != null && account.UserName != String.Empty;
+        }
+        
+  
+        public void Dispose ()
+        {
+            ServiceManager.PlayerEngine.EventChanged -= OnPlayerEngineEventChanged;
+            
+            connection.Stop ();
+        
+            action_service.UIManager.RemoveUi (ui_manager_id);
+            action_service.UIManager.RemoveActionGroup (actions);
+            actions = null;
+        }
+        
+        private void OnPlayerEngineEventChanged (object o, PlayerEngineEventArgs args)
+        {
+            switch (args.Event) {
+                /* Queue if we're watching this song from the beginning,
+                 * it isn't queued yet and the user didn't seek until now,
+                 * we're actually playing, song position and length are greater than 0
+                 * and we already played half of the song or 240 seconds */
+                 
+                case PlayerEngineEvent.Iterate:
+                    if (song_started && !queued && !sought &&
+                        ServiceManager.PlayerEngine.CurrentState == PlayerEngineState.Playing &&
+                        ServiceManager.PlayerEngine.Length > 0 &&
+                        ServiceManager.PlayerEngine.Position > 0 &&
+                        (ServiceManager.PlayerEngine.Position > ServiceManager.PlayerEngine.Length / 2 || ServiceManager.PlayerEngine.Position > (240 * 1000))) {
+                            TrackInfo track = ServiceManager.PlayerEngine.CurrentTrack;
+                            if (track == null) {
+                                queued = sought = false;
+                            } else {
+                                if ((actions["AudioscrobblerEnableAction"] as ToggleAction).Active) {
+                                    queue.Add (track, DateTime.Now - TimeSpan.FromSeconds (ServiceManager.PlayerEngine.Position));
+                                }
+                                queued = true;
+                            }
+                    }
+                    
+                    break;
+                 
+                /* Start of Stream: new song started */
+                case PlayerEngineEvent.StartOfStream:
+                    queued = sought = false;
+                    song_started = true;
+                    break;
+                
+                /* End of Stream: song finished */
+                case PlayerEngineEvent.EndOfStream:
+                    song_started = queued = sought = false;
+                    break;
+                
+                /* Did the user seek? */
+                case PlayerEngineEvent.Seek:
+                    sought = true;
+                    break;
+            }
+        }
+        
+        private void OnConfigurePlugin (object o, EventArgs args)
+        {
+            AccountLoginDialog dialog = new AccountLoginDialog (account, true);
+            dialog.SaveOnEdit = true;
+            if (account.UserName == null) {
+                dialog.AddSignUpButton ();
+            }
+            dialog.Run ();
+            dialog.Destroy ();
+        }
+        
+        private void OnVisitOwnProfile (object o, EventArgs args)
+        {
+            account.VisitUserProfile (account.UserName);
+        }
+        
+        private void OnToggleEnabled (object o, EventArgs args)
+        {
+            Enabled = (o as ToggleAction).Active;
+        }
+        
+        internal bool Enabled {
+            get { return EngineEnabledSchema.Get (); }
+            set { 
+                EngineEnabledSchema.Set (value);
+                if (!connection.Started) {
+                    connection.Connect ();
+                }
+                
+                (actions["AudioscrobblerEnableAction"] as ToggleAction).Active = value;
+            }
+        }
+           
+        public static readonly SchemaEntry<string> LastUserSchema = new SchemaEntry<string> (
+            "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<bool> EngineEnabledSchema = new SchemaEntry<bool> (
+            "plugins.audioscrobbler", "engine_enabled",
+            false,
+            "Engine enabled",
+            "Audioscrobbler reporting engine enabled"
+        );
+        
+        string IService.ServiceName {
+            get { return "AudioscrobblerService"; }
+        }
+    }
+}

Added: trunk/banshee/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/Queue.cs
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/Queue.cs	Sat Feb 23 08:35:36 2008
@@ -0,0 +1,256 @@
+//
+// Queue.cs
+//
+// Author:
+//   Chris Toshok <toshok ximian com>
+//   Alexander Hixon <hixon alexander mediati org>
+//
+// Copyright (C) 2005-2008 Novell, Inc.
+//
+// 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.IO;
+using System.Text;
+using System.Security.Cryptography;
+using Mono.Security.Cryptography;
+using System.Collections.Generic;
+using System.Web;
+using System.Xml;
+
+using Hyena;
+
+using Banshee.Base;
+using Banshee.Collection;
+
+using Lastfm;
+
+namespace Banshee.Lastfm.Audioscrobbler
+{
+    class Queue : IQueue
+    {
+        internal class QueuedTrack
+        {
+            public QueuedTrack (TrackInfo track, DateTime start_time)
+            {
+                this.artist = track.ArtistName;
+                this.album = track.AlbumTitle;
+                this.title = track.TrackTitle;
+                this.track_number = (int) track.TrackNumber;
+                this.duration = (int) track.Duration.TotalSeconds;
+                this.start_time = DateTimeUtil.ToTimeT(start_time.ToUniversalTime ());
+            }
+
+            public QueuedTrack (string artist, string album,
+                                string title, int track_number, int duration, long start_time)
+            {
+                this.artist = artist;
+                this.album = album;
+                this.title = title;
+                this.track_number = track_number;
+                this.duration = duration;
+                this.start_time = start_time;
+            }
+
+            public long StartTime {
+                get { return start_time; }
+            }
+            
+            public string Artist {
+                get { return artist; }
+            }
+            
+            public string Album {
+                get { return album; }
+            }
+            
+            public string Title {
+                get { return title; }
+            }
+            
+            public int TrackNumber {
+                get { return track_number; }
+            }
+            
+            public int Duration {
+                get { return duration; }
+            }
+
+            string artist;
+            string album;
+            string title;
+            int track_number;
+            int duration;
+            long start_time;
+        }
+
+        List<QueuedTrack> queue;
+        string xml_path;
+        bool dirty;
+
+        public event EventHandler TrackAdded;
+
+        public Queue ()
+        {
+            string xmlfilepath = Path.Combine (Path.Combine (Banshee.Base.Paths.ApplicationData,
+                "plugins"), "last.fm");
+            xml_path = Path.Combine (xmlfilepath, "audioscrobbler-queue.xml");
+            queue = new List<QueuedTrack> ();
+            
+            if (!Directory.Exists(xmlfilepath)) {
+                Directory.CreateDirectory (xmlfilepath);
+            }
+
+            Load ();
+        }
+
+        public void Save ()
+        {
+            if (!dirty)
+                return;
+
+            XmlTextWriter writer = new XmlTextWriter (xml_path, Encoding.Default);
+
+            writer.Formatting = Formatting.Indented;
+            writer.Indentation = 4;
+            writer.IndentChar = ' ';
+
+            writer.WriteStartDocument (true);
+
+            writer.WriteStartElement ("AudioscrobblerQueue");
+            foreach (QueuedTrack track in queue) {
+                writer.WriteStartElement ("QueuedTrack");    
+                writer.WriteElementString ("Artist", track.Artist);
+                writer.WriteElementString ("Album", track.Album);
+                writer.WriteElementString ("Title", track.Title);
+                writer.WriteElementString ("TrackNumber", track.TrackNumber.ToString());
+                writer.WriteElementString ("Duration", track.Duration.ToString());
+                writer.WriteElementString ("StartTime", track.StartTime.ToString());
+                writer.WriteEndElement (); // Track
+            }
+            writer.WriteEndElement (); // AudioscrobblerQueue
+            writer.WriteEndDocument ();
+            writer.Close ();
+        }
+
+        public void Load ()
+        {
+            queue.Clear ();
+
+            try {
+                string query = "//AudioscrobblerQueue/QueuedTrack";
+                XmlDocument doc = new XmlDocument ();
+
+                doc.Load (xml_path);
+                XmlNodeList nodes = doc.SelectNodes (query);
+
+                foreach (XmlNode node in nodes) {
+                    string artist = "";    
+                    string album = "";
+                    string title = "";
+                    int track_number = 0;
+                    int duration = 0;
+                    long start_time = 0;
+
+                    foreach (XmlNode child in node.ChildNodes) {
+                        if (child.Name == "Artist" && child.ChildNodes.Count != 0) {
+                            artist = child.ChildNodes [0].Value;
+                        } else if (child.Name == "Album" && child.ChildNodes.Count != 0) {
+                            album = child.ChildNodes [0].Value;
+                        } else if (child.Name == "Title" && child.ChildNodes.Count != 0) {
+                            title = child.ChildNodes [0].Value;
+                        } else if (child.Name == "TrackNumber" && child.ChildNodes.Count != 0) {
+                            track_number = Convert.ToInt32 (child.ChildNodes [0].Value);
+                        } else if (child.Name == "Duration" && child.ChildNodes.Count != 0) {
+                            duration = Convert.ToInt32 (child.ChildNodes [0].Value);
+                        } else if (child.Name == "StartTime" && child.ChildNodes.Count != 0) {
+                            start_time = Convert.ToInt64 (child.ChildNodes [0].Value);
+                        }
+                    }
+
+                    queue.Add (new QueuedTrack (artist, album, title, track_number, duration, start_time));
+                }
+            } catch { 
+            }
+        }
+
+        public string GetTransmitInfo (out int numtracks)
+        {
+            string str_track_number = "";
+            StringBuilder sb = new StringBuilder ();
+
+            int i;
+            for (i = 0; i < queue.Count; i ++) {
+                /* Last.FM 1.2 can handle up to 50 songs in one request */
+                if (i == 49) break;
+
+                QueuedTrack track = (QueuedTrack) queue[i];
+
+                if (track.TrackNumber != 0)
+                    str_track_number = track.TrackNumber.ToString();
+
+                sb.AppendFormat (
+                    "&a[{9}]={0}&t[{9}]={1}&i[{9}]={2}&o[{9}]={3}&r[{9}]={4}&l[{9}]={5}&b[{9}]={6}&n[{9}]={7}&m[{9}]={8}",
+                    HttpUtility.UrlEncode (track.Artist),
+                    HttpUtility.UrlEncode (track.Title),
+                    track.StartTime.ToString (),
+                    "P" /* source: chosen by user */, 
+                    ""  /* rating: L/B/S */, 
+                    track.Duration.ToString (),
+                    HttpUtility.UrlEncode (track.Album),
+                    str_track_number,
+                    "" /* musicbrainz id */,
+                     i);
+            }
+
+            numtracks = i;
+            return sb.ToString ();
+        }
+
+        public void Add (object track, DateTime started_at)
+        {
+            TrackInfo t = (track as TrackInfo);
+            if (t != null) {
+                Log.DebugFormat ("Queued: {0}", t);
+                queue.Add (new QueuedTrack (t, started_at));
+                dirty = true;
+                RaiseTrackAdded (this, new EventArgs ());
+            }
+        }
+
+        public void RemoveRange (int first, int count)
+        {
+            queue.RemoveRange (first, count);
+            dirty = true;
+        }
+
+        public int Count {
+            get { return queue.Count; }
+        }
+
+        private void RaiseTrackAdded (object o, EventArgs args)
+        {
+            EventHandler handler = TrackAdded;
+            if (handler != null)
+                handler (o, args);
+        }
+    }
+}

Modified: trunk/banshee/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.addin.xml
==============================================================================
--- trunk/banshee/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.addin.xml	(original)
+++ trunk/banshee/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.addin.xml	Sat Feb 23 08:35:36 2008
@@ -9,4 +9,8 @@
     <Source class="Banshee.Lastfm.Radio.LastfmSource"/>
   </Extension>
   
+  <Extension path="/Banshee/ServiceManager/Service">
+    <Service class="Banshee.Lastfm.Audioscrobbler.AudioscrobblerService"/>
+  </Extension>
+  
 </Addin>

Modified: trunk/banshee/src/Extensions/Banshee.Lastfm/Makefile.am
==============================================================================
--- trunk/banshee/src/Extensions/Banshee.Lastfm/Makefile.am	(original)
+++ trunk/banshee/src/Extensions/Banshee.Lastfm/Makefile.am	Sat Feb 23 08:35:36 2008
@@ -1,8 +1,10 @@
 ASSEMBLY = Banshee.Lastfm
 TARGET = library
-LINK = $(REF_EXTENSION_LASTFM)
+LINK = $(REF_EXTENSION_LASTFM) -r:System.Web -r:Mono.Security
 
 SOURCES =  \
+    Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs \
+    Banshee.Lastfm.Audioscrobbler/Queue.cs \
 	Banshee.Lastfm.Radio/Connection.cs \
 	Banshee.Lastfm.Radio/LastfmActions.cs \
 	Banshee.Lastfm.Radio/LastfmSource.cs \
@@ -16,6 +18,7 @@
 RESOURCES =  \
 	Banshee.Lastfm.addin.xml \
 	Resources/ActiveSourceUI.xml \
+	Resources/AudioscrobblerMenu.xml \
 	Resources/audioscrobbler.png \
 	Resources/GlobalUI.xml \
 	Resources/lastfm.glade \

Modified: trunk/banshee/src/Extensions/Makefile.am
==============================================================================
--- trunk/banshee/src/Extensions/Makefile.am	(original)
+++ trunk/banshee/src/Extensions/Makefile.am	Sat Feb 23 08:35:36 2008
@@ -1,5 +1,4 @@
 SUBDIRS = \
-	Banshee.Audioscrobbler \
 	Banshee.Lastfm \
 	Banshee.MultimediaKeys \
 	Banshee.NotificationArea \

Added: trunk/banshee/src/Libraries/Lastfm/Lastfm/AudioscrobblerConnection.cs
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Libraries/Lastfm/Lastfm/AudioscrobblerConnection.cs	Sat Feb 23 08:35:36 2008
@@ -0,0 +1,546 @@
+//
+// AudioscrobblerConnection.cs
+//
+// Author:
+//   Chris Toshok <toshok ximian com>
+//   Alexander Hixon <hixon alexander mediati org>
+//
+// Copyright (C) 2005-2008 Novell, Inc.
+//
+// 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.IO;
+using System.Net;
+using System.Text;
+using System.Timers;
+using System.Security.Cryptography;
+using Mono.Security.Cryptography;
+using System.Web;
+
+using Hyena;
+
+namespace Lastfm
+{
+    public class AudioscrobblerConnection
+    {
+        enum State {
+            IDLE,
+            NEED_HANDSHAKE,
+            NEED_TRANSMIT,
+            WAITING_FOR_REQ_STREAM,
+            WAITING_FOR_HANDSHAKE_RESP,
+            WAITING_FOR_RESP
+        };
+
+        const int TICK_INTERVAL = 2000; /* 2 seconds */
+        const int FAILURE_LOG_MINUTES = 5; /* 5 minute delay on logging failure to upload information */
+        const int RETRY_SECONDS = 60; /* 60 second delay for transmission retries */
+        const int MAX_RETRY_SECONDS = 7200; /* 2 hours, as defined in the last.fm protocol */
+        const int TIME_OUT = 5000; /* 5 seconds timeout for webrequests */
+        const string CLIENT_ID = "bsh";
+        const string CLIENT_VERSION = "0.1";
+        const string SCROBBLER_URL = "http://post.audioscrobbler.com/";;
+        const string SCROBBLER_VERSION = "1.2";
+
+        Account account;
+        string post_url;
+        string session_id = null;
+        string now_playing_url;
+        
+        bool started = false;
+        public bool Started {
+            get { return started; }
+        }
+
+        System.Timers.Timer timer;
+        DateTime next_interval;
+        DateTime last_upload_failed_logged;
+
+        IQueue queue;
+        
+        int hard_failures = 0;
+        int hard_failure_retry_sec = 60;
+ 
+        bool now_playing_submitted;
+        
+        DateTime song_start_time;
+        //TrackInfo last_track;
+        
+        WebRequest now_playing_post;
+        WebRequest current_web_req;
+        IAsyncResult current_async_result;
+        State state;
+        
+        public AudioscrobblerConnection (Account account, IQueue queue)
+        {
+            this.account = account;
+            
+            account.Updated += AccountUpdated;
+            
+            state = State.IDLE;
+            this.queue = queue;
+        }
+        
+        private void AccountUpdated (object o, EventArgs args)
+        {
+            Connect ();
+        }
+        
+        public void Connect ()
+        {
+            if (session_id == null) {
+                if (!started) {
+                    Start ();
+                }
+                
+                Handshake ();
+            }
+        }
+
+        private void Start ()
+        {
+            state = State.NEED_HANDSHAKE;
+            started = true;
+            queue.TrackAdded += delegate(object o, EventArgs args) {
+                StartTransitionHandler ();
+            };
+            
+            queue.Load ();
+        }
+
+        private void StartTransitionHandler ()
+        {
+            if (timer == null) {
+                timer = new System.Timers.Timer ();
+                timer.Interval = TICK_INTERVAL;
+                timer.AutoReset = true;
+                timer.Elapsed += new ElapsedEventHandler (StateTransitionHandler);
+                
+                timer.Start ();
+                Console.WriteLine ("Timer started.");
+            } else if (!timer.Enabled) {
+                timer.Start ();
+                Console.WriteLine ("Restarting timer from stopped state.");
+            }
+        }
+
+        public void Stop ()
+        {
+            StopTransitionHandler ();
+
+            if (current_web_req != null) {
+                current_web_req.Abort ();
+            }
+
+            queue.Save ();
+            
+            started = false;
+        }
+
+        private void StopTransitionHandler ()
+        {
+            if (timer != null) {
+                timer.Stop ();
+            }
+        }
+
+        private void StateTransitionHandler (object o, ElapsedEventArgs e)
+        {
+            Console.WriteLine ("State transition handler running.");
+            /* if we're not connected, don't bother doing anything
+             * involving the network. */
+             // TODO!
+            /*if (!NetworkDetect.Instance.Connected) {
+                return true;
+            }*/
+                        
+            if ((state == State.IDLE || state == State.NEED_TRANSMIT) && hard_failures > 2) {
+                state = State.NEED_HANDSHAKE;
+                hard_failures = 0;
+            }
+
+            /* and address changes in our engine state */
+            switch (state) {
+            case State.IDLE:
+                if (account.UserName != null && account.CryptedPassword != null && session_id == null) {
+                    state = State.NEED_HANDSHAKE;
+                } else {
+                    if (queue.Count > 0)
+                        state = State.NEED_TRANSMIT;
+                    else if (now_playing_submitted)
+                        StopTransitionHandler ();
+                }
+                
+                break;
+            case State.NEED_HANDSHAKE:
+                if (DateTime.Now > next_interval) {
+                    Handshake ();
+                }
+                
+                break;
+            case State.NEED_TRANSMIT:
+                if (DateTime.Now > next_interval) {
+                    TransmitQueue ();
+                }
+                break;
+            case State.WAITING_FOR_RESP:
+            case State.WAITING_FOR_REQ_STREAM:
+            case State.WAITING_FOR_HANDSHAKE_RESP:
+                /* nothing here */
+                break;
+            }
+            
+            // Only submit if queue is empty, otherwise the submission
+            // gets overruled by the queue submission by Last.fm
+            /*if (queue.Count == 0 && !now_playing_submitted && state == State.IDLE && is_playing) {
+                NowPlaying (PlayerEngineCore.CurrentTrack);
+            }*/
+            // TODO
+        }
+
+        //
+        // Async code for transmitting the current queue of tracks
+        //
+        class TransmitState
+        {
+            public StringBuilder StringBuilder;
+            public int Count;
+        }
+
+        void TransmitQueue ()
+        {
+            int num_tracks_transmitted;
+
+            /* save here in case we're interrupted before we complete
+             * the request.  we save it again when we get an OK back
+             * from the server */
+            queue.Save ();
+
+            next_interval = DateTime.MinValue;
+
+            if (post_url == null) {
+                return;
+            }
+
+            StringBuilder sb = new StringBuilder ();
+
+            sb.AppendFormat ("s={0}", session_id);
+
+            sb.Append (queue.GetTransmitInfo (out num_tracks_transmitted));
+
+            current_web_req = WebRequest.Create (post_url);
+            current_web_req.Method = "POST";
+            current_web_req.ContentType = "application/x-www-form-urlencoded";
+            current_web_req.ContentLength = sb.Length;
+            
+            //Console.WriteLine ("Sending {0} ({1} bytes) to {2}", sb.ToString (), sb.Length, post_url);
+
+            TransmitState ts = new TransmitState ();
+            ts.Count = num_tracks_transmitted;
+            ts.StringBuilder = sb;
+
+            state = State.WAITING_FOR_REQ_STREAM;
+            current_async_result = current_web_req.BeginGetRequestStream (TransmitGetRequestStream, ts);
+            if (!(current_async_result.AsyncWaitHandle.WaitOne (TIME_OUT, false))) {
+		        Hyena.Log.Warning ("Audioscrobbler upload failed", 
+                                             "The request timed out and was aborted", false);
+                next_interval = DateTime.Now + new TimeSpan (0, 0, RETRY_SECONDS);
+                hard_failures++;
+                state = State.IDLE;
+                
+                current_web_req.Abort();
+            }
+        }
+
+        void TransmitGetRequestStream (IAsyncResult ar)
+        {
+            Stream stream;
+
+            try {
+                stream = current_web_req.EndGetRequestStream (ar);
+            }
+            catch (Exception e) {
+                Hyena.Log.Warning ("Failed to get the request stream", e.ToString (), false);
+
+                state = State.IDLE;
+                next_interval = DateTime.Now + new TimeSpan (0, 0, RETRY_SECONDS);
+                return;
+            }
+
+            TransmitState ts = (TransmitState) ar.AsyncState;
+            StringBuilder sb = ts.StringBuilder;
+
+            StreamWriter writer = new StreamWriter (stream);
+            writer.Write (sb.ToString ());
+            writer.Close ();
+
+            state = State.WAITING_FOR_RESP;
+            current_async_result = current_web_req.BeginGetResponse (TransmitGetResponse, ts);
+            if (current_async_result == null) {
+                next_interval = DateTime.Now + new TimeSpan (0, 0, RETRY_SECONDS);
+                hard_failures++;
+                state = State.IDLE;
+            }
+        }
+
+        void TransmitGetResponse (IAsyncResult ar)
+        {
+            WebResponse resp;
+
+            try {
+                resp = current_web_req.EndGetResponse (ar);
+            }
+            catch (Exception e) {
+                Console.WriteLine ("Failed to get the response: {0}", e);
+
+                state = State.IDLE;
+                next_interval = DateTime.Now + new TimeSpan (0, 0, RETRY_SECONDS);
+                return;
+            }
+
+            TransmitState ts = (TransmitState) ar.AsyncState;
+
+            Stream s = resp.GetResponseStream ();
+
+            StreamReader sr = new StreamReader (s, Encoding.UTF8);
+
+            string line;
+            line = sr.ReadLine ();
+            
+            DateTime now = DateTime.Now;
+            if (line.StartsWith ("FAILED")) {
+                if (now - last_upload_failed_logged > TimeSpan.FromMinutes(FAILURE_LOG_MINUTES)) {
+                    Hyena.Log.Warning ("Audioscrobbler upload failed", line.Substring ("FAILED".Length).Trim(), false);
+                    last_upload_failed_logged = now;
+                }
+                /* retransmit the queue on the next interval */
+                hard_failures++;
+                state = State.NEED_TRANSMIT;
+            }
+            else if (line.StartsWith ("BADSESSION")) {
+                if (now - last_upload_failed_logged > TimeSpan.FromMinutes(FAILURE_LOG_MINUTES)) {
+                    Hyena.Log.Warning ("Audioscrobbler upload failed", "session ID sent was invalid", false);
+                    last_upload_failed_logged = now;
+                }
+                /* attempt to re-handshake (and retransmit) on the next interval */
+                session_id = null;
+                next_interval = DateTime.Now + new TimeSpan (0, 0, RETRY_SECONDS);
+                state = State.NEED_HANDSHAKE;
+                return;
+            }
+            else if (line.StartsWith ("OK")) {
+                /* if we've previously logged failures, be nice and log the successful upload. */
+                if (last_upload_failed_logged != DateTime.MinValue) {
+                    Hyena.Log.Debug ("Audioscrobbler upload succeeded");
+                    last_upload_failed_logged = DateTime.MinValue;
+                }
+                /* we succeeded, pop the elements off our queue */
+                queue.RemoveRange (0, ts.Count);
+                queue.Save ();
+                if (queue.Count == 0) {
+                    // Don't wake up all the time - sleep for a while.
+                    StopTransitionHandler ();
+                }
+                
+                state = State.IDLE;
+            }
+            else {
+                if (now - last_upload_failed_logged > TimeSpan.FromMinutes(FAILURE_LOG_MINUTES)) {
+                    Hyena.Log.Warning ("Audioscrobbler upload failed", String.Format ("Unrecognized response: {0}", line));
+                    last_upload_failed_logged = now;
+                }
+                state = State.IDLE;
+            }
+        }
+
+        //
+        // Async code for handshaking
+        //
+        
+        private string UnixTime ()
+        {
+            return ((int) (DateTime.UtcNow - new DateTime (1970, 1, 1)).TotalSeconds).ToString ();
+        }
+        
+        void Handshake ()
+        {
+            string timestamp = UnixTime();
+            string security_token = Hyena.CryptoUtil.Md5Encode (account.CryptedPassword + timestamp);
+
+            string uri = String.Format ("{0}?hs=true&p={1}&c={2}&v={3}&u={4}&t={5}&a={6}",
+                                        SCROBBLER_URL,
+                                        SCROBBLER_VERSION,
+                                        CLIENT_ID, CLIENT_VERSION,
+                                        HttpUtility.UrlEncode (account.UserName),
+                                        timestamp,
+                                        security_token);
+
+            current_web_req = WebRequest.Create (uri);
+
+            state = State.WAITING_FOR_HANDSHAKE_RESP;
+            current_async_result = current_web_req.BeginGetResponse (HandshakeGetResponse, null);
+            if (current_async_result == null) {
+                next_interval = DateTime.Now + new TimeSpan (0, 0, hard_failure_retry_sec);
+                hard_failures++;
+                if (hard_failure_retry_sec < MAX_RETRY_SECONDS)
+                    hard_failure_retry_sec *= 2;
+                state = State.NEED_HANDSHAKE;
+            }
+        }
+
+        void HandshakeGetResponse (IAsyncResult ar)
+        {
+            bool success = false;
+            bool hard_failure = false;
+            WebResponse resp;
+
+            try {
+                resp = current_web_req.EndGetResponse (ar);
+            }
+            catch (Exception e) {
+                Hyena.Log.Warning ("Failed to handshake: {0}", e.ToString (), false);
+
+                /* back off for a time before trying again */
+                state = State.IDLE;
+                next_interval = DateTime.Now + new TimeSpan (0, 0, RETRY_SECONDS);
+                return;
+            }
+
+            Stream s = resp.GetResponseStream ();
+
+            StreamReader sr = new StreamReader (s, Encoding.UTF8);
+
+            string line;
+
+            line = sr.ReadLine ();
+            if (line.StartsWith ("BANNED")) {
+                Hyena.Log.Warning ("Audioscrobbler sign-on failed", "Player is banned", false);
+                                   
+            }
+            else if (line.StartsWith ("BADAUTH")) {
+                // FIXME: Show to user? :s
+                Hyena.Log.Warning ("Audioscrobbler sign-on failed", "Unrecognized user/password");
+            }
+            else if (line.StartsWith ("BADTIME")) {
+                Hyena.Log.Warning ("Audioscrobbler sign-on failed", 
+                                                  "timestamp provided was not close enough to the current time", false);
+            }
+            else if (line.StartsWith ("FAILED")) {
+                Hyena.Log.Warning ("Audioscrobbler sign-on failed",
+                                                  String.Format ("Temporary server failure: {0}",
+                                                                  line.Substring ("FAILED".Length).Trim()), false);
+                hard_failure = true;
+            }
+            else if (line.StartsWith ("OK")) {
+                success = true;
+            } else {
+                Hyena.Log.Error ("Audioscrobbler sign-on failed", 
+                                                  String.Format ("Unknown error: {0}",
+                                                                  line.Trim()), false);
+                hard_failure = true;
+            }
+            
+            if (success == true) {
+                Hyena.Log.Debug ("Audioscrobbler sign-on succeeded", "Session ID received"); 
+                session_id = sr.ReadLine ().Trim ();
+                now_playing_url = sr.ReadLine ().Trim ();
+                post_url = sr.ReadLine ().Trim ();
+                
+                hard_failures = 0;
+                hard_failure_retry_sec = 60;
+            }
+            else {
+                if (hard_failure == true) {
+                    next_interval = DateTime.Now + new TimeSpan (0, 0, hard_failure_retry_sec);
+                    hard_failures++;
+                    if (hard_failure_retry_sec < MAX_RETRY_SECONDS)
+                        hard_failure_retry_sec *= 2;
+                }
+            }
+
+            /* XXX we shouldn't just try to handshake again for BADUSER */
+            state = success ? State.IDLE : State.NEED_HANDSHAKE;
+        }
+        
+        //
+        // Async code for now playing
+        
+        /*void NowPlaying (TrackInfo track)
+
+        {
+            if (session_id != null && track.Artist != "" && track.Title != "") {
+                
+                string str_track_number = "";
+                if (track.TrackNumber != 0)
+                    str_track_number = track.TrackNumber.ToString();
+                
+                string uri = String.Format ("{0}?s={1}&a={2}&t={3}&b={4}&l={5}&n={6}&m={7}",
+                                            now_playing_url,
+                                            session_id,
+                                            HttpUtility.UrlEncode(track.Artist),
+                                            HttpUtility.UrlEncode(track.Title),
+    			                            HttpUtility.UrlEncode(track.Album),
+                                            track.Duration.TotalSeconds.ToString(),
+                                            str_track_number,
+    			                            "" *//* musicbrainz id *//*);
+
+                now_playing_post = WebRequest.Create (uri);
+                now_playing_post.Method = "POST";
+                now_playing_post.ContentType = "application/x-www-form-urlencoded";
+                now_playing_post.ContentLength = uri.Length;
+                now_playing_post.BeginGetResponse (NowPlayingGetResponse, null);
+                now_playing_submitted = true;
+            }
+        }
+
+        void NowPlayingGetResponse (IAsyncResult ar)
+        {
+            try {
+
+                WebResponse my_resp = now_playing_post.EndGetResponse (ar);
+
+                Stream s = my_resp.GetResponseStream ();
+                StreamReader sr = new StreamReader (s, Encoding.UTF8);
+
+                string line = sr.ReadLine ();
+                if (line.StartsWith ("BADSESSION")) {
+                    Hyena.Log.Warning ("Audioscrobbler NowPlaying failed", "Session ID sent was invalid", false);*/
+                    /* attempt to re-handshake on the next interval */
+                    /*session_id = null;
+                    next_interval = DateTime.Now + new TimeSpan (0, 0, RETRY_SECONDS);
+                    state = State.NEED_HANDSHAKE;
+                    return;
+                }
+                else if (line.StartsWith ("OK")) {
+                    // NowPlaying submitted  
+                }
+                else {
+                    Hyena.Log.Warning ("Audioscrobbler NowPlaying failed", "Unexpected or no response", false);       
+                }
+            }
+            catch (Exception e) {
+                Hyena.Log.Error ("Audioscrobbler NowPlaying failed", 
+                              String.Format("Failed to post NowPlaying: {0}", e));
+            }
+        }*/
+    }
+}

Added: trunk/banshee/src/Libraries/Lastfm/Lastfm/IQueue.cs
==============================================================================
--- (empty file)
+++ trunk/banshee/src/Libraries/Lastfm/Lastfm/IQueue.cs	Sat Feb 23 08:35:36 2008
@@ -0,0 +1,49 @@
+//
+// IQueue.cs
+//
+// Authors:
+//   Alexander Hixon <hixon alexander mediati org>
+//
+// Copyright (C) 2008 Alexander Hixon
+//
+// 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;
+
+namespace Lastfm
+{
+    public interface IQueue
+    {
+        event EventHandler TrackAdded;
+        
+        int Count {
+            get;
+        }
+        
+        void Save ();
+        void Load ();
+        
+        string GetTransmitInfo (out int numtracks);
+        
+        void Add (object track, DateTime started);
+        void RemoveRange (int first, int count);
+    }
+}

Modified: trunk/banshee/src/Libraries/Lastfm/Makefile.am
==============================================================================
--- trunk/banshee/src/Libraries/Lastfm/Makefile.am	(original)
+++ trunk/banshee/src/Libraries/Lastfm/Makefile.am	Sat Feb 23 08:35:36 2008
@@ -4,7 +4,9 @@
 
 SOURCES = \
 	Lastfm/Account.cs \
+	Lastfm/AudioscrobblerConnection.cs \
 	Lastfm/Browser.cs \
+	Lastfm/IQueue.cs \
 	Lastfm/RadioConnection.cs \
 	Lastfm.Data/DataCore.cs \
 	Lastfm.Data/DataEntry.cs \



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