[banshee] LastFM: Add device scrobbling support (bgo#536389)



commit 3973d135783933ff6e9ab665429100fb8ee09bf9
Author: Phil Trimble <philtrimble gmail com>
Date:   Tue May 1 13:51:23 2012 +0100

    LastFM: Add device scrobbling support (bgo#536389)
    
    Enables Last.fm scrobbling of recent plays from a connected device.
    Currently only available on devices supported by the AppleDevice
    extension. The new functionality happens upon connection of a device.
    
    Adds a new preference to control the functionality that will be
    disabled by default. Also updates the Last.fm help page to explain
    how it works.
    
    (Really push the modified files, the previous commit was just
    an incomplete changeset that only included a new file.)
    
    Signed-off-by: Andres G. Aragoneses <knocte gmail com>

 help/C/lastfm.page                                 |   30 ++++-
 src/Core/Banshee.Services/Banshee.Services.csproj  |    1 +
 src/Core/Banshee.Services/Makefile.am              |    1 +
 .../Banshee.Dap.AppleDevice/AppleDeviceSource.cs   |   75 ++++++++++-
 .../AudioscrobblerService.cs                       |  140 +++++++++++++++++++-
 .../Banshee.Lastfm/LastfmPreferences.cs            |   12 ++-
 .../Resources/AudioscrobblerMenu.xml               |    1 +
 7 files changed, 252 insertions(+), 8 deletions(-)
---
diff --git a/help/C/lastfm.page b/help/C/lastfm.page
index 410cf6f..25cd618 100644
--- a/help/C/lastfm.page
+++ b/help/C/lastfm.page
@@ -55,12 +55,12 @@
   </section>
   
   <section id="songreporting">
-  <title>Enable Last.fm Song Reporting</title> 
+  <title>Enable Last.fm Song Reporting From Banshee</title> 
   <p>After you have successfully linked Banshee to your Last.fm profile you must
   ensure that you have enabled Banshee to report your songs.  To enable Banshee to
   report the songs to your Last.fm profile go to Banshee's preferences, select the
   <gui>Source Specific</gui> tab, select <gui>Last.fm</gui> from the dropdown, and
-  press the <gui>Enable Song Reporting</gui> checkbox. If you have an active internet
+  press the <gui>Enable Song Reporting From Banshee</gui> checkbox. If you have an active internet
   connection Banshee will now send Last.fm information regarding the songs
   you play.  To view your play history visit your profile on the Last.fm
   website.  Last.fm will automatically update your music metadata if any of your
@@ -70,6 +70,32 @@
   </p>
   
   </section>
+
+  <section id="devicesongreporting">
+  <title>Enable Last.fm Song Reporting From Your Device</title>
+  <p>After successfully linking Banshee to your Last.fm profile and enabling Banshee to 
+  report songs to Last.fm you can also enable scrobbling from a connected device. Banshee 
+  will, upon connection of your device, attempt to scrobble the songs you have played since the 
+  device was last connected and submit them to Last.fm.
+  </p>
+
+  <p>To enable scrobbling of a connected device go to Banshee's preferences, select the 
+  <gui>Source Specific</gui> tab, select <gui>Last.fm</gui> from the dropdown, and press 
+  the <gui>Enable Song Reporting From Device</gui> checkbox.  If you have an 
+  active internet connection Banshee will, upon connection of your device, now attempt to gather 
+  information regarding the songs that you have played since it was last connected.
+  </p>
+
+  <p>As with regular Banshee scrobbling submissions Last.fm will automatically update 
+  your music metadata if any of your artist, title, or album information is incorrect (although we 
+  again recommend that you use the Metadata Fixer extension to correct your files instead).
+  </p>
+
+  <p>Please note that currently Banshee only supports this feature with Apple products that are
+  supported by the AppleDevice extension.
+  </p>
+
+  </section>
   
   <section id="lastfm-radio">
   <title>Listen to Last.fm Radio</title> 
diff --git a/src/Core/Banshee.Services/Banshee.Services.csproj b/src/Core/Banshee.Services/Banshee.Services.csproj
index 804478a..b52262f 100644
--- a/src/Core/Banshee.Services/Banshee.Services.csproj
+++ b/src/Core/Banshee.Services/Banshee.Services.csproj
@@ -327,6 +327,7 @@
     <Compile Include="Banshee.Collection.Database\DatabaseYearInfo.cs" />
     <Compile Include="Banshee.Collection.Database\DatabaseYearListModel.cs" />
     <Compile Include="Banshee.Collection\InvalidFileException.cs" />
+    <Compile Include="Banshee.Sources\IBatchScrobblerSource.cs" />
   </ItemGroup>
   <ItemGroup>
     <EmbeddedResource Include="Banshee.Services.addin.xml">
diff --git a/src/Core/Banshee.Services/Makefile.am b/src/Core/Banshee.Services/Makefile.am
index 935cec9..3bf10ae 100644
--- a/src/Core/Banshee.Services/Makefile.am
+++ b/src/Core/Banshee.Services/Makefile.am
@@ -210,6 +210,7 @@ SOURCES =  \
 	Banshee.Sources/DatabaseSource.cs \
 	Banshee.Sources/DurationStatusFormatters.cs \
 	Banshee.Sources/ErrorSource.cs \
+	Banshee.Sources/IBatchScrobblerSource.cs \
 	Banshee.Sources/IDiskUsageReporter.cs \
 	Banshee.Sources/IDurationAggregator.cs \
 	Banshee.Sources/IFileSizeAggregator.cs \
diff --git a/src/Dap/Banshee.Dap.AppleDevice/Banshee.Dap.AppleDevice/AppleDeviceSource.cs b/src/Dap/Banshee.Dap.AppleDevice/Banshee.Dap.AppleDevice/AppleDeviceSource.cs
index bbe3773..450345c 100644
--- a/src/Dap/Banshee.Dap.AppleDevice/Banshee.Dap.AppleDevice/AppleDeviceSource.cs
+++ b/src/Dap/Banshee.Dap.AppleDevice/Banshee.Dap.AppleDevice/AppleDeviceSource.cs
@@ -3,8 +3,12 @@
 //
 // Author:
 //   Alan McGovern <amcgovern novell com>
+//   Phil Trimble <philtrimble gmail com>
+//   Andres G. Aragoneses <knocte gmail com>
 //
 // Copyright (C) 2010 Novell, Inc.
+// Copyright (C) 2012 Phil Trimble
+// Copyright (C) 2012 Andres G. Aragoneses
 //
 // Permission is hereby granted, free of charge, to any person obtaining
 // a copy of this software and associated documentation files (the
@@ -42,10 +46,11 @@ using Banshee.Hardware;
 using Banshee.Sources;
 using Banshee.I18n;
 using Banshee.Playlist;
+using Banshee.Collection;
 
 namespace Banshee.Dap.AppleDevice
 {
-    public class AppleDeviceSource : DapSource
+    public class AppleDeviceSource : DapSource, IBatchScrobblerSource
     {
         GPod.Device Device {
             get; set;
@@ -61,6 +66,8 @@ namespace Banshee.Dap.AppleDevice
 
         private Dictionary<int, AppleDeviceTrackInfo> tracks_map = new Dictionary<int, AppleDeviceTrackInfo> (); // FIXME: EPIC FAIL
 
+        public event EventHandler<ScrobblingBatchEventArgs> ReadyToScrobble;
+
 #region Device Setup/Dispose
 
         public override void DeviceInitialize (IDevice device)
@@ -247,6 +254,8 @@ namespace Banshee.Dap.AppleDevice
                 pl_src.UpdateCounts ();
                 AddChildSource (pl_src);
             }
+
+            RaiseReadyToScrobble ();
         }
 
 #endregion
@@ -380,6 +389,7 @@ namespace Banshee.Dap.AppleDevice
         private uint sync_timeout_id = 0;
         private object sync_timeout_mutex = new object ();
         private object sync_mutex = new object ();
+        private object write_mutex = new object ();
         private Thread sync_thread;
         private AutoResetEvent sync_thread_wait;
         private bool sync_thread_dispose = false;
@@ -648,7 +658,11 @@ namespace Banshee.Dap.AppleDevice
             try {
                 message = Catalog.GetString ("Writing media database");
                 UpdateProgress (progressUpdater, message, 1, 1);
-                MediaDatabase.Write ();
+
+                lock (write_mutex) {
+                    MediaDatabase.Write ();
+                }
+
                 Log.Information ("Wrote iPod database");
             } catch (Exception e) {
                 Log.Exception ("Failed to save iPod database", e);
@@ -679,5 +693,62 @@ namespace Banshee.Dap.AppleDevice
 
 #endregion
 
+#region Scrobbling
+
+        private void RaiseReadyToScrobble ()
+        {
+            var handler = ReadyToScrobble;
+            if (handler != null) {
+                var recent_plays = new ScrobblingBatchEventArgs {
+                    ScrobblingBatch = GatherRecentPlayInfo ()
+                };
+                if (recent_plays.ScrobblingBatch.Count != 0) {
+                    handler (this, recent_plays);
+
+                    // We must perform a write to clear out the recent playcount information so we do not
+                    // submit duplicate plays on subsequent invocations.
+                    lock (write_mutex) {
+                        MediaDatabase.Write ();
+                    }
+                }
+            }
+        }
+
+        private IDictionary<TrackInfo, IList<DateTime>> GatherRecentPlayInfo ()
+        {
+            var recent_plays = new Dictionary <TrackInfo, IList<DateTime>> ();
+
+            foreach (var ipod_track in MediaDatabase.Tracks) {
+
+                if (String.IsNullOrEmpty (ipod_track.IpodPath) || ipod_track.RecentPlayCount == 0) {
+                    continue;
+                }
+
+                IList<DateTime> playtimes = GenerateFakePlaytimes (ipod_track);
+
+                recent_plays [new AppleDeviceTrackInfo (ipod_track)] = playtimes;
+            }
+
+            return recent_plays;
+        }
+
+        // Apple products do not save DateTime info for each track play, only a total
+        // sum of number of plays (playcount) of each track.
+        private IList<DateTime> GenerateFakePlaytimes (GPod.Track track)
+        {
+            IList<DateTime> playtimes = new List<DateTime> ();
+
+            //FIXME: avoid sequences of overlapping playtimes?
+            DateTime current_playtime = track.TimePlayed;
+            for (int i = 0; i < track.RecentPlayCount; i++) {
+                playtimes.Add (current_playtime);
+                current_playtime -= TimeSpan.FromMilliseconds (track.TrackLength);
+            }
+
+            return playtimes;
+        }
+
+#endregion
+
     }
 }
diff --git a/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs b/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs
index dc87acb..c33477f 100644
--- a/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs
+++ b/src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Audioscrobbler/AudioscrobblerService.cs
@@ -35,10 +35,12 @@ using System.IO;
 using System.Net;
 using System.Text;
 using System.Security.Cryptography;
+using System.Collections.Generic;
 using Gtk;
 using Mono.Unix;
 
 using Hyena;
+using Hyena.Jobs;
 
 using Lastfm;
 
@@ -48,6 +50,7 @@ using Banshee.Configuration;
 using Banshee.ServiceStack;
 using Banshee.Gui;
 using Banshee.Networking;
+using Banshee.Sources;
 
 using Banshee.Collection;
 
@@ -116,6 +119,10 @@ namespace Banshee.Lastfm.Audioscrobbler
                 PlayerEvent.Seek |
                 PlayerEvent.Iterate);
 
+            if (DeviceEnabled) {
+                SubscribeForDeviceEvents ();
+            }
+
             action_service = ServiceManager.Get<InterfaceActionService> ();
             InterfaceInitialize ();
         }
@@ -136,8 +143,14 @@ namespace Banshee.Lastfm.Audioscrobbler
 
             actions.Add (new ToggleActionEntry [] {
                 new ToggleActionEntry ("AudioscrobblerEnableAction", null,
-                    Catalog.GetString ("_Enable Song Reporting"), null,
-                    Catalog.GetString ("Enable song reporting"), OnToggleEnabled, Enabled)
+                    Catalog.GetString ("_Enable Song Reporting From Banshee"), null,
+                    Catalog.GetString ("Enable song reporting From Banshee"), OnToggleEnabled, Enabled)
+            });
+
+            actions.Add (new ToggleActionEntry [] {
+                new ToggleActionEntry ("AudioscrobblerDeviceEnableAction", null,
+                    Catalog.GetString ("_Enable Song Reporting From Device"), null,
+                    Catalog.GetString ("Enable song reporting From Device"), OnToggleDeviceEnabled, DeviceEnabled)
             });
 
             action_service.UIManager.InsertActionGroup (actions, 0);
@@ -159,6 +172,10 @@ namespace Banshee.Lastfm.Audioscrobbler
 
             ServiceManager.Get<Network> ().StateChanged -= HandleNetworkStateChanged;
 
+            if (DeviceEnabled) {
+                UnsubscribeForDeviceEvents ();
+            }
+
             // When we stop the connection, queue ends up getting saved too, so the
             // track we queued earlier should stay until next session.
             connection.Stop ();
@@ -168,6 +185,26 @@ namespace Banshee.Lastfm.Audioscrobbler
             actions = null;
         }
 
+        List<IBatchScrobblerSource> sources_watched;
+
+        private void SubscribeForDeviceEvents ()
+        {
+            sources_watched = new List<IBatchScrobblerSource> ();
+            ServiceManager.SourceManager.SourceAdded += OnSourceAdded;
+            ServiceManager.SourceManager.SourceRemoved += OnSourceRemoved;
+        }
+
+        private void UnsubscribeForDeviceEvents ()
+        {
+            ServiceManager.SourceManager.SourceAdded -= OnSourceAdded;
+            ServiceManager.SourceManager.SourceRemoved -= OnSourceRemoved;
+            foreach (var source in sources_watched) {
+                source.ReadyToScrobble -= OnReadyToScrobble;
+            }
+            sources_watched.Clear ();
+            sources_watched = null;
+        }
+
         private void HandleNetworkStateChanged (object o, NetworkStateChangedArgs args)
         {
             connection.UpdateNetworkState (args.Connected);
@@ -306,6 +343,11 @@ namespace Banshee.Lastfm.Audioscrobbler
             Enabled = ((ToggleAction) o).Active;
         }
 
+        private void OnToggleDeviceEnabled (object o, EventArgs args)
+        {
+            DeviceEnabled = ((ToggleAction) o).Active;
+        }
+
         internal bool Enabled {
             get { return EngineEnabledSchema.Get (); }
             set {
@@ -314,6 +356,93 @@ namespace Banshee.Lastfm.Audioscrobbler
             }
         }
 
+        internal bool DeviceEnabled {
+            get { return DeviceEngineEnabledSchema.Get (); }
+            set {
+                if (DeviceEnabled == value)
+                    return;
+
+                DeviceEngineEnabledSchema.Set (value);
+                ((ToggleAction) actions["AudioscrobblerDeviceEnableAction"]).Active = value;
+
+                if (value) {
+                    SubscribeForDeviceEvents ();
+                } else {
+                    UnsubscribeForDeviceEvents ();
+                }
+            }
+        }
+
+#region scrobbling
+
+        private void OnSourceAdded (SourceEventArgs args)
+        {
+            var scrobbler_source = args.Source as IBatchScrobblerSource;
+            if (scrobbler_source == null) {
+                return;
+            }
+
+            scrobbler_source.ReadyToScrobble += OnReadyToScrobble;
+            sources_watched.Add (scrobbler_source);
+        }
+
+        private void OnSourceRemoved (SourceEventArgs args)
+        {
+            var scrobbler_source = args.Source as IBatchScrobblerSource;
+            if (scrobbler_source == null) {
+                return;
+            }
+
+            sources_watched.Remove (scrobbler_source);
+            scrobbler_source.ReadyToScrobble -= OnReadyToScrobble;
+        }
+
+        private void OnReadyToScrobble (object source, ScrobblingBatchEventArgs args)
+        {
+            var scrobble_job = new UserJob (Catalog.GetString ("Scrobbling from device"),
+                                            Catalog.GetString ("Scrobbling from device..."));
+
+            scrobble_job.PriorityHints = PriorityHints.DataLossIfStopped;
+            scrobble_job.Register ();
+
+            try {
+                if (!connection.Started) {
+                    connection.Start ();
+                }
+    
+                int added_track_count = 0, processed_track_count = 0;
+                string message = Catalog.GetString ("Processing track {0} of {1} ...");
+                var batchCount = args.ScrobblingBatch.Count;
+    
+                foreach (var track_entry in args.ScrobblingBatch) {
+                    TrackInfo track = track_entry.Key;
+    
+                    if (IsValidForSubmission (track)) {
+                        IList<DateTime> playtimes = track_entry.Value;
+    
+                        foreach (DateTime playtime in playtimes) {
+                            queue.Add (track, playtime);
+                            added_track_count++;
+                        }
+                        Log.DebugFormat ("Added to Last.fm queue: {0} - Number of plays: {1}", track, playtimes.Count);
+                    } else {
+                        Log.DebugFormat ("Track {0} failed validation check for Last.fm submission, skipping...",
+                                         track);
+                    }
+    
+                    scrobble_job.Status = String.Format (message, ++processed_track_count, batchCount);
+                    scrobble_job.Progress = processed_track_count / (double) batchCount;
+                }
+    
+                Log.InformationFormat ("Number of played tracks from device added to Last.fm queue: {0}", added_track_count);
+
+            } finally {
+                scrobble_job.Finish ();
+            }
+        }
+
+#endregion
+
         public static readonly SchemaEntry<string> LastUserSchema = new SchemaEntry<string> (
             "plugins.lastfm", "username", "", "Last.fm user", "Last.fm username"
         );
@@ -336,6 +465,13 @@ namespace Banshee.Lastfm.Audioscrobbler
             "Audioscrobbler reporting engine enabled"
         );
 
+        public static readonly SchemaEntry<bool> DeviceEngineEnabledSchema = new SchemaEntry<bool> (
+            "plugins.audioscrobbler", "device_engine_enabled",
+            false,
+            "Device engine enabled",
+            "Audioscrobbler device reporting engine enabled"
+        );
+
         string IService.ServiceName {
             get { return "AudioscrobblerService"; }
         }
diff --git a/src/Extensions/Banshee.Lastfm/Banshee.Lastfm/LastfmPreferences.cs b/src/Extensions/Banshee.Lastfm/Banshee.Lastfm/LastfmPreferences.cs
index 0a1261d..ac1c77d 100644
--- a/src/Extensions/Banshee.Lastfm/Banshee.Lastfm/LastfmPreferences.cs
+++ b/src/Extensions/Banshee.Lastfm/Banshee.Lastfm/LastfmPreferences.cs
@@ -3,6 +3,7 @@
 // 
 // Author:
 //   Aaron Bockover <abockover novell com>
+//   Phil Trimble <philtrimble gmail com>
 // 
 // Copyright (c) 2010 Novell, Inc.
 // 
@@ -49,6 +50,7 @@ namespace Banshee.Lastfm
         private Section prefs_section;
         private SchemaPreference<string> username_preference;
         private Preference<bool> reporting_preference;
+        private Preference<bool> reporting_device_preference;
         private LastfmSource source;
         private AudioscrobblerService scrobbler;
 
@@ -88,9 +90,14 @@ namespace Banshee.Lastfm
             scrobbler = ServiceManager.Get<Banshee.Lastfm.Audioscrobbler.AudioscrobblerService> ();
             if (scrobbler != null) {
                 reporting_preference = new Preference<bool> ("enable-song-reporting",
-                    Catalog.GetString ("_Enable Song Reporting"), null, scrobbler.Enabled);
+                    Catalog.GetString ("_Enable Song Reporting From Banshee"), null, scrobbler.Enabled);
                 reporting_preference.ValueChanged += root => scrobbler.Enabled = reporting_preference.Value;
                 prefs_section.Add (reporting_preference);
+
+                reporting_device_preference = new Preference<bool> ("enable-device-song-reporting",
+                    Catalog.GetString ("_Enable Song Reporting From Device"), null, scrobbler.DeviceEnabled);
+                reporting_device_preference.ValueChanged += root => scrobbler.DeviceEnabled = reporting_device_preference.Value;
+                prefs_section.Add (reporting_device_preference);
             }
         }
 
@@ -122,8 +129,9 @@ namespace Banshee.Lastfm
 
         private void OnPreferencesServiceInstallWidgetAdapters (object sender, EventArgs args)
         {
-            if (reporting_preference != null && scrobbler != null) {
+            if (reporting_preference != null && reporting_device_preference != null && scrobbler != null) {
                 reporting_preference.Value = scrobbler.Enabled;
+                reporting_device_preference.Value = scrobbler.DeviceEnabled;
             }
 
             if (account_section == null) {
diff --git a/src/Extensions/Banshee.Lastfm/Resources/AudioscrobblerMenu.xml b/src/Extensions/Banshee.Lastfm/Resources/AudioscrobblerMenu.xml
index bde075c..eeaa101 100644
--- a/src/Extensions/Banshee.Lastfm/Resources/AudioscrobblerMenu.xml
+++ b/src/Extensions/Banshee.Lastfm/Resources/AudioscrobblerMenu.xml
@@ -3,6 +3,7 @@
         <menu name="ToolsMenu" action="ToolsMenuAction">
             <menu name="Audioscrobbler" action="AudioscrobblerAction">
                     <menuitem name="AudioscrobblerEnable" action="AudioscrobblerEnableAction" />
+                    <menuitem name="AudioscrobblerDeviceEnable" action="AudioscrobblerDeviceEnableAction" />                    
                     <menuitem name="AudioscrobblerVisit" action="AudioscrobblerVisitAction" />
             </menu>
         </menu>



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