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
- From: ahixon svn gnome org
- To: svn-commits-list gnome org
- Subject: 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
- Date: Sat, 23 Feb 2008 08:35:37 +0000 (GMT)
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]