~ubuntu-branches/ubuntu/oneiric/rhythmbox/oneiric

« back to all changes in this revision

Viewing changes to plugins/artdisplay/CoverArtDatabase.py

  • Committer: Bazaar Package Importer
  • Author(s): Rico Tzschichholz
  • Date: 2011-07-29 16:41:38 UTC
  • mto: This revision was merged to the branch mainline in revision 191.
  • Revision ID: james.westby@ubuntu.com-20110729164138-wwicy8nqalm18ck7
Tags: upstream-2.90.1~20110802
ImportĀ upstreamĀ versionĀ 2.90.1~20110802

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
 
2
#
 
3
# Copyright (C) 2006 - Gareth Murphy, Martin Szulecki,
 
4
# Ed Catmur <ed@catmur.co.uk>
 
5
#
 
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)
 
9
# any later version.
 
10
#
 
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.
 
18
#
 
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.
 
23
#
 
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.
 
27
 
 
28
import os
 
29
import itertools
 
30
import gobject
 
31
import gi
 
32
 
 
33
import rb
 
34
from gi.repository import GdkPixbuf
 
35
from gi.repository import RB
 
36
 
 
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
 
42
 
 
43
from urllib import unquote, pathname2url
 
44
 
 
45
ART_SEARCHES_LOCAL = [LocalCoverArtSearch, EmbeddedCoverArtSearch]
 
46
ART_SEARCHES_REMOTE = [PodcastCoverArtSearch, LastFMCoverArtSearch, MusicBrainzCoverArtSearch]
 
47
 
 
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"]
 
58
 
 
59
class TicketSystem:
 
60
        def __init__ (self):
 
61
                self.counter = itertools.count ()
 
62
                self.hash = {}
 
63
                self.dead = set ()
 
64
 
 
65
        def dump (self):
 
66
                for k in self.hash.keys():
 
67
                        print " item %s: %s" % (str(k), self.hash[k])
 
68
 
 
69
                print "dead tickets: %s" % str(self.dead)
 
70
 
 
71
        def get (self, item):
 
72
                ticket = self.counter.next ()
 
73
                self.hash.setdefault (item, set ()).add (ticket)
 
74
                return ticket
 
75
 
 
76
        def find (self, item, comparator, *args):
 
77
                for titem in self.hash.keys():
 
78
                        if comparator(item, titem, *args):
 
79
                                return titem
 
80
                return None
 
81
 
 
82
        def bury (self, ticket):
 
83
                try:
 
84
                        self.dead.remove (ticket)
 
85
                        return True
 
86
                except KeyError:
 
87
                        return False
 
88
 
 
89
        def forget (self, item, ticket):
 
90
                try:
 
91
                        tickets = self.hash[item]
 
92
                        tickets.remove(ticket)
 
93
                        if len(tickets) == 0:
 
94
                                del self.hash[item]
 
95
                        return True
 
96
                except KeyError:
 
97
                        self.dead.remove (ticket)
 
98
                        return False
 
99
 
 
100
        def purge (self, item):
 
101
                tickets = self.hash.pop (item, set())
 
102
                self.dead.update (tickets)
 
103
 
 
104
        def release (self, item, ticket):
 
105
                try:
 
106
                        othertickets = self.hash.pop (item) - set([ticket])
 
107
                        self.dead.update (othertickets)
 
108
                        return True
 
109
                except KeyError:
 
110
                        self.dead.remove (ticket)
 
111
                        return False
 
112
 
 
113
def get_search_props(db, entry):
 
114
        artist = entry.get_string(RB.RhythmDBPropType.ALBUM_ARTIST)
 
115
        if artist == "":
 
116
                artist = entry.get_string(RB.RhythmDBPropType.ARTIST)
 
117
        album = entry.get_string(RB.RhythmDBPropType.ALBUM)
 
118
        return (artist, album)
 
119
 
 
120
 
 
121
class CoverArtDatabase (object):
 
122
        def __init__ (self):
 
123
                self.ticket = TicketSystem ()
 
124
                self.same_search = {}
 
125
 
 
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)
 
131
 
 
132
                artist = artist.replace('/', '-')
 
133
                album = album.replace('/', '-')
 
134
                return art_folder + '/%s - %s.%s' % (artist, album, extension)
 
135
 
 
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
 
142
 
 
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)
 
146
 
 
147
                l = rb.Loader()
 
148
                l.get_url (str (uri), loader_cb)
 
149
 
 
150
        def set_pixbuf (self, db, entry, pixbuf, callback):
 
151
                if entry is None or pixbuf is None:
 
152
                        return
 
153
 
 
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:
 
157
                        try:
 
158
                                Engine ().save_pixbuf (db, entry, pixbuf)
 
159
                        except AttributeError:
 
160
                                pass
 
161
 
 
162
        def cache_pixbuf (self, db, entry, pixbuf):
 
163
                if entry is None or pixbuf is None:
 
164
                        return None
 
165
 
 
166
                meta_location = self.build_art_cache_filename (db, entry, ART_CACHE_EXTENSION_META)
 
167
                self.write_meta_file (meta_location, None, None)
 
168
 
 
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
 
174
                else:
 
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)
 
182
 
 
183
        def cancel_get_pixbuf (self, entry):
 
184
                self.ticket.purge (entry)
 
185
 
 
186
        def get_pixbuf (self, db, entry, is_playing, callback):
 
187
                if entry is None:
 
188
                        callback (entry, None, None, None, None)
 
189
                        return
 
190
 
 
191
                st_artist, st_album = get_search_props(db, entry)
 
192
 
 
193
                # replace quote characters
 
194
                # don't replace single quote: could be important punctuation
 
195
                for char in ["\""]:
 
196
                        st_artist = st_artist.replace (char, '')
 
197
                        st_album = st_album.replace (char, '')
 
198
 
 
199
                rb.Coroutine (self.image_search, db, st_album, st_artist, entry, is_playing, callback).begin ()
 
200
 
 
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")
 
206
 
 
207
                art_location = None
 
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
 
212
 
 
213
                # Check local cache
 
214
                if art_location:
 
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)
 
220
                        return
 
221
 
 
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)
 
228
 
 
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)
 
235
                        return
 
236
 
 
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
 
241
                        plexer.clear ()
 
242
                        engine.search (db, entry, is_playing, plexer.send ())
 
243
                        while True:
 
244
                                yield None
 
245
                                _, (engine, entry, results) = plexer.receive ()
 
246
                                if not results:
 
247
                                        break
 
248
 
 
249
                                def handle_result_pixbuf (pixbuf, engine_uri, tooltip_image, tooltip_text, should_save):
 
250
                                        if self.ticket.release (entry, ticket):
 
251
                                                if should_save:
 
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)
 
255
                                                        else:
 
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)
 
258
 
 
259
                                                        self.write_meta_file (art_location_meta, tooltip_image, tooltip_text)
 
260
                                                else:
 
261
                                                        uri = engine_uri
 
262
 
 
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)
 
268
 
 
269
                                        self.write_blist (blist_location, blist)
 
270
                                        self.same_search.pop (entry, None)
 
271
 
 
272
                                # fetch the meta details for the engine
 
273
                                (tooltip_image, tooltip_text) = engine.get_result_meta (results)
 
274
 
 
275
                                # first check if the engine gave us a pixbuf
 
276
                                pixbuf = engine.get_result_pixbuf (results)
 
277
                                if pixbuf:
 
278
                                        handle_result_pixbuf (pixbuf, None, tooltip_image, tooltip_text, True)
 
279
                                        return
 
280
 
 
281
                                # then check URIs
 
282
                                for url in engine.get_best_match_urls (results):
 
283
                                        if str(url) == "":
 
284
                                                print "got empty url from engine %s." % (engine)
 
285
                                                continue
 
286
 
 
287
                                        l = rb.Loader()
 
288
                                        yield l.get_url (str (url), plexer.send ())
 
289
                                        _, (data, ) = plexer.receive ()
 
290
                                        pixbuf = self.image_data_load (data)
 
291
                                        if pixbuf:
 
292
                                                handle_result_pixbuf (pixbuf, url, tooltip_image, tooltip_text, engine_remote)
 
293
                                                return
 
294
 
 
295
                                if not engine.search_next ():
 
296
                                        if engine_remote:
 
297
                                                blist.append (engine_name)
 
298
                                        break
 
299
 
 
300
                                if self.ticket.bury (ticket):
 
301
                                        self.write_blist (blist_location, blist)
 
302
                                        self.same_search.pop (entry, None)
 
303
                                        return
 
304
 
 
305
                        if self.ticket.bury (ticket):
 
306
                                self.write_blist (blist_location, blist)
 
307
                                self.same_search.pop (entry, None)
 
308
                                return
 
309
 
 
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)
 
316
 
 
317
                self.write_blist (blist_location, blist)
 
318
                self.same_search.pop (entry, None)
 
319
 
 
320
 
 
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])
 
325
                else:
 
326
                        return (None, None)
 
327
 
 
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"])
 
332
                        meta_file.close()
 
333
                elif os.path.exists (meta_location):
 
334
                        os.unlink (meta_location)
 
335
 
 
336
 
 
337
        def read_blist (self, blist_location):
 
338
                if os.path.exists (blist_location):
 
339
                        return [line.strip () for line in file (blist_location)]
 
340
                else:
 
341
                        return []
 
342
 
 
343
        def write_blist (self, blist_location, blist):
 
344
                if blist:
 
345
                        blist_file = file (blist_location, 'w')
 
346
                        blist_file.writelines (blist)
 
347
                        blist_file.close ()
 
348
                elif os.path.exists (blist_location):
 
349
                        os.unlink (blist_location)
 
350
 
 
351
        def image_data_load(self, data):
 
352
                if data and len (data) >= 1000:
 
353
                        try:
 
354
                                l = GdkPixbuf.PixbufLoader()
 
355
                                l.write(data)
 
356
                                l.close()
 
357
                                return l.get_pixbuf()
 
358
                        except gobject.GError, e:
 
359
                                print "error reading image: %s" % str(e)
 
360
                                import sys
 
361
                                sys.excepthook(*sys.exc_info())
 
362
                                pass
 
363
 
 
364
                return None