5
// Aaron Bockover <abockover@novell.com>
7
// Copyright (C) 2007-2008 Novell, Inc.
9
// Permission is hereby granted, free of charge, to any person obtaining
10
// a copy of this software and associated documentation files (the
11
// "Software"), to deal in the Software without restriction, including
12
// without limitation the rights to use, copy, modify, merge, publish,
13
// distribute, sublicense, and/or sell copies of the Software, and to
14
// permit persons to whom the Software is furnished to do so, subject to
15
// the following conditions:
17
// The above copyright notice and this permission notice shall be
18
// included in all copies or substantial portions of the Software.
20
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30
using System.Collections.Generic;
31
using System.Text.RegularExpressions;
39
using Hyena.Collections;
40
using Hyena.Data.Sqlite;
44
using Banshee.Configuration;
45
using Banshee.ServiceStack;
47
namespace Banshee.Collection.Gui
49
public class ArtworkManager : IService
51
private Dictionary<int, SurfaceCache> scale_caches = new Dictionary<int, SurfaceCache> ();
52
private HashSet<int> cacheable_cover_sizes = new HashSet<int> ();
53
private HashSet<string> null_artwork_ids = new HashSet<string> ();
55
private class SurfaceCache : LruCache<string, Cairo.ImageSurface>
57
public SurfaceCache (int max_items) : base (max_items)
61
protected override void ExpireItem (Cairo.ImageSurface item)
64
((IDisposable)item).Dispose ();
69
public ArtworkManager ()
81
} catch (Exception e) {
82
Log.Exception ("Could not migrate album artwork cache directory", e);
85
if (ApplicationContext.CommandLine.Contains ("fetch-artwork")) {
86
ResetScanResultCache ();
89
Banshee.Metadata.MetadataService.Instance.ArtworkUpdated += OnArtworkUpdated;
92
public void Dispose ()
94
Banshee.Metadata.MetadataService.Instance.ArtworkUpdated -= OnArtworkUpdated;
97
private void OnArtworkUpdated (IBasicTrackInfo track)
99
ClearCacheFor (track.ArtworkId, true);
102
public Cairo.ImageSurface LookupSurface (string id)
104
return LookupScaleSurface (id, 0);
107
public Cairo.ImageSurface LookupScaleSurface (string id, int size)
109
return LookupScaleSurface (id, size, false);
112
public Cairo.ImageSurface LookupScaleSurface (string id, int size, bool useCache)
114
SurfaceCache cache = null;
115
Cairo.ImageSurface surface = null;
121
if (useCache && scale_caches.TryGetValue (size, out cache) && cache.TryGetValue (id, out surface)) {
125
if (null_artwork_ids.Contains (id)) {
129
Pixbuf pixbuf = LookupScalePixbuf (id, size);
130
if (pixbuf == null || pixbuf.Handle == IntPtr.Zero) {
131
null_artwork_ids.Add (id);
136
surface = PixbufImageSurface.Create (pixbuf);
137
if (surface == null) {
146
int bytes = 4 * size * size;
147
int max = (1 << 20) / bytes;
149
ChangeCacheSize (size, max);
150
cache = scale_caches[size];
153
cache.Add (id, surface);
156
DisposePixbuf (pixbuf);
160
public Pixbuf LookupPixbuf (string id)
162
return LookupScalePixbuf (id, 0);
165
public Pixbuf LookupScalePixbuf (string id, int size)
167
if (id == null || (size != 0 && size < 10)) {
171
if (null_artwork_ids.Contains (id)) {
175
// Find the scaled, cached file
176
string path = CoverArtSpec.GetPathForSize (id, size);
177
if (File.Exists (new SafeUri (path))) {
179
return new Pixbuf (path);
181
null_artwork_ids.Add (id);
186
string orig_path = CoverArtSpec.GetPathForSize (id, 0);
187
bool orig_exists = File.Exists (new SafeUri (orig_path));
190
// It's possible there is an image with extension .cover that's waiting
191
// to be converted into a jpeg
192
string unconverted_path = System.IO.Path.ChangeExtension (orig_path, "cover");
193
if (File.Exists (new SafeUri (unconverted_path))) {
195
Pixbuf pixbuf = new Pixbuf (unconverted_path);
196
if (pixbuf.Width < 50 || pixbuf.Height < 50) {
197
Hyena.Log.DebugFormat ("Ignoring cover art {0} because less than 50x50", unconverted_path);
198
null_artwork_ids.Add (id);
202
pixbuf.Save (orig_path, "jpeg");
206
File.Delete (new SafeUri (unconverted_path));
211
if (orig_exists && size >= 10) {
213
Pixbuf pixbuf = new Pixbuf (orig_path);
215
// Make it square if width and height difference is within 20%
216
const double max_ratio = 1.2;
217
double ratio = (double)pixbuf.Height / pixbuf.Width;
218
int width = size, height = size;
219
if (ratio > max_ratio) {
220
width = (int)Math.Round (size / ratio);
221
}else if (ratio < 1d / max_ratio) {
222
height = (int)Math.Round (size * ratio);
225
Pixbuf scaled_pixbuf = pixbuf.ScaleSimple (width, height, Gdk.InterpType.Bilinear);
227
if (IsCachedSize (size)) {
228
Directory.Create (System.IO.Path.GetDirectoryName (path));
229
scaled_pixbuf.Save (path, "jpeg");
231
Log.InformationFormat ("Uncached artwork size {0} requested", size);
234
DisposePixbuf (pixbuf);
235
return scaled_pixbuf;
239
null_artwork_ids.Add (id);
243
public void ClearCacheFor (string id)
245
ClearCacheFor (id, false);
248
public void ClearCacheFor (string id, bool inMemoryCacheOnly)
250
if (String.IsNullOrEmpty (id)) {
254
// Clear from the in-memory cache
255
foreach (int size in scale_caches.Keys) {
256
scale_caches[size].Remove (id);
259
null_artwork_ids.Remove (id);
261
if (inMemoryCacheOnly) {
265
// And delete from disk
266
foreach (int size in CachedSizes ()) {
267
var uri = new SafeUri (CoverArtSpec.GetPathForSize (id, size));
268
if (File.Exists (uri)) {
274
public void AddCachedSize (int size)
276
cacheable_cover_sizes.Add (size);
279
public bool IsCachedSize (int size)
281
return cacheable_cover_sizes.Contains (size);
284
public IEnumerable<int> CachedSizes ()
286
return cacheable_cover_sizes;
289
public void ChangeCacheSize (int size, int max_count)
292
if (scale_caches.TryGetValue (size, out cache)) {
293
if (max_count > cache.MaxCount) {
295
"Growing surface cache for {0}px images to {1:0.00} MiB ({2} items)",
296
size, 4 * size * size * max_count / 1048576d, max_count);
297
cache.MaxCount = max_count;
301
"Creating new surface cache for {0}px images, capped at {1:0.00} MiB ({2} items)",
302
size, 4 * size * size * max_count / 1048576d, max_count);
303
scale_caches.Add (size, new SurfaceCache (max_count));
307
private static int dispose_count = 0;
308
public static void DisposePixbuf (Pixbuf pixbuf)
310
if (pixbuf != null && pixbuf.Handle != IntPtr.Zero) {
314
// There is an issue with disposing Pixbufs where we need to explicitly
315
// call the GC otherwise it doesn't get done in a timely way. But if we
316
// do it every time, it slows things down a lot; so only do it every 100th.
317
if (++dispose_count % 100 == 0) {
318
System.GC.Collect ();
324
string IService.ServiceName {
325
get { return "ArtworkManager"; }
328
#region Cache Directory Versioning/Migration
330
private const int CUR_VERSION = 3;
331
private void MigrateCacheDir ()
333
int version = CacheVersion;
334
if (version == CUR_VERSION) {
338
var legacy_root_path = CoverArtSpec.LegacyRootPath;
341
string legacy_artwork_path = Paths.Combine (LegacyPaths.ApplicationData, "covers");
343
if (!Directory.Exists (legacy_root_path)) {
344
Directory.Create (legacy_root_path);
346
if (Directory.Exists (legacy_artwork_path)) {
347
Directory.Move (new SafeUri (legacy_artwork_path), new SafeUri (legacy_root_path));
351
if (Directory.Exists (legacy_artwork_path)) {
352
Log.InformationFormat ("Deleting old (Banshee < 1.0) artwork cache directory {0}", legacy_artwork_path);
353
Directory.Delete (legacy_artwork_path, true);
359
foreach (string dir in Directory.GetDirectories (legacy_root_path)) {
361
string dirname = System.IO.Path.GetFileName (dir);
362
if (Int32.TryParse (dirname, out size) && !IsCachedSize (size)) {
363
Directory.Delete (dir, true);
369
Log.InformationFormat ("Deleted {0} extraneous album-art cache directories", deleted);
374
Log.Information ("Migrating album-art cache directory");
375
var started = DateTime.Now;
378
var root_path = CoverArtSpec.RootPath;
379
if (!Directory.Exists (root_path)) {
380
Directory.Create (root_path);
383
string sql = "SELECT Title, ArtistName FROM CoreAlbums";
384
using (var reader = new HyenaDataReader (ServiceManager.DbConnection.Query (sql))) {
385
while (reader.Read ()) {
386
var album = reader.Get<string>(0);
387
var artist = reader.Get<string>(1);
388
var old_file = CoverArtSpec.CreateLegacyArtistAlbumId (artist, album);
389
var new_file = CoverArtSpec.CreateArtistAlbumId (artist, album);
391
if (String.IsNullOrEmpty (old_file) || String.IsNullOrEmpty (new_file)) {
395
old_file = String.Format ("{0}.jpg", old_file);
396
new_file = String.Format ("{0}.jpg", new_file);
398
var old_path = new SafeUri (Paths.Combine (legacy_root_path, old_file));
399
var new_path = new SafeUri (Paths.Combine (root_path, new_file));
401
if (Banshee.IO.File.Exists (old_path) && !Banshee.IO.File.Exists (new_path)) {
402
Banshee.IO.File.Move (old_path, new_path);
408
if (ServiceManager.DbConnection.TableExists ("PodcastSyndications")) {
409
sql = "SELECT Title FROM PodcastSyndications";
410
foreach (var title in ServiceManager.DbConnection.QueryEnumerable<string> (sql)) {
411
var old_digest = CoverArtSpec.LegacyEscapePart (title);
412
var new_digest = CoverArtSpec.Digest (title);
414
if (String.IsNullOrEmpty (old_digest) || String.IsNullOrEmpty (new_digest)) {
418
var old_file = String.Format ("podcast-{0}.jpg", old_digest);
419
var new_file = String.Format ("podcast-{0}.jpg", new_digest);
421
var old_path = new SafeUri (Paths.Combine (legacy_root_path, old_file));
422
var new_path = new SafeUri (Paths.Combine (root_path, new_file));
424
if (Banshee.IO.File.Exists (old_path) && !Banshee.IO.File.Exists (new_path)) {
425
Banshee.IO.File.Move (old_path, new_path);
432
ResetScanResultCache ();
435
Directory.Delete (legacy_root_path, true);
436
Log.InformationFormat ("Migrated {0} files in {1}s", count, DateTime.Now.Subtract(started).TotalSeconds);
439
CacheVersion = CUR_VERSION;
442
private void ResetScanResultCache ()
445
ServiceManager.DbConnection.Execute ("DELETE FROM CoverArtDownloads");
446
DatabaseConfigurationClient.Client.Set<DateTime> ("last_cover_art_scan", DateTime.MinValue);
447
Log.InformationFormat ("Reset CoverArtDownloads table so missing artwork will get fetched");
451
private static SafeUri cache_version_file = new SafeUri (Paths.Combine (CoverArtSpec.RootPath, ".cache_version"));
452
private static int CacheVersion {
454
var file = cache_version_file;
455
if (!Banshee.IO.File.Exists (file)) {
456
file = new SafeUri (Paths.Combine (CoverArtSpec.LegacyRootPath, ".cache_version"));
457
if (!Banshee.IO.File.Exists (file)) {
463
using (var reader = new System.IO.StreamReader (Banshee.IO.File.OpenRead (file))) {
465
if (Int32.TryParse (reader.ReadLine (), out version)) {
474
using (var writer = new System.IO.StreamWriter (Banshee.IO.File.OpenWrite (cache_version_file, true))) {
475
writer.Write (value.ToString ());