1
# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
3
# Copyright (C) 2006 - Gareth Murphy, Martin Szulecki,
4
# Ed Catmur <ed@catmur.co.uk>
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2, or (at your option)
11
# The Rhythmbox authors hereby grant permission for non-GPL compatible
12
# GStreamer plugins to be used and distributed together with GStreamer
13
# and Rhythmbox. This permission is above and beyond the permissions granted
14
# by the GPL license by which Rhythmbox is covered. If you modify this code
15
# you may extend this exception to your version of the code, but you are not
16
# obligated to do so. If you do not wish to do so, delete this exception
17
# statement from your version.
19
# This program is distributed in the hope that it will be useful,
20
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
# GNU General Public License for more details.
24
# You should have received a copy of the GNU General Public License
25
# along with this program; if not, write to the Free Software
26
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
34
from gi.repository import GdkPixbuf
35
from gi.repository import RB
37
from PodcastCoverArtSearch import PodcastCoverArtSearch
38
from MusicBrainzCoverArtSearch import MusicBrainzCoverArtSearch
39
from LastFMCoverArtSearch import LastFMCoverArtSearch
40
from EmbeddedCoverArtSearch import EmbeddedCoverArtSearch
41
from LocalCoverArtSearch import LocalCoverArtSearch
43
from urllib import unquote, pathname2url
45
ART_SEARCHES_LOCAL = [LocalCoverArtSearch, EmbeddedCoverArtSearch]
46
ART_SEARCHES_REMOTE = [PodcastCoverArtSearch, LastFMCoverArtSearch, MusicBrainzCoverArtSearch]
48
ART_FOLDER = os.path.join(RB.user_cache_dir(), 'covers')
49
ART_CACHE_EXTENSION_JPG = 'jpg'
50
ART_CACHE_EXTENSION_PNG = 'png'
51
ART_CACHE_EXTENSION_META = 'rb-meta'
52
ART_CACHE_FORMAT_JPG = 'jpeg'
53
ART_CACHE_FORMAT_PNG = 'png'
54
ART_CACHE_SETTINGS_NAMES_JPG = ["quality"]
55
ART_CACHE_SETTINGS_VALUES_JPG = ["100"]
56
ART_CACHE_SETTINGS_NAMES_PNG = ["compression"]
57
ART_CACHE_SETTINGS_VALUES_PNG = ["9"]
61
self.counter = itertools.count ()
66
for k in self.hash.keys():
67
print " item %s: %s" % (str(k), self.hash[k])
69
print "dead tickets: %s" % str(self.dead)
72
ticket = self.counter.next ()
73
self.hash.setdefault (item, set ()).add (ticket)
76
def find (self, item, comparator, *args):
77
for titem in self.hash.keys():
78
if comparator(item, titem, *args):
82
def bury (self, ticket):
84
self.dead.remove (ticket)
89
def forget (self, item, ticket):
91
tickets = self.hash[item]
92
tickets.remove(ticket)
97
self.dead.remove (ticket)
100
def purge (self, item):
101
tickets = self.hash.pop (item, set())
102
self.dead.update (tickets)
104
def release (self, item, ticket):
106
othertickets = self.hash.pop (item) - set([ticket])
107
self.dead.update (othertickets)
110
self.dead.remove (ticket)
113
def get_search_props(db, entry):
114
artist = entry.get_string(RB.RhythmDBPropType.ALBUM_ARTIST)
116
artist = entry.get_string(RB.RhythmDBPropType.ARTIST)
117
album = entry.get_string(RB.RhythmDBPropType.ALBUM)
118
return (artist, album)
121
class CoverArtDatabase (object):
123
self.ticket = TicketSystem ()
124
self.same_search = {}
126
def build_art_cache_filename (self, db, entry, extension):
127
artist, album = get_search_props(db, entry)
128
art_folder = os.path.expanduser (ART_FOLDER)
129
if not os.path.exists (art_folder):
130
os.makedirs (art_folder)
132
artist = artist.replace('/', '-')
133
album = album.replace('/', '-')
134
return art_folder + '/%s - %s.%s' % (artist, album, extension)
136
def engines (self, blist):
137
for Engine in ART_SEARCHES_LOCAL:
138
yield Engine (), Engine.__name__, False
139
for Engine in ART_SEARCHES_REMOTE:
140
if Engine.__name__ not in blist:
141
yield Engine (), Engine.__name__, True
143
def set_pixbuf_from_uri (self, db, entry, uri, callback):
144
def loader_cb (data):
145
self.set_pixbuf (db, entry, self.image_data_load (data), callback)
148
l.get_url (str (uri), loader_cb)
150
def set_pixbuf (self, db, entry, pixbuf, callback):
151
if entry is None or pixbuf is None:
154
art_location_url = self.cache_pixbuf(db, entry, pixbuf)
155
callback (entry, pixbuf, art_location_url, None, None)
156
for Engine in ART_SEARCHES_LOCAL:
158
Engine ().save_pixbuf (db, entry, pixbuf)
159
except AttributeError:
162
def cache_pixbuf (self, db, entry, pixbuf):
163
if entry is None or pixbuf is None:
166
meta_location = self.build_art_cache_filename (db, entry, ART_CACHE_EXTENSION_META)
167
self.write_meta_file (meta_location, None, None)
169
if pixbuf.get_has_alpha():
170
art_location = self.build_art_cache_filename (db, entry, ART_CACHE_EXTENSION_PNG)
171
art_cache_format = ART_CACHE_FORMAT_PNG
172
art_cache_settings_names = ART_CACHE_SETTINGS_NAMES_PNG
173
art_cache_settings_values = ART_CACHE_SETTINGS_VALUES_PNG
175
art_location = self.build_art_cache_filename (db, entry, ART_CACHE_EXTENSION_JPG)
176
art_cache_format = ART_CACHE_FORMAT_JPG
177
art_cache_settings_names = ART_CACHE_SETTINGS_NAMES_JPG
178
art_cache_settings_values = ART_CACHE_SETTINGS_VALUES_JPG
179
self.ticket.purge (entry)
180
pixbuf.savev (art_location, art_cache_format, art_cache_settings_names, art_cache_settings_values)
181
return "file://" + pathname2url(art_location)
183
def cancel_get_pixbuf (self, entry):
184
self.ticket.purge (entry)
186
def get_pixbuf (self, db, entry, is_playing, callback):
188
callback (entry, None, None, None, None)
191
st_artist, st_album = get_search_props(db, entry)
193
# replace quote characters
194
# don't replace single quote: could be important punctuation
196
st_artist = st_artist.replace (char, '')
197
st_album = st_album.replace (char, '')
199
rb.Coroutine (self.image_search, db, st_album, st_artist, entry, is_playing, callback).begin ()
201
def image_search (self, plexer, db, st_album, st_artist, entry, is_playing, callback):
202
art_location_jpg = self.build_art_cache_filename (db, entry, ART_CACHE_EXTENSION_JPG)
203
art_location_png = self.build_art_cache_filename (db, entry, ART_CACHE_EXTENSION_PNG)
204
art_location_meta = self.build_art_cache_filename (db, entry, ART_CACHE_EXTENSION_META)
205
blist_location = self.build_art_cache_filename (db, entry, "rb-blist")
208
if os.path.exists (art_location_jpg):
209
art_location = art_location_jpg
210
if os.path.exists (art_location_png):
211
art_location = art_location_png
215
self.ticket.purge (entry)
216
pixbuf = GdkPixbuf.Pixbuf.new_from_file (art_location)
217
(tooltip_image, tooltip_text) = self.read_meta_file (art_location_meta)
218
art_location_url = "file://" + pathname2url(art_location)
219
callback (entry, pixbuf, art_location_url, tooltip_image, tooltip_text)
222
# Check if we're already searching for art for this album
223
# (this won't work for compilations, but there isn't much we can do about that)
224
def find_same_search(a, b, db):
225
a_artist, a_album = get_search_props(db, a)
226
b_artist, b_album = get_search_props(db, b)
227
return (a_artist == b_artist) and (a_album == b_album)
229
match_entry = self.ticket.find(entry, find_same_search, db)
230
if match_entry is not None:
231
print "entry %s matches existing search for %s" % (
232
entry.get_string(RB.RhythmDBPropType.LOCATION),
233
match_entry.get_string(RB.RhythmDBPropType.LOCATION))
234
self.same_search.setdefault (match_entry, []).append(entry)
237
blist = self.read_blist (blist_location)
238
ticket = self.ticket.get (entry)
239
for engine, engine_name, engine_remote in self.engines (blist):
240
print "trying %s" % engine_name
242
engine.search (db, entry, is_playing, plexer.send ())
245
_, (engine, entry, results) = plexer.receive ()
249
def handle_result_pixbuf (pixbuf, engine_uri, tooltip_image, tooltip_text, should_save):
250
if self.ticket.release (entry, ticket):
252
if pixbuf.get_has_alpha ():
253
pixbuf.savev (art_location_png, ART_CACHE_FORMAT_PNG, ART_CACHE_SETTINGS_NAMES_PNG, ART_CACHE_SETTINGS_VALUES_PNG)
254
uri = "file://" + pathname2url(art_location_png)
256
pixbuf.savev (art_location_jpg, ART_CACHE_FORMAT_JPG, ART_CACHE_SETTINGS_NAMES_JPG, ART_CACHE_SETTINGS_VALUES_JPG)
257
uri = "file://" + pathname2url(art_location_jpg)
259
self.write_meta_file (art_location_meta, tooltip_image, tooltip_text)
263
print "found image for %s" % (entry.get_string(RB.RhythmDBPropType.LOCATION))
264
callback (entry, pixbuf, uri, tooltip_image, tooltip_text)
265
for m in self.same_search.pop(entry, []):
266
print "and for same search %s" % (m.get_string(RB.RhythmDBPropType.LOCATION))
267
callback (m, pixbuf, uri, tooltip_image, tooltip_text)
269
self.write_blist (blist_location, blist)
270
self.same_search.pop (entry, None)
272
# fetch the meta details for the engine
273
(tooltip_image, tooltip_text) = engine.get_result_meta (results)
275
# first check if the engine gave us a pixbuf
276
pixbuf = engine.get_result_pixbuf (results)
278
handle_result_pixbuf (pixbuf, None, tooltip_image, tooltip_text, True)
282
for url in engine.get_best_match_urls (results):
284
print "got empty url from engine %s." % (engine)
288
yield l.get_url (str (url), plexer.send ())
289
_, (data, ) = plexer.receive ()
290
pixbuf = self.image_data_load (data)
292
handle_result_pixbuf (pixbuf, url, tooltip_image, tooltip_text, engine_remote)
295
if not engine.search_next ():
297
blist.append (engine_name)
300
if self.ticket.bury (ticket):
301
self.write_blist (blist_location, blist)
302
self.same_search.pop (entry, None)
305
if self.ticket.bury (ticket):
306
self.write_blist (blist_location, blist)
307
self.same_search.pop (entry, None)
310
if self.ticket.forget (entry, ticket):
311
print "didn't find image for %s" % (entry.get_string(RB.RhythmDBPropType.LOCATION))
312
callback (entry, None, None, None, None)
313
for m in self.same_search.pop (entry, []):
314
print "or for same search %s" % (m.get_string(RB.RhythmDBPropType.LOCATION))
315
callback (m, None, None, None, None)
317
self.write_blist (blist_location, blist)
318
self.same_search.pop (entry, None)
321
def read_meta_file (self, meta_location):
322
if os.path.exists (meta_location):
323
data = [line.strip () for line in file (meta_location)]
324
return (data[0], data[1])
328
def write_meta_file (self, meta_location, tooltip_image, tooltip_text):
329
if tooltip_text is not None:
330
meta_file = file (meta_location, 'w')
331
meta_file.writelines([tooltip_image, "\n", tooltip_text, "\n"])
333
elif os.path.exists (meta_location):
334
os.unlink (meta_location)
337
def read_blist (self, blist_location):
338
if os.path.exists (blist_location):
339
return [line.strip () for line in file (blist_location)]
343
def write_blist (self, blist_location, blist):
345
blist_file = file (blist_location, 'w')
346
blist_file.writelines (blist)
348
elif os.path.exists (blist_location):
349
os.unlink (blist_location)
351
def image_data_load(self, data):
352
if data and len (data) >= 1000:
354
l = GdkPixbuf.PixbufLoader()
357
return l.get_pixbuf()
358
except gobject.GError, e:
359
print "error reading image: %s" % str(e)
361
sys.excepthook(*sys.exc_info())