=== added file 'src/Core/Banshee.Core/Banshee.Base/ArtworkKey.cs'
--- src/Core/Banshee.Core/Banshee.Base/ArtworkKey.cs	1970-01-01 00:00:00 +0000
+++ src/Core/Banshee.Core/Banshee.Base/ArtworkKey.cs	2009-02-28 21:14:36 +0000
@@ -0,0 +1,118 @@
+//
+// ArtworkKey.cs
+//
+// Author:
+//   John Millikin <jmillikin@gmail.com>
+//
+// 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;
+
+namespace Banshee.Base
+{
+    public class ArtworkKey
+    {
+        private readonly string album_id;
+        private readonly string local_dir;
+        
+        public string AlbumId {
+            get { return album_id; }
+        }
+        
+        public string LocalDirectory {
+            get { return local_dir; }
+        }
+        
+        public virtual string[] ArtworkIds {
+            get { return new string[] { AlbumId }; }
+        }
+        
+        protected static string PickArtist (string album, string track)
+        {
+            return String.IsNullOrEmpty (album)? track : album;
+        }
+        
+        protected ArtworkKey (string album_id): this (null, album_id) {}
+        
+        protected ArtworkKey (string local_dir, string album_id)
+        {
+            this.local_dir = String.IsNullOrEmpty (local_dir)? null : local_dir;
+            this.album_id = album_id;
+        }
+        
+        public static ArtworkKey ForAlbum (string artist, string title)
+        {
+            return new ArtworkKey (String.Format ("album-{0}-{1}",
+                                   CoverArtSpec.DigestPart (artist),
+                                   CoverArtSpec.DigestPart (title)));
+        }
+        
+        public static ArtworkKey ForRadio (string station_name)
+        {
+            return new ArtworkKey (String.Format ("radio-{0}",
+                                   CoverArtSpec.DigestPart (station_name)));
+        }
+        
+        public static ArtworkKey ForPodcast (string title)
+        {
+            return new ArtworkKey (String.Format ("podcast-{0}",
+                                   CoverArtSpec.DigestPart (title)));
+        }
+    }
+    
+    public class TrackArtworkKey : ArtworkKey
+    {
+        private readonly string track_id;
+        
+        public string TrackId {
+            get { return track_id; }
+        }
+        
+        public override string[] ArtworkIds {
+            get { return new string[] { TrackId, AlbumId }; }
+        }
+        
+        protected TrackArtworkKey (string local_dir, string album_id, string track_id)
+        : base (local_dir, album_id)
+        {
+            this.track_id = track_id;
+        }
+        
+        public static TrackArtworkKey ForTrack (string path, string album_artist,
+                                                string track_artist, string album_title,
+                                                string track_title)
+        {
+            string dirname = String.IsNullOrEmpty (path)
+                ? null
+                : Path.GetDirectoryName (path);
+            
+            string artist_digest = CoverArtSpec.DigestPart (PickArtist (album_artist, track_artist));
+            string album_digest = CoverArtSpec.DigestPart (album_title);
+            string track_digest = CoverArtSpec.DigestPart (track_title);
+            
+            string album_id = String.Format ("album-{0}-{1}", artist_digest, album_digest);
+            string track_id = String.Format ("track-{0}-{1}-{2}", artist_digest, album_digest, track_digest);
+            
+            return new TrackArtworkKey (dirname, album_id, track_id);
+        }
+    }
+}

=== modified file 'src/Core/Banshee.Core/Banshee.Base/CoverArtSpec.cs'
--- src/Core/Banshee.Core/Banshee.Base/CoverArtSpec.cs	2008-10-01 18:18:47 +0000
+++ src/Core/Banshee.Core/Banshee.Base/CoverArtSpec.cs	2009-02-28 23:30:11 +0000
@@ -28,7 +28,9 @@
 
 using System;
 using System.IO;
-using System.Text.RegularExpressions;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.Text;
 
 using Mono.Unix;
 
@@ -36,79 +38,161 @@
 {
     public static class CoverArtSpec
     {
-        private static string unknown_artist_tr = Catalog.GetString ("Unknown Artist");
-        private static string unknown_artist = "Unknown Artist";
-        private static string unknown_album_tr = Catalog.GetString ("Unknown Album");
-        private static string unknown_album = "Unknown Album";
-
-        public static bool CoverExists (string artist, string album)
-        {
-            return CoverExists (CreateArtistAlbumId (artist, album));
-        }
-    
-        public static bool CoverExists (string aaid)
-        {
-            return CoverExistsForSize (aaid, 0);
-        }
-        
-        public static bool CoverExistsForSize (string aaid, int size)
-        {
-            return aaid == null ? false : File.Exists (GetPathForSize (aaid, size));
-        }
-        
-        public static string GetPath (string aaid)
-        {
-            return GetPathForSize (aaid, 0);
-        }
-    
-        public static string GetPathForSize (string aaid, int size)
-        {
-            return size == 0
-                ? Path.Combine (RootPath, String.Format ("{0}.jpg", aaid))
-                : Path.Combine (RootPath, Path.Combine (size.ToString (), String.Format ("{0}.jpg", aaid))); 
-        }
-    
-        public static string CreateArtistAlbumId (string artist, string album)
-        {
-            return CreateArtistAlbumId (artist, album, false);
-        }
-
-        public static string CreateArtistAlbumId (string artist, string album, bool asUriPart)
-        {
-            if (artist == unknown_artist || artist == unknown_artist_tr || album == unknown_album || album == unknown_album_tr) {
-                return null;
-            }
-
-            string sm_artist = EscapePart (artist);
-            string sm_album = EscapePart (album);
-
-            return String.IsNullOrEmpty (sm_artist) || String.IsNullOrEmpty (sm_album)
-                ? null 
-                : String.Format ("{0}{1}{2}", sm_artist, asUriPart ? "/" : "-", sm_album); 
-        }
+        // Characters that should be ignored when generating the artwork ID
+        private static char[] unwanted_chars = new char[] {
+            '!', '@', '#', '$', '^', '&', '*', '+', '=', '_',
+            '|', '\\', '/', '"', '\'', '`', '?', '~'
+        };
+        
+        // Characters used to designate unwanted notes, and their corresponding
+        // terminator
+        private static Dictionary<char, char> BuildNoteChars ()
+        {
+            Dictionary<char, char> dict = new Dictionary<char, char> ();
+            dict['('] = ')';
+            dict['{'] = '}';
+            dict['['] = ']';
+            dict['<'] = '>';
+            return dict;
+        }
+        private static Dictionary<char, char> note_chars = BuildNoteChars ();
+        private static HashAlgorithm artwork_digest = (HashAlgorithm) MD5.Create ();
+        
+        private static string root_path = Paths.Combine (XdgBaseDirectorySpec.GetUserDirectory (
+            "XDG_CACHE_HOME", ".cache"),  "media-art");
         
         public static string EscapePart (string part)
         {
             if (String.IsNullOrEmpty (part)) {
+                return "";
+            }
+            
+            StringBuilder sb = new StringBuilder ();
+            
+            bool got_space = false;
+            char? skip_terminator = null;
+            foreach (char c in part.ToLower ()) {
+                if (skip_terminator != null) {
+                    if (c == skip_terminator) {
+                        skip_terminator = null;
+                    }
+                    continue;
+                }
+                
+                if (note_chars.ContainsKey (c)) {
+                    skip_terminator = note_chars[c];
+                    continue;
+                }
+                
+                if (c == ' ' || c == '\t') {
+                    if (!got_space) {
+                        sb.Append (" ");
+                        got_space = true;
+                    }
+                    continue;
+                }
+                got_space = false;
+                
+                if (Array.IndexOf (unwanted_chars, c) == -1) {
+                    sb.Append (c);
+                }
+            }
+            
+            return sb.ToString ().Normalize (NormalizationForm.FormKD);
+        }
+        
+        public static string DigestPart (string part)
+        {
+            string escaped = EscapePart (part);
+            if (String.IsNullOrEmpty (escaped)) {
+                escaped = " ";
+            }
+            byte[] escaped_bytes = Encoding.UTF8.GetBytes (escaped);
+            byte[] digest = artwork_digest.ComputeHash (escaped_bytes);
+            return BitConverter.ToString (digest).Replace ("-", "").ToLower ();
+        }
+        
+        private static string GetLocalPath (ArtworkKey key, string art_id)
+        {
+            return String.IsNullOrEmpty (key.LocalDirectory)
+                ? null
+                : Paths.Combine (key.LocalDirectory, ".mediaartlocal",
+                                 String.Format ("{0}.jpeg", art_id));
+        }
+        
+        private static string GetCachePath (string art_id)
+        {
+            return Paths.Combine (root_path, String.Format ("{0}.jpeg", art_id));
+        }
+        
+        public static bool ArtworkExists (ArtworkKey key)
+        {
+            foreach (string art_id in key.ArtworkIds) {
+                if (File.Exists (GetLocalPath (key, art_id))) {
+                    return true;
+                }
+            }
+            
+            foreach (string art_id in key.ArtworkIds) {
+                if (File.Exists (GetCachePath (art_id))) {
+                    return true;
+                }
+            }
+            
+            return false;
+        }
+        
+        public static bool TrackArtworkExists (ArtworkKey key)
+        {
+            TrackArtworkKey track_key = key as TrackArtworkKey;
+            if (track_key == null) {
+                return false;
+            }
+            return (File.Exists (GetLocalPath (key, track_key.TrackId)) ||
+                    File.Exists (GetCachePath (track_key.TrackId)));
+        }
+        
+        // Find already existing artwork
+        public static string FindArtworkPath (ArtworkKey key)
+        {
+            string path;
+            foreach (string art_id in key.ArtworkIds) {
+                path = GetLocalPath (key, art_id);
+                if (File.Exists (path)) {
+                    return path;
+                }
+            }
+            
+            foreach (string art_id in key.ArtworkIds) {
+                path = GetCachePath (art_id);
+                if (File.Exists (path)) {
+                    return path;
+                }
+            }
+            
+            return GetCachePath (key.AlbumId);
+        }
+        
+        // Where artwork for a track should be saved to
+        public static string ArtworkSavePath (ArtworkKey key)
+        {
+            return GetCachePath (key.AlbumId);
+        }
+        
+        public static string TrackArtworkSavePath (ArtworkKey key)
+        {
+            TrackArtworkKey track_key = key as TrackArtworkKey;
+            if (track_key == null) {
                 return null;
             }
-            
-            int lp_index = part.LastIndexOf ('(');
-            if (lp_index > 0) {
-                part = part.Substring (0, lp_index);
-            }
-            
-            // TODO compile regex
-            return Regex.Replace (part, @"[^A-Za-z0-9]*", "").ToLower ();
+            return GetCachePath (track_key.TrackId);
         }
         
-        private static string root_path = Path.Combine (XdgBaseDirectorySpec.GetUserDirectory (
-            "XDG_CACHE_HOME", ".cache"),  "album-art");
-            
-        static CoverArtSpec () {
+        static CoverArtSpec ()
+        {
             Hyena.Log.DebugFormat ("Album artwork path set to {0}", root_path);
         }
-            
+        
         public static string RootPath {
             get { return root_path; }
         }

=== added file 'src/Core/Banshee.Core/Banshee.Base/Tests/ArtworkKeyTests.cs'
--- src/Core/Banshee.Core/Banshee.Base/Tests/ArtworkKeyTests.cs	1970-01-01 00:00:00 +0000
+++ src/Core/Banshee.Core/Banshee.Base/Tests/ArtworkKeyTests.cs	2009-02-28 22:38:33 +0000
@@ -0,0 +1,107 @@
+//
+// ArtworkKeyTests.cs
+//
+// Author:
+//   John Millikin <jmillikin@gmail.com>
+//
+// 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.
+//
+
+#if ENABLE_TESTS
+
+using System;
+using NUnit.Framework;
+
+using Banshee.Base;
+
+namespace Banshee.Base.Tests
+{
+    [TestFixture]
+    public class ArtworkKeyTests
+    {
+        private void AssertHasIds (ArtworkKey key, params string[] expected)
+        {
+            Assert.AreEqual (expected, key.ArtworkIds);
+        }
+        
+        [Test]
+        public void TestForTrackWithAlbumArtist ()
+        {
+            TrackArtworkKey key = TrackArtworkKey.ForTrack ("/path/file", "Album Artist", "Track Artist", "Album Title", "Track Title");
+            Assert.AreEqual ("album-78ca1bb5751ca06cc9cab08ab0a4de56-9817cc2d313323022f81be184f8e0852", key.AlbumId);
+            Assert.AreEqual ("track-78ca1bb5751ca06cc9cab08ab0a4de56-9817cc2d313323022f81be184f8e0852-674781ef6674d2b4a0df2bf2e117a1a9", key.TrackId);
+            Assert.AreEqual ("/path", key.LocalDirectory);
+            AssertHasIds (key,
+                          "track-78ca1bb5751ca06cc9cab08ab0a4de56-9817cc2d313323022f81be184f8e0852-674781ef6674d2b4a0df2bf2e117a1a9",
+                          "album-78ca1bb5751ca06cc9cab08ab0a4de56-9817cc2d313323022f81be184f8e0852");
+        }
+        
+        [Test]
+        public void TestForTrackWithTrackArtist ()
+        {
+            TrackArtworkKey key = TrackArtworkKey.ForTrack ("/path/file", null, "Track Artist", "Album Title", "Track Title");
+            Assert.AreEqual ("album-68384b7274bd39238d58d7d49a3a5a8f-9817cc2d313323022f81be184f8e0852", key.AlbumId);
+            Assert.AreEqual ("track-68384b7274bd39238d58d7d49a3a5a8f-9817cc2d313323022f81be184f8e0852-674781ef6674d2b4a0df2bf2e117a1a9", key.TrackId);
+            Assert.AreEqual ("/path", key.LocalDirectory);
+            AssertHasIds (key,
+                          "track-68384b7274bd39238d58d7d49a3a5a8f-9817cc2d313323022f81be184f8e0852-674781ef6674d2b4a0df2bf2e117a1a9",
+                          "album-68384b7274bd39238d58d7d49a3a5a8f-9817cc2d313323022f81be184f8e0852");
+        }
+        
+        [Test]
+        public void TestForTrackWithoutPath ()
+        {
+            ArtworkKey key1 = TrackArtworkKey.ForTrack (null, null, null, null, null);
+            ArtworkKey key2 = TrackArtworkKey.ForTrack ("", null, null, null, null);
+            
+            Assert.AreEqual (key1.LocalDirectory, null);
+            Assert.AreEqual (key2.LocalDirectory, null);
+        }
+        
+        [Test]
+        public void TestForAlbum ()
+        {
+            ArtworkKey key = ArtworkKey.ForAlbum ("Artist", "Title");
+            Assert.AreEqual (key.AlbumId, "album-0441f9e2d94c39a70e21b83829259aa4-d5d3db1765287eef77d7927cc956f50a");
+            Assert.AreEqual (key.LocalDirectory, null);
+            AssertHasIds (key, "album-0441f9e2d94c39a70e21b83829259aa4-d5d3db1765287eef77d7927cc956f50a");
+        }
+        
+        [Test]
+        public void TestForRadio ()
+        {
+            ArtworkKey key = ArtworkKey.ForRadio ("Title");
+            Assert.AreEqual (key.AlbumId, "radio-d5d3db1765287eef77d7927cc956f50a");
+            Assert.AreEqual (key.LocalDirectory, null);
+            AssertHasIds (key, "radio-d5d3db1765287eef77d7927cc956f50a");
+        }
+        
+        [Test]
+        public void TestForPodcast ()
+        {
+            ArtworkKey key = ArtworkKey.ForPodcast ("Title");
+            Assert.AreEqual (key.AlbumId, "podcast-d5d3db1765287eef77d7927cc956f50a");
+            Assert.AreEqual (key.LocalDirectory, null);
+            AssertHasIds (key, "podcast-d5d3db1765287eef77d7927cc956f50a");
+        }
+    }
+}
+
+#endif

=== added file 'src/Core/Banshee.Core/Banshee.Base/Tests/CoverArtSpecTests.cs'
--- src/Core/Banshee.Core/Banshee.Base/Tests/CoverArtSpecTests.cs	1970-01-01 00:00:00 +0000
+++ src/Core/Banshee.Core/Banshee.Base/Tests/CoverArtSpecTests.cs	2009-02-28 01:46:44 +0000
@@ -0,0 +1,138 @@
+//
+// CoverArtSpecTests.cs
+//
+// Author:
+//   John Millikin <jmillikin@gmail.com>
+//
+// 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.
+//
+
+#if ENABLE_TESTS
+
+using System;
+using NUnit.Framework;
+
+using Banshee.Base;
+
+namespace Banshee.Base.Tests
+{
+    [TestFixture]
+    public class EscapePartTests
+    {
+        private void AssertEscaped (string original, string expected)
+        {
+            Assert.AreEqual (expected, CoverArtSpec.EscapePart (original));
+        }
+        
+        [Test]
+        public void TestEmpty ()
+        {
+            AssertEscaped (null, "");
+            AssertEscaped ("", "");
+        }
+        
+        [Test]
+        public void TestLowercased ()
+        {
+            AssertEscaped ("A", "a");
+        }
+        
+        [Test]
+        public void TestNormalized ()
+        {
+            AssertEscaped ("\u00e9", "e\u0301");
+        }
+        
+        [Test]
+        public void TestCollapseSpaces ()
+        {
+            AssertEscaped (" ", " ");
+            AssertEscaped ("  ", " ");
+            AssertEscaped ("  a  ", " a ");
+            AssertEscaped (" \t ", " ");
+        }
+        
+        [Test]
+        public void TestUnwanted ()
+        {
+            AssertEscaped ("!", "");
+            AssertEscaped ("@", "");
+            AssertEscaped ("#", "");
+            AssertEscaped ("$", "");
+            AssertEscaped ("^", "");
+            AssertEscaped ("&", "");
+            AssertEscaped ("*", "");
+            AssertEscaped ("_", "");
+            AssertEscaped ("+", "");
+            AssertEscaped ("=", "");
+            AssertEscaped ("|", "");
+            AssertEscaped ("\\", "");
+            AssertEscaped ("/", "");
+            AssertEscaped ("?", "");
+            AssertEscaped ("~", "");
+            AssertEscaped ("`", "");
+            AssertEscaped ("'", "");
+            AssertEscaped ("\"", "");
+        }
+        
+        [Test]
+        public void TestStripNotes ()
+        {
+            AssertEscaped ("a(b)c", "ac");
+            AssertEscaped ("a{b}c", "ac");
+            AssertEscaped ("a[b]c", "ac");
+            AssertEscaped ("a<b>c", "ac");
+            
+            AssertEscaped ("a(b)c(d)e", "ace");
+        }
+    }
+    
+    [TestFixture]
+    public class DigestPartTests
+    {
+        private void AssertDigested (string original, string expected)
+        {
+            Assert.AreEqual (expected, CoverArtSpec.DigestPart (original));
+        }
+        
+        [Test]
+        public void TestEmpty ()
+        {
+            AssertDigested (null, "7215ee9c7d9dc229d2921a40e899ec5f");
+            AssertDigested ("", "7215ee9c7d9dc229d2921a40e899ec5f");
+            AssertDigested (" ", "7215ee9c7d9dc229d2921a40e899ec5f");
+        }
+        
+        [Test]
+        public void TestUnicode ()
+        {
+            AssertDigested ("\u00e9", "5526861fbb1e71a1bda6ac364310a807");
+            AssertDigested ("e\u0301", "5526861fbb1e71a1bda6ac364310a807");
+        }
+        
+        [Test]
+        public void TestEscaped ()
+        {
+            AssertDigested ("(a)", "7215ee9c7d9dc229d2921a40e899ec5f");
+        }
+    }
+}
+
+#endif

=== modified file 'src/Core/Banshee.Core/Banshee.Collection/AlbumInfo.cs'
--- src/Core/Banshee.Core/Banshee.Collection/AlbumInfo.cs	2009-02-26 20:04:55 +0000
+++ src/Core/Banshee.Core/Banshee.Collection/AlbumInfo.cs	2009-02-28 20:10:25 +0000
@@ -40,7 +40,7 @@
         private string artist_name;
         private string artist_name_sort;
         private bool is_compilation;
-        private string artwork_id;
+        private ArtworkKey artwork_key;
         private DateTime release_date = DateTime.MinValue;
         private string musicbrainz_id;
         
@@ -88,13 +88,13 @@
             set { release_date = value; }
         }
         
-        public virtual string ArtworkId {
-            get { 
-                if (artwork_id == null) {
-                    artwork_id = CoverArtSpec.CreateArtistAlbumId (ArtistName, Title);
+        public virtual ArtworkKey ArtworkKey {
+            get {
+                if (artwork_key == null) {
+                    artwork_key = ArtworkKey.ForAlbum (ArtistName, Title);
                 }
                 
-                return artwork_id;
+                return artwork_key;
             }
         }
     }

=== modified file 'src/Core/Banshee.Core/Banshee.Collection/IBasicTrackInfo.cs'
--- src/Core/Banshee.Core/Banshee.Collection/IBasicTrackInfo.cs	2008-07-22 13:37:08 +0000
+++ src/Core/Banshee.Core/Banshee.Collection/IBasicTrackInfo.cs	2009-02-28 21:10:20 +0000
@@ -34,7 +34,7 @@
         string AlbumArtist { get; }
         string AlbumTitle { get; }
         string TrackTitle { get; }
-        string ArtworkId { get; }
+        Banshee.Base.ArtworkKey ArtworkKey { get; }
         Banshee.Base.SafeUri Uri { get; }
         TrackMediaAttributes MediaAttributes { get; }
     }

=== modified file 'src/Core/Banshee.Core/Banshee.Collection/TrackInfo.cs'
--- src/Core/Banshee.Core/Banshee.Collection/TrackInfo.cs	2009-02-26 20:04:55 +0000
+++ src/Core/Banshee.Core/Banshee.Collection/TrackInfo.cs	2009-02-28 21:15:32 +0000
@@ -59,7 +59,9 @@
 
         public delegate void PlaybackFinishedHandler (TrackInfo track, double percentComplete);
         public static event PlaybackFinishedHandler PlaybackFinished;
-            
+        
+        private ArtworkKey artwork_key;
+        
         private SafeUri uri;
         private SafeUri more_info_uri;
         private string mimetype;
@@ -152,7 +154,8 @@
                 return false;
             }
             
-            return ArtworkId == track.ArtworkId;
+            return (track.AlbumArtist == AlbumArtist &&
+                    track.AlbumTitle == AlbumTitle);
         }
 
         public virtual void Save ()
@@ -327,11 +330,25 @@
             } 
         }     
         
-        [Exportable (ExportName = "artwork-id")]
-        public virtual string ArtworkId { 
-            get { return CoverArtSpec.CreateArtistAlbumId (AlbumArtist, AlbumTitle); }
+        public virtual ArtworkKey ArtworkKey {
+            get {
+                if (artwork_key == null) {
+                    if (HasAttribute (TrackMediaAttributes.Podcast)) {
+                        artwork_key = ArtworkKey.ForPodcast (AlbumTitle);
+                    } else {
+                        artwork_key = TrackArtworkKey.ForTrack (
+                            Uri.AbsolutePath,
+                            AlbumArtist,
+                            ArtistName,
+                            AlbumTitle,
+                            TrackTitle);
+                    }
+                }
+                
+                return artwork_key;
+            }
         }
-
+        
         [Exportable]
         public virtual string Genre {
             get { return genre; }

=== modified file 'src/Core/Banshee.Core/Banshee.Streaming/CommonTags.cs'
--- src/Core/Banshee.Core/Banshee.Streaming/CommonTags.cs	2009-02-11 01:24:04 +0000
+++ src/Core/Banshee.Core/Banshee.Streaming/CommonTags.cs	2009-02-28 20:16:24 +0000
@@ -74,7 +74,6 @@
         public const string AlbumGain         = "replaygain-album-gain";
         public const string AlbumPeak         = "replaygain-album-peak";
         public const string StreamType        = "stream-type";
-        public const string AlbumCoverId      = "album-cover-id"; 
         public const string MoreInfoUri       = "more-info-uri";
         public const string MusicBrainzTrackId = "musicbrainz-trackid";
         public const string MusicBrainzArtistId = "musicbrainz-artistid";

=== modified file 'src/Core/Banshee.Core/Banshee.Streaming/StreamTagger.cs'
--- src/Core/Banshee.Core/Banshee.Streaming/StreamTagger.cs	2009-02-11 01:24:04 +0000
+++ src/Core/Banshee.Core/Banshee.Streaming/StreamTagger.cs	2009-02-28 20:16:18 +0000
@@ -310,15 +310,6 @@
                             track.MediaAttributes |= TrackMediaAttributes.VideoStream;
                         }
                         break;
-                    /*case CommonTags.AlbumCoverId:
-                        foreach(string ext in TrackInfo.CoverExtensions) {
-                            string path = Paths.GetCoverArtPath((string) tag.Value, "." + ext);
-                            if(System.IO.File.Exists(path)) {
-                                track.CoverArtFileName = path;
-                                break;
-                            }
-                        }
-                        break;*/
                 }
             } catch {
             }

=== modified file 'src/Core/Banshee.Core/Makefile.am'
--- src/Core/Banshee.Core/Makefile.am	2008-10-21 20:03:00 +0000
+++ src/Core/Banshee.Core/Makefile.am	2009-02-28 22:23:46 +0000
@@ -3,6 +3,7 @@
 LINK = $(REF_BANSHEE_CORE)
 SOURCES =  \
 	Banshee.Base/ApplicationContext.cs \
+	Banshee.Base/ArtworkKey.cs \
 	Banshee.Base/CoverArtSpec.cs \
 	Banshee.Base/FileNamePattern.cs \
 	Banshee.Base/Localization.cs \
@@ -12,6 +13,8 @@
 	Banshee.Base/ProductInformation.cs \
 	Banshee.Base/Resource.cs \
 	Banshee.Base/SafeUri.cs \
+	Banshee.Base/Tests/ArtworkKeyTests.cs \
+	Banshee.Base/Tests/CoverArtSpecTests.cs \
 	Banshee.Base/Tests/FileNamePatternTests.cs \
 	Banshee.Base/Tests/TaglibReadWriteTests.cs \
 	Banshee.Base/UriList.cs \
@@ -74,4 +77,4 @@
 include $(top_srcdir)/build/build.mk
 
 EXTRA_DIST += Banshee.Core.dll.config
-module_SCRIPTS += Banshee.Core.dll.config
\ No newline at end of file
+module_SCRIPTS += Banshee.Core.dll.config

=== modified file 'src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackInfo.cs'
--- src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackInfo.cs	2009-02-26 20:04:55 +0000
+++ src/Core/Banshee.Services/Banshee.Collection.Database/DatabaseTrackInfo.cs	2009-02-28 06:37:43 +0000
@@ -107,12 +107,12 @@
             return TrackEqual (this, db_track);
         }
 
-        public override string ArtworkId {
+        public override ArtworkKey ArtworkKey {
             get {
-                if (PrimarySource != null && PrimarySource.TrackArtworkIdHandler != null) {
-                    return PrimarySource.TrackArtworkIdHandler (this);
+                if (PrimarySource != null && PrimarySource.TrackArtworkKeyHandler != null) {
+                    return PrimarySource.TrackArtworkKeyHandler (this);
                 }
-                return base.ArtworkId;
+                return base.ArtworkKey;
             }
         }
 

=== modified file 'src/Core/Banshee.Services/Banshee.Metadata.Embedded/EmbeddedQueryJob.cs'
--- src/Core/Banshee.Services/Banshee.Metadata.Embedded/EmbeddedQueryJob.cs	2008-10-03 19:34:53 +0000
+++ src/Core/Banshee.Services/Banshee.Metadata.Embedded/EmbeddedQueryJob.cs	2009-02-28 23:59:32 +0000
@@ -29,6 +29,7 @@
 using System;
 using System.IO;
 using System.Collections.Generic;
+using System.Security.Cryptography;
 
 using TagLib;
 
@@ -42,6 +43,7 @@
     public class EmbeddedQueryJob : MetadataServiceJob
     {
         private TrackInfo track;
+        private static HashAlgorithm artwork_digest = (HashAlgorithm) MD5.Create ();
         
         public EmbeddedQueryJob(IBasicTrackInfo track)
         {
@@ -51,7 +53,7 @@
         
         public override void Run()
         {
-            if(track == null || CoverArtSpec.CoverExists (track.ArtworkId)) {
+            if(track == null || CoverArtSpec.TrackArtworkExists (track.ArtworkKey)) {
                 return;
             }
           
@@ -63,26 +65,15 @@
             if (track.Uri == null || !track.Uri.IsFile || !Banshee.IO.File.Exists (track.Uri))
                 return;
 
-            string artist_album_id = track.ArtworkId;
-
-            if(artist_album_id == null) {
-                return;
-            }
-            
             IPicture [] pictures = GetEmbeddedPictures(track.Uri);
                     
             if(pictures != null && pictures.Length > 0) {
                 int preferred_index = GetPictureIndexToUse(pictures);
                 IPicture picture = pictures[preferred_index];
-                string path = CoverArtSpec.GetPath(artist_album_id);
                 
-                if(SavePicture(picture, path)) {    
-                    StreamTag tag = new StreamTag();
-                    tag.Name = CommonTags.AlbumCoverId;
-                    tag.Value = artist_album_id;   
-                    
-                    AddTag(tag);
-                } 
+                if (!GotDuplicateCover (picture)) {
+                    SavePicture (picture, GetSavePath ());
+                }
             }
         }
 
@@ -118,13 +109,38 @@
             }
 
             // Set the extension as .cover unless it's already a JPG
-            string extension = "cover";
-            if (picture.MimeType != null && (picture.MimeType.Contains ("jpeg") || picture.MimeType.Contains ("jpg"))) {
-                extension = "jpg";
+            if (picture.MimeType == null ||
+                !(picture.MimeType.Contains ("jpeg") ||
+                  picture.MimeType.Contains ("jpg"))) {
+                image_path = Path.ChangeExtension (image_path, "cover");
             }
-
-            SaveAtomically (Path.ChangeExtension (image_path, extension), new MemoryStream (picture.Data.Data));
+            
+            SaveAtomically (image_path, new MemoryStream (picture.Data.Data));
             return true;
         }
+        
+        protected string GetSavePath ()
+        {
+            ArtworkKey art_key = track.ArtworkKey;
+            if (CoverArtSpec.ArtworkExists (art_key)) {
+                return CoverArtSpec.TrackArtworkSavePath (art_key);
+            }
+            return CoverArtSpec.ArtworkSavePath (art_key);
+        }
+        
+        protected bool GotDuplicateCover (IPicture picture)
+        {
+            if (!CoverArtSpec.ArtworkExists (Track.ArtworkKey)) {
+                return false;
+            }
+            
+            string album_path = CoverArtSpec.FindArtworkPath (Track.ArtworkKey);
+            Stream album_stream = new FileStream (album_path, FileMode.Open, FileAccess.Read);
+            
+            byte[] album_digest = artwork_digest.ComputeHash (album_stream);
+            byte[] new_cover_digest = artwork_digest.ComputeHash (picture.Data.Data);
+            
+            return Hyena.CryptoUtil.ByteArrayEqual (album_digest, new_cover_digest);
+        }
     }
 }

=== modified file 'src/Core/Banshee.Services/Banshee.Metadata.FileSystem/FileSystemQueryJob.cs'
--- src/Core/Banshee.Services/Banshee.Metadata.FileSystem/FileSystemQueryJob.cs	2008-10-01 17:43:58 +0000
+++ src/Core/Banshee.Services/Banshee.Metadata.FileSystem/FileSystemQueryJob.cs	2009-02-28 20:16:30 +0000
@@ -50,7 +50,7 @@
         
         public override void Run ()
         {
-            if (Track == null || CoverArtSpec.CoverExists (Track.ArtworkId)) {
+            if (Track == null || CoverArtSpec.ArtworkExists (Track.ArtworkKey)) {
                 return;
             }
           
@@ -62,8 +62,7 @@
         
         protected void Fetch ()
         {
-            if (Track.Uri == null || !Track.Uri.IsFile ||
-                    Track.ArtworkId == null || !Banshee.IO.File.Exists (Track.Uri)) {
+            if (Track.Uri == null || !Track.Uri.IsFile || !Banshee.IO.File.Exists (Track.Uri)) {
                 return;
             }
             
@@ -107,21 +106,16 @@
             
             if (best_file != null) {
                 try {
-                    string extension = "cover";
-                    if (best_file.EndsWith ("jpg", true, System.Globalization.CultureInfo.InvariantCulture) ||
-                        best_file.EndsWith ("jpeg", true, System.Globalization.CultureInfo.InvariantCulture)) {
-                        extension = "jpg";
+                    string save_path = CoverArtSpec.ArtworkSavePath (Track.ArtworkKey);
+                    
+                    if (!(best_file.EndsWith ("jpg", true, System.Globalization.CultureInfo.InvariantCulture) ||
+                          best_file.EndsWith ("jpeg", true, System.Globalization.CultureInfo.InvariantCulture))) {
+                        save_path = Path.ChangeExtension (save_path, "cover");
                     }
 
                     // Copy the file to the cover art directory
-                    SaveAtomically (Path.ChangeExtension (CoverArtSpec.GetPath (Track.ArtworkId), extension), Banshee.IO.File.OpenRead (new SafeUri (best_file)));
+                    SaveAtomically (save_path, Banshee.IO.File.OpenRead (new SafeUri (best_file)));
 
-                    // Send the new StreamTag
-                    StreamTag tag = new StreamTag ();
-                    tag.Name = CommonTags.AlbumCoverId;
-                    tag.Value = Track.ArtworkId;
-                    AddTag (tag);
-                    
                     Log.Debug ("Got cover art from track's folder", best_file);
                 } catch (Exception e) {
                     Log.Exception (e);

=== modified file 'src/Core/Banshee.Services/Banshee.Metadata.LastFM/LastFMQueryJob.cs'
--- src/Core/Banshee.Services/Banshee.Metadata.LastFM/LastFMQueryJob.cs	2008-08-21 19:52:38 +0000
+++ src/Core/Banshee.Services/Banshee.Metadata.LastFM/LastFMQueryJob.cs	2009-02-28 20:17:33 +0000
@@ -62,10 +62,9 @@
             if (Track == null || (Track.MediaAttributes & TrackMediaAttributes.Podcast) != 0) {
                 return;
             }
-        
-            string artwork_id = Track.ArtworkId;
-
-            if (artwork_id == null || CoverArtSpec.CoverExists (artwork_id) || !ServiceManager.Get<Network> ().Connected) {
+  
+            ArtworkKey artwork_key = Track.ArtworkKey;
+            if (CoverArtSpec.ArtworkExists (artwork_key) || !ServiceManager.Get<Network> ().Connected) {
                 return;
             }
             
@@ -112,18 +111,13 @@
                 
             // Download the cover
             try {
-                if ((high_res_url != null && SaveHttpStreamCover (new Uri (high_res_url), artwork_id, null)) ||
-                   SaveHttpStreamCover (new Uri (best_url), artwork_id, null)) {
+                if ((high_res_url != null && SaveHttpStreamCover (new Uri (high_res_url), artwork_key, null)) ||
+                   SaveHttpStreamCover (new Uri (best_url), artwork_key, null)) {
                     if (best_url.Contains ("amazon")) {
-                        Log.Debug ("Downloaded cover art from Amazon", artwork_id);
+                        Log.Debug ("Downloaded cover art from Amazon", artwork_key.ToString ());
                     } else {
-                        Log.Debug ("Downloaded cover art from Lastfm", artwork_id);
+                        Log.Debug ("Downloaded cover art from Lastfm", artwork_key.ToString ());
                     }
-
-                    StreamTag tag = new StreamTag ();
-                    tag.Name = CommonTags.AlbumCoverId;
-                    tag.Value = artwork_id;
-                    AddTag (tag);
                 }
             } catch (Exception e) {
                 Log.Exception ("Cover art found on Lastfm, but downloading it failed. Probably server under high load or dead link", e);

=== modified file 'src/Core/Banshee.Services/Banshee.Metadata.MusicBrainz/MusicBrainzQueryJob.cs'
--- src/Core/Banshee.Services/Banshee.Metadata.MusicBrainz/MusicBrainzQueryJob.cs	2008-08-21 19:52:38 +0000
+++ src/Core/Banshee.Services/Banshee.Metadata.MusicBrainz/MusicBrainzQueryJob.cs	2009-02-28 20:17:47 +0000
@@ -70,11 +70,8 @@
                 return false;
             }
             
-            string artwork_id = Track.ArtworkId;
-            
-            if (artwork_id == null) {
-                return false;
-            } else if (CoverArtSpec.CoverExists (artwork_id)) {
+            ArtworkKey artwork_key = Track.ArtworkKey;
+            if (CoverArtSpec.ArtworkExists (artwork_key)) {
                 return false;
             } else if (!InternetConnected) {
                 return false;
@@ -87,15 +84,9 @@
                 }
             }
             
-            if (SaveHttpStreamCover (new Uri (String.Format (AmazonUriFormat, asin)), artwork_id, 
+            if (SaveHttpStreamCover (new Uri (String.Format (AmazonUriFormat, asin)), artwork_key, 
                 new string [] { "image/gif" })) {
-                Log.Debug ("Downloaded cover art from Amazon", artwork_id);
-                StreamTag tag = new StreamTag ();
-                tag.Name = CommonTags.AlbumCoverId;
-                tag.Value = artwork_id;
-
-                AddTag (tag);
-                
+                Log.Debug ("Downloaded cover art from Amazon", artwork_key.ToString ());
                 return true;
             }
             

=== modified file 'src/Core/Banshee.Services/Banshee.Metadata.Rhapsody/RhapsodyQueryJob.cs'
--- src/Core/Banshee.Services/Banshee.Metadata.Rhapsody/RhapsodyQueryJob.cs	2008-08-21 19:52:38 +0000
+++ src/Core/Banshee.Services/Banshee.Metadata.Rhapsody/RhapsodyQueryJob.cs	2009-02-28 23:13:09 +0000
@@ -48,6 +48,18 @@
     {
         private static Uri base_uri = new Uri("http://www.rhapsody.com/");
         
+        private static string EscapeUrlPart (string part)
+        {
+            return Regex.Replace (part, @"[^A-Za-z0-9]*", "").Replace (" ", "-").ToLower ();
+        }
+        
+        private static string GetAlbumUrl (IBasicTrackInfo track)
+        {
+            string artist = EscapeUrlPart (track.AlbumArtist);
+            string album = EscapeUrlPart (track.AlbumTitle);
+            return String.Format ("{0}/{1}", artist, album);
+        }
+        
         public RhapsodyQueryJob(IBasicTrackInfo track)
         {
             Track = track;
@@ -59,13 +71,12 @@
                 return;
             }
         
-            string artwork_id = Track.ArtworkId;
-            
-            if(artwork_id == null || CoverArtSpec.CoverExists(artwork_id) || !InternetConnected) {
+            ArtworkKey artwork_key = Track.ArtworkKey;
+            if (CoverArtSpec.ArtworkExists (artwork_key) || !InternetConnected) {
                 return;
             }
             
-            Uri data_uri = new Uri(base_uri, String.Format("/{0}/data.xml", artwork_id.Replace('-', '/')));
+            Uri data_uri = new Uri(base_uri, String.Format("/{0}/data.xml", GetAlbumUrl (Track)));
         
             XmlDocument doc = new XmlDocument();
             HttpWebResponse response = GetHttpStream (data_uri);
@@ -89,14 +100,9 @@
                 string second_attempt = art_node.Attributes["src"].Value;
                 string first_attempt = second_attempt.Replace("170x170", "500x500");
 
-                if(SaveHttpStreamCover(new Uri(first_attempt), artwork_id, null) || 
-                    SaveHttpStreamCover(new Uri(second_attempt), artwork_id, null)) {
-                    Log.Debug ("Downloaded cover art from Rhapsody", artwork_id);
-                    StreamTag tag = new StreamTag();
-                    tag.Name = CommonTags.AlbumCoverId;
-                    tag.Value = artwork_id;
-                
-                    AddTag(tag);
+                if(SaveHttpStreamCover(new Uri(first_attempt), artwork_key, null) || 
+                    SaveHttpStreamCover(new Uri(second_attempt), artwork_key, null)) {
+                    Log.Debug ("Downloaded cover art from Rhapsody", artwork_key.ToString ());
                 }
             }
             

=== modified file 'src/Core/Banshee.Services/Banshee.Metadata/MetadataService.cs'
--- src/Core/Banshee.Services/Banshee.Metadata/MetadataService.cs	2008-10-01 18:18:47 +0000
+++ src/Core/Banshee.Services/Banshee.Metadata/MetadataService.cs	2009-02-28 07:01:46 +0000
@@ -78,7 +78,7 @@
         
         public void Lookup (IBasicTrackInfo track, JobPriority priority)
         {
-            if (track == null || queries == null || track.ArtworkId == null) {
+            if (track == null || queries == null) {
                 return;
             }
             

=== modified file 'src/Core/Banshee.Services/Banshee.Metadata/MetadataServiceJob.cs'
--- src/Core/Banshee.Services/Banshee.Metadata/MetadataServiceJob.cs	2009-01-13 22:11:19 +0000
+++ src/Core/Banshee.Services/Banshee.Metadata/MetadataServiceJob.cs	2009-02-28 06:50:26 +0000
@@ -148,9 +148,9 @@
             return true;
         }
         
-        protected bool SaveHttpStreamCover (Uri uri, string albumArtistId, string [] ignoreMimeTypes)
+        protected bool SaveHttpStreamCover (Uri uri, ArtworkKey artwork_key, string [] ignoreMimeTypes)
         {
-            return SaveHttpStream (uri, CoverArtSpec.GetPath (albumArtistId), ignoreMimeTypes);
+            return SaveHttpStream (uri, CoverArtSpec.ArtworkSavePath (artwork_key), ignoreMimeTypes);
         }
 
         protected void SaveAtomically (string path, Stream from_stream)

=== modified file 'src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs'
--- src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs	2009-02-27 20:27:23 +0000
+++ src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs	2009-02-28 06:38:20 +0000
@@ -73,7 +73,7 @@
 
     public delegate bool TrackEqualHandler (DatabaseTrackInfo a, TrackInfo b);
     public delegate object TrackExternalObjectHandler (DatabaseTrackInfo a);
-    public delegate string TrackArtworkIdHandler (DatabaseTrackInfo a);
+    public delegate ArtworkKey TrackArtworkKeyHandler (DatabaseTrackInfo a);
 
     public abstract class PrimarySource : DatabaseSource, IDisposable
     {
@@ -91,10 +91,10 @@
             protected set { track_external_object_handler = value; }
         }
 
-        private TrackArtworkIdHandler track_artwork_id_handler;
-        public TrackArtworkIdHandler TrackArtworkIdHandler {
-            get { return track_artwork_id_handler; }
-            protected set { track_artwork_id_handler = value; }
+        private TrackArtworkKeyHandler track_artwork_key_handler;
+        public TrackArtworkKeyHandler TrackArtworkKeyHandler {
+            get { return track_artwork_key_handler; }
+            protected set { track_artwork_key_handler = value; }
         }
 
         #endregion

=== modified file 'src/Core/Banshee.ThickClient/Banshee.Collection.Gui/ArtworkManager.cs'
--- src/Core/Banshee.ThickClient/Banshee.Collection.Gui/ArtworkManager.cs	2008-10-06 20:04:57 +0000
+++ src/Core/Banshee.ThickClient/Banshee.Collection.Gui/ArtworkManager.cs	2009-02-28 23:46:09 +0000
@@ -34,6 +34,7 @@
 using Mono.Unix;
 
 using Gdk;
+using Gnome;
 
 using Hyena;
 using Hyena.Gui;
@@ -47,8 +48,9 @@
     public class ArtworkManager : IService
     {
         private Dictionary<int, SurfaceCache> scale_caches  = new Dictionary<int, SurfaceCache> ();
+        private ThumbnailFactory thumbnail_factory = new ThumbnailFactory (ThumbnailSize.Normal);
             
-        private class SurfaceCache : LruCache<string, Cairo.ImageSurface>
+        private class SurfaceCache : LruCache<ArtworkKey, Cairo.ImageSurface>
         {
             public SurfaceCache (int max_items) : base (max_items)
             {
@@ -96,30 +98,30 @@
             Log.Debug (String.Format ("Migrated {0} album art images.", artwork_count));
         }
         
-        public Cairo.ImageSurface LookupSurface (string id)
-        {
-            return LookupScaleSurface (id, 0);
-        }
-        
-        public Cairo.ImageSurface LookupScaleSurface (string id, int size)
-        {
-            return LookupScaleSurface (id, size, false);
-        }
-        
-        public Cairo.ImageSurface LookupScaleSurface (string id, int size, bool useCache)
+        public Cairo.ImageSurface LookupSurface (ArtworkKey key)
+        {
+            return LookupScaleSurface (key, 0);
+        }
+        
+        public Cairo.ImageSurface LookupScaleSurface (ArtworkKey key, int size)
+        {
+            return LookupScaleSurface (key, size, false);
+        }
+        
+        public Cairo.ImageSurface LookupScaleSurface (ArtworkKey key, int size, bool useCache)
         {
             SurfaceCache cache = null;
             Cairo.ImageSurface surface = null;
             
-            if (id == null) {
+            if (key == null) {
                 return null;
             }
             
-            if (useCache && scale_caches.TryGetValue (size, out cache) && cache.TryGetValue (id, out surface)) {
+            if (useCache && scale_caches.TryGetValue (size, out cache) && cache.TryGetValue (key, out surface)) {
                 return surface;
             }
         
-            Pixbuf pixbuf = LookupScalePixbuf (id, size);
+            Pixbuf pixbuf = LookupScalePixbuf (key, size);
             if (pixbuf == null) {
                 return null;
             }
@@ -145,37 +147,27 @@
                     scale_caches.Add (size, cache);
                 }
                 
-                cache.Add (id, surface);
+                cache.Add (key, surface);
                 return surface;
             } finally {
                 DisposePixbuf (pixbuf);
             }
         }
         
-        public Pixbuf LookupPixbuf (string id)
+        public Pixbuf LookupPixbuf (ArtworkKey key)
         {
-            return LookupScalePixbuf (id, 0);
+            return LookupScalePixbuf (key, 0);
         }
         
-        public Pixbuf LookupScalePixbuf (string id, int size)
+        public Pixbuf LookupScalePixbuf (ArtworkKey key, int size)
         {
-            if (id == null || (size != 0 && size < 10)) {
+            if (key == null || (size != 0 && size < 10)) {
                 return null;
             }
-
-            // Find the scaled, cached file
-            string path = CoverArtSpec.GetPathForSize (id, size);
-            if (File.Exists (path)) {
-                try {
-                    return new Pixbuf (path);
-                } catch {
-                    return null;
-                }
-            }
-
-            string orig_path = CoverArtSpec.GetPathForSize (id, 0);
+            
+            string orig_path = CoverArtSpec.FindArtworkPath (key);
             bool orig_exists = File.Exists (orig_path);
-
+            
             if (!orig_exists) {
                 // It's possible there is an image with extension .cover that's waiting
                 // to be converted into a jpeg
@@ -197,15 +189,38 @@
                 }
             }
             
-            if (orig_exists && size >= 10) {
-                try {
-                    Pixbuf pixbuf = new Pixbuf (orig_path);
-                    Pixbuf scaled_pixbuf = pixbuf.ScaleSimple (size, size, Gdk.InterpType.Bilinear);
-                    Directory.CreateDirectory (Path.GetDirectoryName (path));
-                    scaled_pixbuf.Save (path, "jpeg");
-                    DisposePixbuf (pixbuf);
-                    return scaled_pixbuf;
-                } catch {}
+            if (orig_exists) {
+                Pixbuf pixbuf;
+                
+                if (size > 128) {
+                    // Don't bother with thumbnails, just load directly from the
+                    // original art.
+                    pixbuf = new Pixbuf (orig_path);
+                    
+                } else if (size >= 10) {
+                    string orig_uri = SafeUri.FilenameToUri (orig_path);
+                    DateTime orig_mtime = File.GetLastWriteTime (orig_path);
+                    
+                    // Check for a thumbnail
+                    string path = thumbnail_factory.Lookup (orig_uri, orig_mtime);
+                    
+                    try {
+                        if (File.Exists (path)) {
+                            pixbuf = new Pixbuf (path);
+                        } else {
+                            pixbuf = thumbnail_factory.GenerateThumbnail (orig_uri, "image/jpeg");
+                            thumbnail_factory.SaveThumbnail (pixbuf, orig_uri, orig_mtime);
+                        }
+                    } catch {
+                        return null;
+                    }
+                } else {
+                    return null;
+                }
+                
+                Pixbuf scaled_pixbuf = pixbuf.ScaleSimple (size, size, Gdk.InterpType.Bilinear);
+                DisposePixbuf (pixbuf);
+                return scaled_pixbuf;
             }
             
             return null;

=== modified file 'src/Core/Banshee.ThickClient/Banshee.Collection.Gui/ColumnCellAlbum.cs'
--- src/Core/Banshee.ThickClient/Banshee.Collection.Gui/ColumnCellAlbum.cs	2008-10-06 20:04:57 +0000
+++ src/Core/Banshee.ThickClient/Banshee.Collection.Gui/ColumnCellAlbum.cs	2009-02-28 20:09:16 +0000
@@ -68,7 +68,7 @@
             
             bool is_default = false;          
             ImageSurface image = artwork_manager == null ? null 
-                : artwork_manager.LookupScaleSurface (album.ArtworkId, image_size, true);
+                : artwork_manager.LookupScaleSurface (album.ArtworkKey, image_size, true);
             
             if (image == null) {
                 image = default_cover_image;
@@ -156,4 +156,4 @@
             return (height < image_size ? image_size : height) + 6;
         }
     }
-}
\ No newline at end of file
+}

=== modified file 'src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/TrackEditorDialog.cs'
--- src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/TrackEditorDialog.cs	2009-02-20 19:50:16 +0000
+++ src/Core/Banshee.ThickClient/Banshee.Gui.TrackEditor/TrackEditorDialog.cs	2009-02-28 07:07:41 +0000
@@ -368,7 +368,7 @@
             }
             
             ArtworkManager artwork = ServiceManager.Get<ArtworkManager> ();
-            Gdk.Pixbuf cover_art = artwork.LookupScalePixbuf (current_track.ArtworkId, 64);
+            Gdk.Pixbuf cover_art = artwork.LookupScalePixbuf (current_track.ArtworkKey, 64);
             header_image.Pixbuf = cover_art;
             if (cover_art == null) {
                 header_image.IconName = "media-optical";

=== modified file 'src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/ClassicTrackInfoDisplay.cs'
--- src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/ClassicTrackInfoDisplay.cs	2008-10-08 17:15:57 +0000
+++ src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/ClassicTrackInfoDisplay.cs	2009-02-28 07:08:21 +0000
@@ -289,7 +289,7 @@
                 return false;
             }
             
-            Gdk.Pixbuf pixbuf = ArtworkManager.LookupPixbuf (CurrentTrack.ArtworkId);
+            Gdk.Pixbuf pixbuf = ArtworkManager.LookupPixbuf (CurrentTrack.ArtworkKey);
          
             if (pixbuf == null) {
                 HidePopup ();

=== modified file 'src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/TrackInfoDisplay.cs'
--- src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/TrackInfoDisplay.cs	2008-11-09 23:25:10 +0000
+++ src/Core/Banshee.ThickClient/Banshee.Gui.Widgets/TrackInfoDisplay.cs	2009-02-28 07:07:58 +0000
@@ -364,7 +364,7 @@
         
         private void LoadImage (TrackInfo track)
         {
-            ImageSurface image = artwork_manager.LookupScaleSurface (track.ArtworkId, ArtworkSizeRequest);
+            ImageSurface image = artwork_manager.LookupScaleSurface (track.ArtworkKey, ArtworkSizeRequest);
 
             if (image == null) {
                 LoadMissingImage ((track.MediaAttributes & TrackMediaAttributes.VideoStream) != 0);

=== modified file 'src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/DatabaseRebuilder.cs'
--- src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/DatabaseRebuilder.cs	2008-05-30 20:43:50 +0000
+++ src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/DatabaseRebuilder.cs	2009-02-28 23:28:23 +0000
@@ -201,10 +201,8 @@
         
         private void ResolveCoverArt (Track track)
         {
-            string aaid = CoverArtSpec.CreateArtistAlbumId (track.Artist, track.Album);
-            string path = CoverArtSpec.GetPath (aaid);
-            
-            if (File.Exists (path)) {
+            if (CoverArtSpec.ArtworkExists (track.ArtworkKey)) {
+                string path = CoverArtSpec.FindArtworkPath (track.ArtworkKey);
                 IpodTrackInfo.SetIpodCoverArt (source.IpodDevice, track, path);
             }
         }

=== modified file 'src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/IpodTrackInfo.cs'
--- src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/IpodTrackInfo.cs	2008-10-03 15:37:00 +0000
+++ src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/IpodTrackInfo.cs	2009-02-28 23:27:54 +0000
@@ -250,8 +250,9 @@
                 }
             }
             
-            if (CoverArtSpec.CoverExists (ArtworkId)) {
-                SetIpodCoverArt (device, track, CoverArtSpec.GetPath (ArtworkId));
+            if (CoverArtSpec.ArtworkExists (ArtworkKey)) {
+                string artwork_path = CoverArtSpec.FindArtworkPath (ArtworkKey);
+                SetIpodCoverArt (device, track, artwork_path);
             }
         }
         

=== modified file 'src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage/MassStorageSource.cs'
--- src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage/MassStorageSource.cs	2009-01-21 21:10:50 +0000
+++ src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage/MassStorageSource.cs	2009-02-28 19:52:13 +0000
@@ -455,25 +455,20 @@
                     !String.IsNullOrEmpty (CoverArtFileName) && (FolderDepth == -1 || FolderDepth > 0)) {
                 SafeUri cover_uri = new SafeUri (System.IO.Path.Combine (System.IO.Path.GetDirectoryName (new_uri.LocalPath),
                                                                          CoverArtFileName));
-                string coverart_id;
-                if (track.HasAttribute (TrackMediaAttributes.Podcast)) {
-                    coverart_id = String.Format ("podcast-{0}", Banshee.Base.CoverArtSpec.EscapePart (track.AlbumTitle));
-                } else {
-                    coverart_id = track.ArtworkId;
-                }                
                 
-                if (!File.Exists (cover_uri) && CoverArtSpec.CoverExists (coverart_id)) {       
+                ArtworkKey coverart_key = track.ArtworkKey;
+                if (!File.Exists (cover_uri) && CoverArtSpec.ArtworkExists (coverart_key)) {
                     Gdk.Pixbuf pic = null;
                     
                     if (CoverArtSize == 0) {
                         if (CoverArtFileType == "jpg" || CoverArtFileType == "jpeg") {                        
-                            SafeUri local_cover_uri = new SafeUri (Banshee.Base.CoverArtSpec.GetPath (coverart_id));
+                            SafeUri local_cover_uri = new SafeUri (CoverArtSpec.FindArtworkPath (coverart_key));
                             Banshee.IO.File.Copy (local_cover_uri, cover_uri, false);
                         } else {
-                            pic = artwork_manager.LookupPixbuf (coverart_id);
+                            pic = artwork_manager.LookupPixbuf (coverart_key);
                         }
                     } else {
-                        pic = artwork_manager.LookupScalePixbuf (coverart_id, CoverArtSize);
+                        pic = artwork_manager.LookupScalePixbuf (coverart_key, CoverArtSize);
                     }
 
                     if (pic != null) {

=== modified file 'src/Dap/Banshee.Dap.Mtp/Banshee.Dap.Mtp/MtpSource.cs'
--- src/Dap/Banshee.Dap.Mtp/Banshee.Dap.Mtp/MtpSource.cs	2009-02-10 21:37:41 +0000
+++ src/Dap/Banshee.Dap.Mtp/Banshee.Dap.Mtp/MtpSource.cs	2009-02-28 07:12:21 +0000
@@ -334,7 +334,7 @@
                         if (supports_jpegs && can_sync_albumart) {
                             try {
                                 Gdk.Pixbuf pic = ServiceManager.Get<Banshee.Collection.Gui.ArtworkManager> ().LookupScalePixbuf (
-                                    track.ArtworkId, thumb_width
+                                    track.ArtworkKey, thumb_width
                                 );
                                 if (pic != null) {
                                     byte [] bytes = pic.SaveToBuffer ("jpeg");

=== modified file 'src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtJob.cs'
--- src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtJob.cs	2009-01-26 04:46:25 +0000
+++ src/Extensions/Banshee.CoverArt/Banshee.CoverArt/CoverArtJob.cs	2009-02-28 19:51:51 +0000
@@ -171,7 +171,7 @@
             } catch (Exception e) {
                 Log.Exception (e);
             } finally {
-                bool have_cover_art = CoverArtSpec.CoverExists (track.ArtistName, track.AlbumTitle);
+                bool have_cover_art = CoverArtSpec.ArtworkExists (track.ArtworkKey);
                 ServiceManager.DbConnection.Execute (
                     "INSERT OR REPLACE INTO CoverArtDownloads (AlbumID, Downloaded, LastAttempt) VALUES (?, ?, ?)",
                     track.AlbumId, have_cover_art, DateTime.Now);

=== modified file 'src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/LastfmSourceContents.cs'
--- src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/LastfmSourceContents.cs	2008-10-06 20:04:57 +0000
+++ src/Extensions/Banshee.Lastfm/Banshee.Lastfm.Radio/LastfmSourceContents.cs	2009-02-28 07:13:12 +0000
@@ -226,7 +226,7 @@
                         AlbumInfo album = new AlbumInfo (track.Album);
                         album.ArtistName = track.Artist;
 
-                        Gdk.Pixbuf pb = artwork_manager == null ? null : artwork_manager.LookupScalePixbuf (album.ArtworkId, 40);
+                        Gdk.Pixbuf pb = artwork_manager == null ? null : artwork_manager.LookupScalePixbuf (album.ArtworkKey, 40);
                         if (pb != null) {
                             tile.Pixbuf = pb;
                         }

=== modified file 'src/Extensions/Banshee.NotificationArea/Banshee.NotificationArea/NotificationAreaService.cs'
--- src/Extensions/Banshee.NotificationArea/Banshee.NotificationArea/NotificationAreaService.cs	2009-02-27 20:51:30 +0000
+++ src/Extensions/Banshee.NotificationArea/Banshee.NotificationArea/NotificationAreaService.cs	2009-02-28 07:13:34 +0000
@@ -386,7 +386,7 @@
             Gdk.Pixbuf image = null;
             
             if (artwork_manager_service != null) {
-                image = artwork_manager_service.LookupScalePixbuf (current_track.ArtworkId, 42);
+                image = artwork_manager_service.LookupScalePixbuf (current_track.ArtworkKey, 42);
             }
             
             if (image == null) {

=== modified file 'src/Extensions/Banshee.Podcasting/Banshee.Podcasting.Data/PodcastSource.cs'
--- src/Extensions/Banshee.Podcasting/Banshee.Podcasting.Data/PodcastSource.cs	2008-10-09 04:11:12 +0000
+++ src/Extensions/Banshee.Podcasting/Banshee.Podcasting.Data/PodcastSource.cs	2009-02-28 06:37:07 +0000
@@ -94,7 +94,7 @@
         {
             this.baseDirectory = baseDirectory;
             TrackExternalObjectHandler = GetPodcastInfoObject;
-            TrackArtworkIdHandler = GetTrackArtworkId;
+            TrackArtworkKeyHandler = GetTrackArtworkKey;
             MediaTypes = TrackMediaAttributes.Podcast;
             NotMediaTypes = TrackMediaAttributes.AudioBook;
             SyncCondition = "(substr(CoreTracks.Uri, 0, 4) != 'http' AND CoreTracks.PlayCount = 0)";
@@ -166,9 +166,9 @@
             return new PodcastTrackInfo (track);
         }
 
-        private string GetTrackArtworkId (DatabaseTrackInfo track)
+        private ArtworkKey GetTrackArtworkKey (DatabaseTrackInfo track)
         {
-            return PodcastService.ArtworkIdFor (PodcastTrackInfo.From (track).Feed);
+            return PodcastService.ArtworkKeyFor (PodcastTrackInfo.From (track).Feed);
         }
         
         protected override bool HasArtistAlbum {

=== modified file 'src/Extensions/Banshee.Podcasting/Banshee.Podcasting.Gui/ColumnCellPodcast.cs'
--- src/Extensions/Banshee.Podcasting/Banshee.Podcasting.Gui/ColumnCellPodcast.cs	2008-10-09 04:11:12 +0000
+++ src/Extensions/Banshee.Podcasting/Banshee.Podcasting.Gui/ColumnCellPodcast.cs	2009-02-28 07:14:04 +0000
@@ -74,7 +74,7 @@
             
             bool is_default = false;
             ImageSurface image = artwork_manager == null ? null 
-                : artwork_manager.LookupScaleSurface (PodcastService.ArtworkIdFor (feed), image_size, true);
+                : artwork_manager.LookupScaleSurface (PodcastService.ArtworkKeyFor (feed), image_size, true);
             
             if (image == null) {
                 image = default_cover_image;

=== modified file 'src/Extensions/Banshee.Podcasting/Banshee.Podcasting/PodcastImageFetchJob.cs'
--- src/Extensions/Banshee.Podcasting/Banshee.Podcasting/PodcastImageFetchJob.cs	2008-10-09 04:11:12 +0000
+++ src/Extensions/Banshee.Podcasting/Banshee.Podcasting/PodcastImageFetchJob.cs	2009-02-28 07:14:43 +0000
@@ -69,17 +69,15 @@
                 return;
             }
             
-            string cover_art_id = PodcastService.ArtworkIdFor (feed);
+            ArtworkKey cover_art_key = PodcastService.ArtworkKeyFor (feed);
             
-            if (cover_art_id == null) {
-                return;
-            } else if (CoverArtSpec.CoverExists (cover_art_id)) {
+            if (CoverArtSpec.ArtworkExists (cover_art_key)) {
                 return;
             } else if (!InternetConnected) {
                 return;
             }
             
-            if (SaveHttpStreamCover (new Uri (feed.ImageUrl), cover_art_id, null)) {
+            if (SaveHttpStreamCover (new Uri (feed.ImageUrl), cover_art_key, null)) {
                 Banshee.Sources.Source src = ServiceManager.SourceManager.ActiveSource;
                 if (src != null && (src is PodcastSource || src.Parent is PodcastSource)) {
                     (src as Banshee.Sources.DatabaseSource).Reload ();
@@ -88,4 +86,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}

=== modified file 'src/Extensions/Banshee.Podcasting/Banshee.Podcasting/PodcastService.cs'
--- src/Extensions/Banshee.Podcasting/Banshee.Podcasting/PodcastService.cs	2009-02-20 22:14:08 +0000
+++ src/Extensions/Banshee.Podcasting/Banshee.Podcasting/PodcastService.cs	2009-02-28 07:15:01 +0000
@@ -355,7 +355,8 @@
         
         private void RefreshArtworkFor (Feed feed)
         {
-            if (feed.LastDownloadTime != DateTime.MinValue && !CoverArtSpec.CoverExists (PodcastService.ArtworkIdFor (feed))) {
+            
+            if (feed.LastDownloadTime != DateTime.MinValue && !CoverArtSpec.ArtworkExists (ArtworkKeyFor (feed))) {
                 Banshee.Kernel.Scheduler.Schedule (new PodcastImageFetchJob (feed), Banshee.Kernel.JobPriority.BelowNormal);
             }
         }
@@ -589,9 +590,9 @@
             }
         }          
 
-        public static string ArtworkIdFor (Feed feed)
+        public static ArtworkKey ArtworkKeyFor (Feed feed)
         {
-            return String.Format ("podcast-{0}", Banshee.Base.CoverArtSpec.EscapePart (feed.Title));
+            return ArtworkKey.ForPodcast (feed.Title);
         }
 
         // Via Monopod

=== modified file 'src/Libraries/Hyena/Hyena/CryptoUtil.cs'
--- src/Libraries/Hyena/Hyena/CryptoUtil.cs	2008-08-14 19:55:45 +0000
+++ src/Libraries/Hyena/Hyena/CryptoUtil.cs	2009-02-28 21:40:11 +0000
@@ -64,5 +64,20 @@
 
             return shash.ToString ();
         }
+        
+        public static bool ByteArrayEqual (byte[] a, byte[] b)
+        {
+            if (a.Length != b.Length) {
+                return false;
+            }
+            
+            for (int ii = 0; ii < a.Length; ii++) {
+                if (a[ii] != b[ii]) {
+                    return false;
+                }
+            }
+            
+            return true;
+        }
     }
 }

