[banshee] Metadata ratings and playcount import and export (bgo#532650)



commit 16047c8167559da34a8b9b3ce7b9106c7a1dc59b
Author: Nicholas Parker <nickbp gmail com>
Date:   Tue Nov 17 15:00:54 2009 +1100

    Metadata ratings and playcount import and export (bgo#532650)
    
    Signed-off-by: Alexander Kojevnikov <alexander kojevnikov com>

 .../Banshee.Base/Tests/TaglibReadWriteTests.cs     |    8 +-
 .../Banshee.Configuration.Schema/LibrarySchema.cs  |    7 +
 src/Core/Banshee.Core/Banshee.Core.csproj          |    4 +-
 .../Banshee.Streaming/StreamRatingTagger.cs        |  380 ++++++++++++++++++++
 .../Banshee.Core/Banshee.Streaming/StreamTagger.cs |   72 +++--
 src/Core/Banshee.Core/Makefile.am                  |    1 +
 .../DatabaseImportManager.cs                       |    3 +-
 .../DatabaseTrackInfo.cs                           |   40 ++-
 .../Banshee.Metadata/SaveTrackMetadataJob.cs       |    7 +-
 .../Banshee.Metadata/SaveTrackMetadataService.cs   |   20 +-
 .../Banshee.Preferences/PreferenceService.cs       |    3 +-
 .../Banshee.Library.Gui/ImportDialog.cs            |    1 +
 .../Banshee.Dap.MassStorage/MassStorageSource.cs   |    4 +-
 13 files changed, 498 insertions(+), 52 deletions(-)
---
diff --git a/src/Core/Banshee.Core/Banshee.Base/Tests/TaglibReadWriteTests.cs b/src/Core/Banshee.Core/Banshee.Base/Tests/TaglibReadWriteTests.cs
index ffe6bba..8a94b8b 100644
--- a/src/Core/Banshee.Core/Banshee.Base/Tests/TaglibReadWriteTests.cs
+++ b/src/Core/Banshee.Core/Banshee.Base/Tests/TaglibReadWriteTests.cs
@@ -180,13 +180,13 @@ namespace Banshee.Base.Tests
             change (track);
 
             // Save changes
-            bool saved = StreamTagger.SaveToFile (track);
+            bool saved = StreamTagger.SaveToFile (track, true, true);
             Assert.IsTrue (saved);
 
             // Read changes
             file = StreamTagger.ProcessUri (uri);
             track = new TrackInfo ();
-            StreamTagger.TrackInfoMerge (track, file);
+            StreamTagger.TrackInfoMerge (track, file, false, true);
 
             // Verify changes
             verify (track);
@@ -200,6 +200,8 @@ namespace Banshee.Base.Tests
             track.TrackNumber = 4;
             track.DiscNumber = 4;
             track.Year = 1999;
+            track.Rating = 2;
+            track.PlayCount = 3;
         }
 
         private void VerifyTrack (TrackInfo track)
@@ -210,6 +212,8 @@ namespace Banshee.Base.Tests
             Assert.AreEqual (4, track.TrackNumber);
             Assert.AreEqual (4, track.DiscNumber);
             Assert.AreEqual (1999, track.Year);
+            Assert.AreEqual (2, track.Rating);
+            Assert.AreEqual (3, track.PlayCount);
         }
 
         private Type unix_io_type;
diff --git a/src/Core/Banshee.Core/Banshee.Configuration.Schema/LibrarySchema.cs b/src/Core/Banshee.Core/Banshee.Configuration.Schema/LibrarySchema.cs
index a83fe4f..6a17f7d 100644
--- a/src/Core/Banshee.Core/Banshee.Configuration.Schema/LibrarySchema.cs
+++ b/src/Core/Banshee.Core/Banshee.Configuration.Schema/LibrarySchema.cs
@@ -77,5 +77,12 @@ namespace Banshee.Configuration.Schema
             "Write metadata back to audio files",
             "If enabled, metadata (tags) will be written back to audio files when using the track metadata editor."
         );
+
+        public static readonly SchemaEntry<bool> WriteRatingsAndPlayCounts = new SchemaEntry<bool>(
+            "library", "write_rating",
+            false,
+            "Store ratings within supported files",
+            "If enabled, rating and playcount metadata will be written back to audio files."
+        );
     }
 }
diff --git a/src/Core/Banshee.Core/Banshee.Core.csproj b/src/Core/Banshee.Core/Banshee.Core.csproj
index aa201bd..f1bda3c 100644
--- a/src/Core/Banshee.Core/Banshee.Core.csproj
+++ b/src/Core/Banshee.Core/Banshee.Core.csproj
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"; ToolsVersion="3.5">
   <PropertyGroup>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -111,6 +111,8 @@
     <Compile Include="Banshee.Collection\TrackFilterType.cs" />
     <Compile Include="Banshee.Base\PlatformHacks.cs" />
     <Compile Include="Banshee.Streaming\CommonTags.cs" />
+    <Compile Include="Banshee.Streaming\SaveTrackMetadataJob.cs" />
+    <Compile Include="Banshee.Streaming\StreamRatingTagger.cs" />
     <Compile Include="Banshee.Streaming\StreamTag.cs" />
     <Compile Include="Banshee.Streaming\StreamTagger.cs" />
     <Compile Include="Banshee.Streaming\StreamPlaybackError.cs" />
diff --git a/src/Core/Banshee.Core/Banshee.Streaming/StreamRatingTagger.cs b/src/Core/Banshee.Core/Banshee.Streaming/StreamRatingTagger.cs
new file mode 100644
index 0000000..0551c28
--- /dev/null
+++ b/src/Core/Banshee.Core/Banshee.Streaming/StreamRatingTagger.cs
@@ -0,0 +1,380 @@
+//
+// StreamRatingTagger.cs
+//
+// Author:
+//   Nicholas Parker <nickbp gmail com>
+//
+// Copyright (C) 2008-2009 Nicholas Parker
+//
+// 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 Banshee.Collection;
+using System.Collections;
+
+namespace Banshee.Streaming
+{
+    internal static class ID3v2RatingTagger
+    {
+        // What we call ourselves in POPM tags.
+        private static string POPM_our_creator_name = "Banshee";
+
+        // Ordered list of ID3v2 POPM authors to attempt when importing.
+        // Banshee must be listed first, to ensure that we give priority to our own ratings.
+        // If new entries are added to this list, also make sure that
+        // PopmToBanshee and BansheeToPopm are still accurate.
+        private static string[] POPM_known_creator_list = {
+            POPM_our_creator_name,// This item must be first
+            "quodlibet lists sacredchao net",// Quod Libet (their default)
+            "Windows Media Player 9 Series",// WMP/Vista
+            "no email",// MediaMonkey
+            "mcored gmail com" // iTSfv
+        };
+
+        // Converts ID3v2 POPM rating to Banshee rating
+        private static int PopmToBanshee (byte popm_rating)
+        {
+            // The following schemes are used by the other POPM-compatible players:
+            // WMP/Vista: "Windows Media Player 9 Series" ratings:
+            //   1 = 1, 2 = 64, 3=128, 4=196 (not 192), 5=255
+            // MediaMonkey: "no email" ratings:
+            //   0.5=26, 1=51, 1.5=76, 2=102, 2.5=128,
+            //   3=153, 3.5=178, 4=204, 4.5=230, 5=255
+            // Quod Libet: "quodlibet lists sacredchao net" ratings
+            //   (but that email can be changed):
+            //   arbitrary scale from 0-255
+            // Compatible with all these rating scales (what we'll use):
+            //   unrated=0, 1=1-63, 2=64-127, 3=128-191, 4=192-254, 5=255
+            if (popm_rating == 0x0)// unrated
+                return 0;
+            if (popm_rating < 0x40)// 1-63
+                return 1;
+            if (popm_rating < 0x80)// 64-127
+                return 2;
+            if (popm_rating < 0xC0)// 128-191
+                return 3;
+            if (popm_rating < 0xFF)// 192-254
+                return 4;
+            return 5;// 255
+        }
+
+        // Converts Banshee rating to ID3v2 POPM rating
+        private static byte BansheeToPopm (int banshee_rating)
+        {
+            switch (banshee_rating) {
+            case 1:
+                return 0x1;
+            case 2:
+                return 0x40;// 64
+            case 3:
+                return 0x80;// 128
+            case 4:
+                return 0xC0;// 192
+            case 5:
+                return 0xFF;// 255
+            default:
+                return 0x0;// unrated/unknown
+            }
+        }
+
+        private static TagLib.Id3v2.Tag GetTag (TagLib.File file)
+        {
+            try {
+                return file.GetTag (TagLib.TagTypes.Id3v2) as TagLib.Id3v2.Tag;
+            } catch (System.NullReferenceException e) {
+                // TagLib# can crash here on unusual files (Ex: FLAC files with ID3v2 metadata)
+                // Perhaps FLAC+ID3v2 is an unsupported combination for TagLib#?
+                Hyena.Log.WarningFormat ("Got exception when accessing ID3v2 Metadata in {0}:",
+                                         file.Name);
+                Hyena.Log.Warning (e.Message);
+                Hyena.Log.Warning (e.StackTrace);
+                return null;
+            }
+        }
+
+        // Overwrites all POPM frames with the new rating and playcount.
+        // If no *known-compatible* frames are found, a new "Banshee"-authored
+        // frame is also created to store this information.
+        public static void StoreRatingAndPlayCount (int rating, int playcount,
+                                                    TagLib.File to_file)
+        {
+            TagLib.Id3v2.Tag id3v2tag = GetTag (to_file);
+            if (id3v2tag == null) {
+                return;
+            }
+
+            bool known_frames_found = false;
+            foreach (TagLib.Id3v2.PopularimeterFrame popm in
+                     id3v2tag.GetFrames<TagLib.Id3v2.PopularimeterFrame> ()) {
+                if (System.Array.IndexOf (POPM_known_creator_list, popm.User) >= 0) {
+                    // Found a known-good POPM frame, don't need to create a "Banshee" frame.
+                    known_frames_found = true;
+                }
+
+                popm.Rating = BansheeToPopm (rating);
+                popm.PlayCount = (ulong)playcount;
+                Hyena.Log.DebugFormat ("Exporting ID3v2 Rating={0}({1}) and Playcount={2}({3}) to File \"{4}\" as Creator \"{5}\"",
+                                       rating, popm.Rating,
+                                       playcount, popm.PlayCount,
+                                       to_file.Name, popm.User);
+            }
+
+            if (!known_frames_found) {
+                // No known-good frames found, create a new POPM frame (with creator string "Banshee")
+                TagLib.Id3v2.PopularimeterFrame popm = TagLib.Id3v2.PopularimeterFrame.Get (id3v2tag,
+                                                                                            POPM_our_creator_name,
+                                                                                            true);
+                popm.Rating = BansheeToPopm (rating);
+                popm.PlayCount = (ulong)playcount;
+                Hyena.Log.DebugFormat ("Exporting ID3v2 Rating={0}({1}) and Playcount={2}({3}) to File \"{4}\" as Creator \"{5}\"",
+                                       rating, popm.Rating,
+                                       playcount, popm.PlayCount,
+                                       to_file.Name, POPM_our_creator_name);
+            }
+        }
+
+        // Scans the file for *known-compatible* POPM frames, with priority given to
+        // frames at the top of the known creator list.
+        public static void GetRatingAndPlayCount (TagLib.File from_file,
+                                                  ref int rating, ref int playcount)
+        {
+            TagLib.Id3v2.Tag id3v2tag = GetTag (from_file);
+            if (id3v2tag == null) {
+                return;
+            }
+
+            TagLib.Id3v2.PopularimeterFrame popm = null;
+            for (int i = 0; i < POPM_known_creator_list.Length; i++) {
+                popm = TagLib.Id3v2.PopularimeterFrame.Get (id3v2tag,
+                                                            POPM_known_creator_list[i],
+                                                            false);
+                if (popm != null) {
+                    break;
+                }
+            }
+
+            if (popm != null) {
+                rating = PopmToBanshee (popm.Rating);
+                playcount = (int)popm.PlayCount;
+                Hyena.Log.DebugFormat ("Importing ID3v2 Rating={0}({1}) and Playcount={2}({3}) from File \"{4}\" as Creator \"{5}\"",
+                                       rating, popm.Rating,
+                                       playcount, popm.PlayCount,
+                                       from_file.Name, popm.User);
+            }
+        }
+    }
+
+    // Applicable for Vorbis, Speex, and many (most?) FLAC files
+    // Follows the naming standard established by the Quod Libet team
+    // See: http://code.google.com/p/quodlibet/wiki/Specs_VorbisComments
+    internal static class OggRatingTagger
+    {
+        // What we call ourselves in rating/playcount tags.
+        private static string ogg_our_creator_name = "BANSHEE";
+
+        // Prefix to rating field names (lowercase)
+        private static string rating_prefix = "RATING:";
+
+        // Prefix to playcount field names (lowercase)
+        private static string playcount_prefix = "PLAYCOUNT:";
+
+        // Converts Ogg rating to Banshee rating
+        private static int OggToBanshee (string ogg_rating_str)
+        {
+            double ogg_rating = double.Parse (ogg_rating_str);
+            // Quod Libet Ogg ratings are stored as a value
+            // between 0.0 and 1.0 inclusive, where unrated = 0.5.
+            if (ogg_rating == 0.5)// unrated
+                return 0;
+            if (ogg_rating > 0.8)// (0.8,1.0]
+                return 5;
+            if (ogg_rating > 0.6)// (0.6,0.8]
+                return 4;
+            if (ogg_rating > 0.4)// (0.4,0.5),(0.5,0.6]
+                return 3;
+            if (ogg_rating > 0.2)// (0.2,0.4]
+                return 2;
+            else // [0.0,0.2]
+                return 1;
+        }
+
+        // Converts Banshee rating to Ogg rating
+        private static string BansheeToOgg (int banshee_rating)
+        {
+            // I went with this scaling so that if we switch to fractional stars
+            // in the future (such as "0.25 stars"), we'll have room for that.
+            switch (banshee_rating) {
+            case 1:
+                return "0.2";
+            case 2:
+                return "0.4";
+            case 3:
+                return "0.6";
+            case 4:
+                return "0.8";
+            case 5:
+                return "1.0";
+            default:
+                return "0.5";// unrated/unknown
+            }
+        }
+
+        private static TagLib.Ogg.XiphComment GetTag (TagLib.File file)
+        {
+            try {
+                return file.GetTag (TagLib.TagTypes.Xiph) as TagLib.Ogg.XiphComment;
+            } catch (System.NullReferenceException e) {
+                // Haven't seen crashes when getting Ogg tags, but just in case..
+                // (See commentary for ID3v2 version)
+                Hyena.Log.WarningFormat ("Got exception when accessing Ogg Metadata in {0}:",
+                                         file.Name);
+                Hyena.Log.Warning (e.Message);
+                Hyena.Log.Warning (e.StackTrace);
+                return null;
+            }
+        }
+
+        // Scans the file for ogg rating/playcount tags as defined by the Quod Libet standard
+        // If a Banshee tag is found, it is given priority.
+        // If a Banshee tag is not found, the last rating/playcount tags found are used
+        public static void GetRatingAndPlayCount (TagLib.File from_file,
+                                                  ref int rating, ref int playcount)
+        {
+            TagLib.Ogg.XiphComment xiphtag = GetTag (from_file);
+            if (xiphtag == null) {
+                return;
+            }
+
+            bool banshee_rating_done = false, banshee_playcount_done = false;
+            string rating_raw = "", playcount_raw = "";
+
+            foreach (string fieldname in xiphtag) {
+
+                if (!banshee_rating_done &&
+                    fieldname.ToUpper ().StartsWith (rating_prefix)) {
+
+                    rating_raw = xiphtag.GetFirstField (fieldname);
+                    string rating_creator = fieldname.Substring (rating_prefix.Length);
+                    if (rating_creator.ToUpper () == ogg_our_creator_name) {
+                        // We made this rating, consider it authoritative.
+                        banshee_rating_done = true;
+                        // Don't return -- we might not have seen a playcount yet.
+                    }
+
+                } else if (!banshee_playcount_done &&
+                           fieldname.ToUpper ().StartsWith (playcount_prefix)) {
+
+                    playcount_raw = xiphtag.GetFirstField (fieldname);
+                    string playcount_creator = fieldname.Substring (playcount_prefix.Length);
+                    if (playcount_creator.ToUpper () == ogg_our_creator_name) {
+                        // We made this playcount, consider it authoritative.
+                        banshee_playcount_done = true;
+                        // Don't return -- we might not have seen a rating yet.
+                    }
+                }
+            }
+            if (rating_raw != "") {
+                rating = OggToBanshee (rating_raw);
+            }
+            if (playcount_raw != "") {
+                playcount = int.Parse (playcount_raw);
+            }
+            Hyena.Log.DebugFormat ("Importing Ogg Rating={0}({1}) and Playcount={2}({3}) from File \"{4}\"",
+                                   rating, rating_raw,
+                                   playcount, playcount_raw, from_file.Name);
+        }
+
+        // Scans the file for ogg rating/playcount tags as defined by the Quod Libet standard
+        // All applicable tags are overwritten with the new values, regardless of tag author
+        public static void StoreRatingAndPlayCount (int rating, int playcount,
+                                                    TagLib.File to_file)
+        {
+            TagLib.Ogg.XiphComment xiphtag = GetTag (to_file);
+            if (xiphtag == null) {
+                return;
+            }
+
+            ArrayList rating_fieldnames = new ArrayList ();
+            ArrayList playcount_fieldnames = new ArrayList ();
+
+            // Collect list of rating and playcount tags to be updated:
+            foreach (string fieldname in xiphtag) {
+                if (fieldname.ToUpper ().StartsWith (rating_prefix)) {
+                    rating_fieldnames.Add (fieldname);
+                } else if (fieldname.ToUpper ().StartsWith (playcount_prefix)) {
+                    playcount_fieldnames.Add (fieldname);
+                }
+            }
+            // Add "BANSHEE" tags if no rating/playcount tags were found:
+            if (rating_fieldnames.Count == 0) {
+                rating_fieldnames.Add (rating_prefix+ogg_our_creator_name);
+            }
+            if (playcount_fieldnames.Count == 0) {
+                playcount_fieldnames.Add (playcount_prefix+ogg_our_creator_name);
+            }
+
+            string ogg_rating = BansheeToOgg (rating);
+            foreach (string ratingname in rating_fieldnames) {
+                xiphtag.SetField (ratingname, ogg_rating);
+                Hyena.Log.DebugFormat ("Exporting Ogg Rating={0}({1}) to File \"{2}\" as Creator \"{3}\"",
+                                       rating, ogg_rating,
+                                       to_file.Name,
+                                       ratingname.Substring (rating_prefix.Length));
+            }
+            string ogg_playcount = playcount.ToString ();
+            foreach (string playcountname in playcount_fieldnames) {
+                xiphtag.SetField (playcountname, ogg_playcount);
+                Hyena.Log.DebugFormat ("Exporting Ogg Playcount={0}({1}) to File \"{2}\" as Creator \"{3}\"",
+                                       playcount, ogg_playcount,
+                                       to_file.Name,
+                                       playcountname.Substring (playcount_prefix.Length));
+            }
+        }
+    }
+
+    public static class StreamRatingTagger
+    {
+        public static void GetRatingAndPlayCount (TagLib.File from_file,
+                                                  ref int rating, ref int playcount)
+        {
+            if ((from_file.Tag.TagTypes & TagLib.TagTypes.Id3v2) != 0) {
+                ID3v2RatingTagger.GetRatingAndPlayCount (from_file,
+                                                         ref rating, ref playcount);
+            }
+            if ((from_file.Tag.TagTypes & TagLib.TagTypes.Xiph) != 0) {
+                OggRatingTagger.GetRatingAndPlayCount (from_file,
+                                                       ref rating, ref playcount);
+            }
+        }
+
+        public static void StoreRatingAndPlayCount (int rating, int playcount,
+                                                    TagLib.File to_file)
+        {
+            if ((to_file.Tag.TagTypes & TagLib.TagTypes.Id3v2) != 0) {
+                ID3v2RatingTagger.StoreRatingAndPlayCount (rating, playcount,
+                                                           to_file);
+            }
+            if ((to_file.Tag.TagTypes & TagLib.TagTypes.Xiph) != 0) {
+                OggRatingTagger.StoreRatingAndPlayCount (rating, playcount,
+                                                         to_file);
+            }
+        }
+    }
+}
diff --git a/src/Core/Banshee.Core/Banshee.Streaming/StreamTagger.cs b/src/Core/Banshee.Core/Banshee.Streaming/StreamTagger.cs
index 1260560..589dfaa 100644
--- a/src/Core/Banshee.Core/Banshee.Streaming/StreamTagger.cs
+++ b/src/Core/Banshee.Core/Banshee.Streaming/StreamTagger.cs
@@ -134,6 +134,11 @@ namespace Banshee.Streaming
 
         public static void TrackInfoMerge (TrackInfo track, TagLib.File file, bool preferTrackInfo)
         {
+            TrackInfoMerge (track, file, preferTrackInfo, false);
+        }
+
+        public static void TrackInfoMerge (TrackInfo track, TagLib.File file, bool preferTrackInfo, bool import_rating_and_play_count)
+        {
             // TODO support these as arrays:
             // Performers[] (track artists), AlbumArtists[], Composers[], Genres[]
 
@@ -170,6 +175,13 @@ namespace Banshee.Streaming
                 track.DiscCount = Choose ((int)file.Tag.DiscCount, track.DiscCount, preferTrackInfo);
                 track.Year = Choose ((int)file.Tag.Year, track.Year, preferTrackInfo);
                 track.Bpm = Choose ((int)file.Tag.BeatsPerMinute, track.Bpm, preferTrackInfo);
+
+                if (import_rating_and_play_count) {
+                    int file_rating = 0, file_playcount = 0;
+                    StreamRatingTagger.GetRatingAndPlayCount (file, ref file_rating, ref file_playcount);
+                    track.Rating = Choose (file_rating, track.Rating, preferTrackInfo);
+                    track.PlayCount = Choose (file_playcount, track.PlayCount, preferTrackInfo);
+                }
             } else {
                 track.MediaAttributes = TrackMediaAttributes.AudioStream;
                 if (track.Uri != null && VideoExtensions.IsMatchingFile (track.Uri.LocalPath)) {
@@ -239,7 +251,7 @@ namespace Banshee.Streaming
             } catch {}
         }
 
-        public static bool SaveToFile (TrackInfo track)
+        public static bool SaveToFile (TrackInfo track, bool write_metadata, bool write_rating_and_play_count)
         {
             // FIXME taglib# does not seem to handle writing metadata to video files well at all atm
             // so not allowing
@@ -254,31 +266,39 @@ namespace Banshee.Streaming
                 return false;
             }
 
-            file.Tag.Performers = new string [] { track.ArtistName };
-            file.Tag.PerformersSort = new string [] { track.ArtistNameSort };
-            file.Tag.Album = track.AlbumTitle;
-            file.Tag.AlbumSort = track.AlbumTitleSort;
-            file.Tag.AlbumArtists = track.AlbumArtist == null ? new string [0] : new string [] {track.AlbumArtist};
-            file.Tag.AlbumArtistsSort = (track.AlbumArtistSort == null ? new string [0] : new string [] {track.AlbumArtistSort});
-            // Bug in taglib-sharp-2.0.3.0: Crash if you send it a genre of "{ null }"
-            // on a song with both ID3v1 and ID3v2 metadata. It's happy with "{}", though.
-            // (see http://forum.taglib-sharp.com/viewtopic.php?f=5&t=239 )
-            file.Tag.Genres = (track.Genre == null) ? new string[] {} : new string [] { track.Genre };
-            file.Tag.Title = track.TrackTitle;
-            file.Tag.TitleSort = track.TrackTitleSort;
-            file.Tag.Track = (uint)track.TrackNumber;
-            file.Tag.TrackCount = (uint)track.TrackCount;
-            file.Tag.Composers = new string [] { track.Composer };
-            file.Tag.Conductor = track.Conductor;
-            file.Tag.Grouping = track.Grouping;
-            file.Tag.Copyright = track.Copyright;
-            file.Tag.Comment = track.Comment;
-            file.Tag.Disc = (uint)track.DiscNumber;
-            file.Tag.DiscCount = (uint)track.DiscCount;
-            file.Tag.Year = (uint)track.Year;
-            file.Tag.BeatsPerMinute = (uint)track.Bpm;
-
-            SaveIsCompilation (file, track.IsCompilation);
+            if (write_metadata) {
+                file.Tag.Performers = new string [] { track.ArtistName };
+                file.Tag.PerformersSort = new string [] { track.ArtistNameSort };
+                file.Tag.Album = track.AlbumTitle;
+                file.Tag.AlbumSort = track.AlbumTitleSort;
+                file.Tag.AlbumArtists = track.AlbumArtist == null ? new string [0] : new string [] {track.AlbumArtist};
+                file.Tag.AlbumArtistsSort = (track.AlbumArtistSort == null ? new string [0] : new string [] {track.AlbumArtistSort});
+                // Bug in taglib-sharp-2.0.3.0: Crash if you send it a genre of "{ null }"
+                // on a song with both ID3v1 and ID3v2 metadata. It's happy with "{}", though.
+                // (see http://forum.taglib-sharp.com/viewtopic.php?f=5&t=239 )
+                file.Tag.Genres = (track.Genre == null) ? new string[] {} : new string [] { track.Genre };
+                file.Tag.Title = track.TrackTitle;
+                file.Tag.TitleSort = track.TrackTitleSort;
+                file.Tag.Track = (uint)track.TrackNumber;
+                file.Tag.TrackCount = (uint)track.TrackCount;
+                file.Tag.Composers = new string [] { track.Composer };
+                file.Tag.Conductor = track.Conductor;
+                file.Tag.Grouping = track.Grouping;
+                file.Tag.Copyright = track.Copyright;
+                file.Tag.Comment = track.Comment;
+                file.Tag.Disc = (uint)track.DiscNumber;
+                file.Tag.DiscCount = (uint)track.DiscCount;
+                file.Tag.Year = (uint)track.Year;
+                file.Tag.BeatsPerMinute = (uint)track.Bpm;
+
+                SaveIsCompilation (file, track.IsCompilation);
+            }
+
+            if (write_rating_and_play_count) {
+                // FIXME move StreamRatingTagger to taglib#
+                StreamRatingTagger.StoreRatingAndPlayCount (track.Rating, track.PlayCount, file);
+            }
+
             file.Save ();
 
             track.FileSize = Banshee.IO.File.GetSize (track.Uri);
diff --git a/src/Core/Banshee.Core/Makefile.am b/src/Core/Banshee.Core/Makefile.am
index de474b2..ccf03e9 100644
--- a/src/Core/Banshee.Core/Makefile.am
+++ b/src/Core/Banshee.Core/Makefile.am
@@ -63,6 +63,7 @@ SOURCES =  \
 	Banshee.Kernel/Scheduler.cs \
 	Banshee.Streaming/CommonTags.cs \
 	Banshee.Streaming/StreamPlaybackError.cs \
+	Banshee.Streaming/StreamRatingTagger.cs \
 	Banshee.Streaming/StreamTag.cs \
 	Banshee.Streaming/StreamTagger.cs
 
diff --git a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseImportManager.cs b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseImportManager.cs
index 5b6d5c7..03afd5d 100644
--- a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseImportManager.cs
+++ b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseImportManager.cs
@@ -40,6 +40,7 @@ using Banshee.Base;
 using Banshee.Sources;
 using Banshee.Collection;
 using Banshee.Collection.Database;
+using Banshee.Configuration.Schema;
 using Banshee.ServiceStack;
 using Banshee.Streaming;
 
@@ -175,7 +176,7 @@ namespace Banshee.Collection.Database
             try {
                 track = new DatabaseTrackInfo ();
                 track.Uri = uri;
-                StreamTagger.TrackInfoMerge (track, StreamTagger.ProcessUri (uri));
+                StreamTagger.TrackInfoMerge (track, StreamTagger.ProcessUri (uri), false, true);
 
                 track.PrimarySource = trackPrimarySourceChooser (track);
 
diff --git a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackInfo.cs b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackInfo.cs
index cfdedff..a96c644 100644
--- a/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackInfo.cs
+++ b/src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackInfo.cs
@@ -39,6 +39,8 @@ using Hyena.Query;
 using Banshee.Base;
 using Banshee.Configuration.Schema;
 using Banshee.Database;
+using Banshee.Metadata;
+using Banshee.Preferences;
 using Banshee.Query;
 using Banshee.Sources;
 using Banshee.ServiceStack;
@@ -72,6 +74,33 @@ namespace Banshee.Collection.Database
             Provider.Copy (original, this);
         }
 
+        // Changing these fields shouldn't change DateUpdated (which triggers file save)
+        private static readonly HashSet<QueryField> transient_fields;
+
+        static DatabaseTrackInfo ()
+        {
+            transient_fields = new HashSet<QueryField> () {
+                BansheeQuery.ScoreField,
+                BansheeQuery.SkipCountField,
+                BansheeQuery.LastSkippedField,
+                BansheeQuery.LastPlayedField,
+                BansheeQuery.PlaybackErrorField,
+                BansheeQuery.PlayCountField,
+                BansheeQuery.RatingField
+            };
+            Action<Root> handler = delegate {
+                if (SaveTrackMetadataService.WriteRatingsAndPlayCountsEnabled.Value) {
+                    transient_fields.Remove (BansheeQuery.PlayCountField);
+                    transient_fields.Remove (BansheeQuery.RatingField);
+                } else {
+                    transient_fields.Add (BansheeQuery.PlayCountField);
+                    transient_fields.Add (BansheeQuery.RatingField);
+                }
+            };
+            SaveTrackMetadataService.WriteRatingsAndPlayCountsEnabled.ValueChanged += handler;
+            handler (null);
+        }
+
         public override void OnPlaybackFinished (double percentCompleted)
         {
             if (ProviderRefresh()) {
@@ -127,17 +156,6 @@ namespace Banshee.Collection.Database
             Save (NotifySaved);
         }
 
-        // Changing these fields shouldn't change DateUpdated (which triggers file save)
-        private static HashSet<QueryField> transient_fields = new HashSet<QueryField> {
-            BansheeQuery.ScoreField,
-            BansheeQuery.SkipCountField,
-            BansheeQuery.LastSkippedField,
-            BansheeQuery.PlayCountField,
-            BansheeQuery.LastPlayedField,
-            BansheeQuery.RatingField,
-            BansheeQuery.PlaybackErrorField
-        };
-
         public void Save (bool notify, params QueryField [] fields_changed)
         {
             // If either the artist or album changed,
diff --git a/src/Core/Banshee.Services/Banshee.Metadata/SaveTrackMetadataJob.cs b/src/Core/Banshee.Services/Banshee.Metadata/SaveTrackMetadataJob.cs
index 52081e3..c999d69 100644
--- a/src/Core/Banshee.Services/Banshee.Metadata/SaveTrackMetadataJob.cs
+++ b/src/Core/Banshee.Services/Banshee.Metadata/SaveTrackMetadataJob.cs
@@ -63,7 +63,8 @@ namespace Banshee.Metadata
             );
         }
 
-        public bool WriteEnabled { get; set; }
+        public bool WriteMetadataEnabled { get; set; }
+        public bool WriteRatingsAndPlayCountsEnabled { get; set; }
         public bool RenameEnabled { get; set; }
 
         private HyenaSqliteCommand update_synced_at;
@@ -75,9 +76,9 @@ namespace Banshee.Metadata
             bool wrote = false;
             bool renamed = false;
             try {
-                if (WriteEnabled) {
+                if (WriteMetadataEnabled || WriteRatingsAndPlayCountsEnabled) {
                     Hyena.Log.DebugFormat ("Saving metadata for {0}", track);
-                    wrote = StreamTagger.SaveToFile (track);
+                    wrote = StreamTagger.SaveToFile (track, WriteMetadataEnabled, WriteRatingsAndPlayCountsEnabled);
                 }
 
                 if (RenameEnabled) {
diff --git a/src/Core/Banshee.Services/Banshee.Metadata/SaveTrackMetadataService.cs b/src/Core/Banshee.Services/Banshee.Metadata/SaveTrackMetadataService.cs
index e2bcc98..2587aa9 100644
--- a/src/Core/Banshee.Services/Banshee.Metadata/SaveTrackMetadataService.cs
+++ b/src/Core/Banshee.Services/Banshee.Metadata/SaveTrackMetadataService.cs
@@ -43,12 +43,17 @@ namespace Banshee.Metadata
 {
     public class SaveTrackMetadataService : IService
     {
-        public static SchemaPreference<bool> WriteEnabled = new SchemaPreference<bool> (
+        public static SchemaPreference<bool> WriteMetadataEnabled = new SchemaPreference<bool> (
                 LibrarySchema.WriteMetadata,
                 Catalog.GetString ("Write _metadata to files"),
                 Catalog.GetString ("Save tags and other metadata inside supported media files")
         );
 
+        public static SchemaPreference<bool> WriteRatingsAndPlayCountsEnabled = new SchemaPreference<bool> (
+                LibrarySchema.WriteRatingsAndPlayCounts,
+                Catalog.GetString ("Write _ratings and play counts to files"),
+                Catalog.GetString ("Enable this option to save rating and playcount metadata inside supported audio files."));
+
         public static SchemaPreference<bool> RenameEnabled = new SchemaPreference<bool> (
                 LibrarySchema.MoveOnInfoSave,
                 Catalog.GetString ("_Update file and folder names"),
@@ -62,7 +67,8 @@ namespace Banshee.Metadata
         public SaveTrackMetadataService ()
         {
             Banshee.ServiceStack.Application.RunTimeout (10000, delegate {
-                WriteEnabled.ValueChanged += OnEnabledChanged;
+                WriteMetadataEnabled.ValueChanged += OnEnabledChanged;
+                WriteRatingsAndPlayCountsEnabled.ValueChanged += OnEnabledChanged;
                 RenameEnabled.ValueChanged += OnEnabledChanged;
                 ServiceManager.SourceManager.MusicLibrary.TracksChanged += OnTracksChanged;
                 Save ();
@@ -84,16 +90,18 @@ namespace Banshee.Metadata
 
         private void Save ()
         {
-            if (!(WriteEnabled.Value || RenameEnabled.Value))
+            if (!(WriteMetadataEnabled.Value || WriteRatingsAndPlayCountsEnabled.Value || RenameEnabled.Value))
                 return;
 
             lock (sync) {
                 if (job != null) {
-                    job.WriteEnabled  = WriteEnabled.Value;
+                    job.WriteMetadataEnabled  = WriteMetadataEnabled.Value;
+                    job.WriteRatingsAndPlayCountsEnabled = WriteRatingsAndPlayCountsEnabled.Value;
                     job.RenameEnabled = RenameEnabled.Value;
                 } else {
                     var new_job = new SaveTrackMetadataJob ();
-                    new_job.WriteEnabled  = WriteEnabled.Value;
+                    new_job.WriteMetadataEnabled  = WriteMetadataEnabled.Value;
+                    new_job.WriteRatingsAndPlayCountsEnabled = WriteRatingsAndPlayCountsEnabled.Value;
                     new_job.RenameEnabled = RenameEnabled.Value;
                     new_job.Finished += delegate { lock (sync) { job = null; } };
                     job = new_job;
@@ -109,7 +117,7 @@ namespace Banshee.Metadata
 
         private void OnEnabledChanged (Root pref)
         {
-            if (WriteEnabled.Value || RenameEnabled.Value) {
+            if (WriteMetadataEnabled.Value || WriteRatingsAndPlayCountsEnabled.Value || RenameEnabled.Value) {
                 Save ();
             } else {
                 if (job != null) {
diff --git a/src/Core/Banshee.Services/Banshee.Preferences/PreferenceService.cs b/src/Core/Banshee.Services/Banshee.Preferences/PreferenceService.cs
index bcea2da..3b4d63d 100644
--- a/src/Core/Banshee.Services/Banshee.Preferences/PreferenceService.cs
+++ b/src/Core/Banshee.Services/Banshee.Preferences/PreferenceService.cs
@@ -56,7 +56,8 @@ namespace Banshee.Preferences
             policies.Add (new SchemaPreference<bool> (LibrarySchema.CopyOnImport,
                 Catalog.GetString ("Co_py files to media folders when importing")));
 
-            policies.Add (Banshee.Metadata.SaveTrackMetadataService.WriteEnabled);
+            policies.Add (Banshee.Metadata.SaveTrackMetadataService.WriteMetadataEnabled);
+            policies.Add (Banshee.Metadata.SaveTrackMetadataService.WriteRatingsAndPlayCountsEnabled);
             policies.Add (Banshee.Metadata.SaveTrackMetadataService.RenameEnabled);
 
             // Misc section
diff --git a/src/Core/Banshee.ThickClient/Banshee.Library.Gui/ImportDialog.cs b/src/Core/Banshee.ThickClient/Banshee.Library.Gui/ImportDialog.cs
index a3696e4..722abcd 100644
--- a/src/Core/Banshee.ThickClient/Banshee.Library.Gui/ImportDialog.cs
+++ b/src/Core/Banshee.ThickClient/Banshee.Library.Gui/ImportDialog.cs
@@ -33,6 +33,7 @@ using Glade;
 
 using Mono.Unix;
 
+using Banshee.Configuration.Schema;
 using Banshee.Sources;
 using Banshee.ServiceStack;
 using Banshee.Gui;
diff --git a/src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage/MassStorageSource.cs b/src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage/MassStorageSource.cs
index 8150950..ba753a0 100644
--- a/src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage/MassStorageSource.cs
+++ b/src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage/MassStorageSource.cs
@@ -492,7 +492,9 @@ namespace Banshee.Dap.MassStorage
 
                 if (track.LastSyncedStamp >= Hyena.DateTimeUtil.ToDateTime (track.FileModifiedStamp)) {
                     Log.DebugFormat ("Copying Metadata to File Since Sync time >= Updated Time");
-                    Banshee.Streaming.StreamTagger.SaveToFile (copied_track);
+                    bool write_metadata = Metadata.SaveTrackMetadataService.WriteMetadataEnabled.Value;
+                    bool write_ratings_and_playcounts = Metadata.SaveTrackMetadataService.WriteRatingsAndPlayCountsEnabled.Value;
+                    Banshee.Streaming.StreamTagger.SaveToFile (copied_track, write_metadata, write_ratings_and_playcounts);
                 }
 
                 copied_track.Save (false);



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