This file is indexed.

/usr/share/decibel-audio-player/src/modules/Covers.py is in decibel-audio-player 1.04-1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA

import gui, Image, modules, os, socket, tempfile, tools, traceback, urllib2

from tools     import consts, prefs
from gettext   import gettext as _
from tools.log import logger


# Module information
MOD_INFO = ('Covers', _('Covers'), _('Show album covers'), [], False, True)
MOD_NAME = MOD_INFO[modules.MODINFO_NAME]
MOD_L10N = MOD_INFO[modules.MODINFO_L10N]

AS_API_KEY   = 'fd8dd98d26bb3f288f3e626502f9add6'   # Ingelrest François' Audioscrobbler API key
AS_TAG_START = '<image size="large">'               # The text that is right before the URL to the cover
AS_TAG_END   = '</image>'                           # The text that is right after the URL to the cover

# It seems that a non standard 'user-agent' header may cause problem, so let's cheat
USER_AGENT = 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.1) Gecko/2008072820 Firefox/3.0.1'

# We store both the paths to the thumbnail and to the full size image
(
    CVR_THUMB,
    CVR_FULL
) = range(2)

# Constants for thumbnails
THUMBNAIL_WIDTH   = 100  # Width allocated to thumbnails in the model
THUMBNAIL_HEIGHT  = 100  # Height allocated to thumbnails in the model
THUMBNAIL_OFFSETX =  11  # X-offset to render the thumbnail in the model
THUMBNAIL_OFFSETY =   3  # Y-offset to render the thumbnail in the model

# Constants for full size covers
FULL_SIZE_COVER_WIDTH  = 300
FULL_SIZE_COVER_HEIGHT = 300

# File formats we can read
ACCEPTED_FILE_FORMATS = {'.jpg': None, '.jpeg': None, '.png': None, '.gif': None}

# Default preferences
PREFS_DFT_DOWNLOAD_COVERS      = False
PREFS_DFT_PREFER_USER_COVERS   = True
PREFS_DFT_USER_COVER_FILENAMES = ['cover', 'art', 'front', '*']

# Images for thumbnails
THUMBNAIL_GLOSS = os.path.join(consts.dirPix, 'cover-gloss.png')
THUMBNAIL_MODEL = os.path.join(consts.dirPix, 'cover-model.png')


class Covers(modules.ThreadedModule):

    def __init__(self):
        """ Constructor """
        modules.ThreadedModule.__init__(self, (consts.MSG_EVT_MOD_LOADED,   consts.MSG_EVT_APP_STARTED, consts.MSG_EVT_NEW_TRACK,
                                               consts.MSG_EVT_MOD_UNLOADED, consts.MSG_EVT_APP_QUIT))


    def onModLoaded(self):
        """ The module has been loaded """
        self.cfgWin         = None                                   # Configuration window
        self.coverMap       = {}                                     # Store covers previously requested
        self.currTrack      = None                                   # The current track being played, if any
        self.cacheRootPath  = os.path.join(consts.dirCfg, MOD_NAME)  # Local cache for Internet covers
        self.coverBlacklist = {}                                     # When a cover cannot be downloaded, avoid requesting it again

        if not os.path.exists(self.cacheRootPath):
            os.mkdir(self.cacheRootPath)


    def onModUnloaded(self):
        """ The module has been unloaded """
        if self.currTrack is not None:
            modules.postMsg(consts.MSG_CMD_SET_COVER, {'track': self.currTrack, 'pathThumbnail': None, 'pathFullSize': None})

        # Delete covers that have been generated by this module
        for covers in self.coverMap.itervalues():
            if os.path.exists(covers[CVR_THUMB]):
                os.remove(covers[CVR_THUMB])
            if os.path.exists(covers[CVR_FULL]):
                os.remove(covers[CVR_FULL])
        self.coverMap = None

        # Delete blacklist
        self.coverBlacklist = None


    def generateFullSizeCover(self, inFile, outFile, format):
        """ Resize inFile if needed, and write it to outFile (outFile and inFile may be equal) """
        try:
            # Open the image
            cover = Image.open(inFile)

            # Resize in the best way we can
            if cover.size[0] < FULL_SIZE_COVER_WIDTH: newWidth = cover.size[0]
            else:                                     newWidth = FULL_SIZE_COVER_WIDTH

            if cover.size[1] < FULL_SIZE_COVER_HEIGHT: newHeight = cover.size[1]
            else:                                      newHeight = FULL_SIZE_COVER_HEIGHT

            cover = cover.resize((newWidth, newHeight), Image.ANTIALIAS)

            # We're done
            cover.save(outFile, format)
        except:
            logger.error('[%s] An error occurred while generating a showable full size cover\n\n%s' % (MOD_NAME, traceback.format_exc()))


    def generateThumbnail(self, inFile, outFile, format):
        """ Generate a thumbnail from inFile (e.g., resize it) and write it to outFile (outFile and inFile may be equal) """
        try:
            # Open the image
            cover = Image.open(inFile).convert('RGBA')

            # Resize in the best way we can
            if cover.size[0] < THUMBNAIL_WIDTH:
                newWidth = cover.size[0]
                offsetX  = (THUMBNAIL_WIDTH - cover.size[0]) / 2
            else:
                newWidth = THUMBNAIL_WIDTH
                offsetX  = 0

            if cover.size[1] < THUMBNAIL_HEIGHT:
                newHeight = cover.size[1]
                offsetY   = (THUMBNAIL_HEIGHT - cover.size[1]) / 2
            else:
                newHeight = THUMBNAIL_HEIGHT
                offsetY   = 0

            cover = cover.resize((newWidth, newHeight), Image.ANTIALIAS)

            # Paste the resized cover into our model
            model = Image.open(THUMBNAIL_MODEL).convert('RGBA')
            model.paste(cover, (THUMBNAIL_OFFSETX + offsetX, THUMBNAIL_OFFSETY + offsetY), cover)
            cover = model

            # Don't apply the gloss effect if asked to
            if not prefs.getCmdLine()[0].no_glossy_cover:
                gloss = Image.open(THUMBNAIL_GLOSS).convert('RGBA')
                cover.paste(gloss, (0, 0), gloss)

            # We're done
            cover.save(outFile, format)
        except:
            logger.error('[%s] An error occurred while generating a thumbnail\n\n%s' % (MOD_NAME, traceback.format_exc()))


    def getUserCover(self, trackPath):
        """ Return the path to a cover file in trackPath, None if no cover found """
        # Create a dictionary with candidates
        candidates = {}
        for (file, path) in tools.listDir(trackPath, True):
            (name, ext) = os.path.splitext(file.lower())
            if ext in ACCEPTED_FILE_FORMATS:
                candidates[name] = path

        # Check each possible name using the its index in the list as its priority
        for name in prefs.get(__name__, 'user-cover-filenames', PREFS_DFT_USER_COVER_FILENAMES):
            if name in candidates:
                return candidates[name]

            if name == '*' and len(candidates) != 0:
                return candidates.values()[0]

        return None


    def getFromCache(self, artist, album):
        """ Return the path to the cached cover, or None if it's not cached """
        cachePath    = os.path.join(self.cacheRootPath, str(abs(hash(artist))))
        cacheIdxPath = os.path.join(cachePath, 'INDEX')

        try:
            cacheIdx = tools.pickleLoad(cacheIdxPath)
            cover    = os.path.join(cachePath, cacheIdx[artist + album])
            if os.path.exists(cover):
                return cover
        except:
            pass

        return None


    def __getFromInternet(self, artist, album):
        """
            Try to download the cover from the Internet
            If successful, add it to the cache and return the path to it
            Otherwise, return None
        """
        # Make sure to not be blocked by the request
        socket.setdefaulttimeout(consts.socketTimeout)

        # Request information to Last.fm
        # Beware of UTF-8 characters: we need to percent-encode all characters
        try:
            url = 'http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=%s&artist=%s&album=%s' % (AS_API_KEY,
                tools.percentEncode(artist), tools.percentEncode(album))
            request = urllib2.Request(url, headers = {'User-Agent': USER_AGENT})
            stream = urllib2.urlopen(request)
            data = stream.read()
        except urllib2.HTTPError, err:
            if err.code == 400:
                logger.error('[%s] No known cover for %s / %s' % (MOD_NAME, artist, album))
            else:
                logger.error('[%s] Information request failed\n\n%s' % (MOD_NAME, traceback.format_exc()))
            return None
        except:
            logger.error('[%s] Information request failed\n\n%s' % (MOD_NAME, traceback.format_exc()))
            return None

        # Extract the URL to the cover image
        malformed = True
        startIdx  = data.find(AS_TAG_START)
        endIdx    = data.find(AS_TAG_END, startIdx)
        if startIdx != -1 and endIdx != -1:
            coverURL    = data[startIdx+len(AS_TAG_START):endIdx]
            coverFormat = os.path.splitext(coverURL)[1].lower()
            if coverURL.startswith('http://') and coverFormat in ACCEPTED_FILE_FORMATS:
                malformed = False

        if malformed:
            logger.error('[%s] Received malformed data\n\n%s' % (MOD_NAME, data))
            return None

        # Download the cover image
        try:
            request = urllib2.Request(coverURL, headers = {'User-Agent': USER_AGENT})
            stream  = urllib2.urlopen(request)
            data    = stream.read()

            if len(data) < 1024:
                raise Exception, 'The cover image seems incorrect (%u bytes is too small)' % len(data)
        except:
            logger.error('[%s] Cover image request failed\n\n%s' % (MOD_NAME, traceback.format_exc()))
            return None

        # So far, so good: let's cache the image
        cachePath    = os.path.join(self.cacheRootPath, str(abs(hash(artist))))
        cacheIdxPath = os.path.join(cachePath, 'INDEX')

        if not os.path.exists(cachePath):
            os.mkdir(cachePath)

        try:    cacheIdx = tools.pickleLoad(cacheIdxPath)
        except: cacheIdx = {}

        nextInt   = len(cacheIdx) + 1
        filename  = str(nextInt) + coverFormat
        coverPath = os.path.join(cachePath, filename)

        cacheIdx[artist + album] = filename
        tools.pickleSave(cacheIdxPath, cacheIdx)

        try:
            output = open(coverPath, 'wb')
            output.write(data)
            output.close()
            return coverPath
        except:
            logger.error('[%s] Could not save the downloaded cover\n\n%s' % (MOD_NAME, traceback.format_exc()))

        return None


    def getFromInternet(self, artist, album):
        """ Wrapper for __getFromInternet(), manage blacklist """
        coverKey = artist + album

        # If we already tried without success, don't try again
        if coverKey in self.coverBlacklist:
            return None

        # Otherwise, try to download the cover
        cover = self.__getFromInternet(artist, album)

        # If the download failed, blacklist the album
        if cover is None:
            self.coverBlacklist[coverKey] = None

        return cover


    def onNewTrack(self, track):
        """ A new track is being played, try to retrieve the corresponding cover """
        # Make sure we have enough information
        if track.getArtist() == consts.UNKNOWN_ARTIST or track.getAlbum() == consts.UNKNOWN_ALBUM:
            modules.postMsg(consts.MSG_CMD_SET_COVER, {'track': track, 'pathThumbnail': None, 'pathFullSize': None})
            return

        album          = track.getAlbum().lower()
        artist         = track.getArtist().lower()
        coverKey       = artist + album
        rawCover       = None
        self.currTrack = track

        # Let's see whether we already have the cover
        if coverKey in self.coverMap:
            covers        = self.coverMap[coverKey]
            pathFullSize  = covers[CVR_FULL]
            pathThumbnail = covers[CVR_THUMB]

            # Make sure the files are still there
            if os.path.exists(pathThumbnail) and os.path.exists(pathFullSize):
                modules.postMsg(consts.MSG_CMD_SET_COVER, {'track': track, 'pathThumbnail': pathThumbnail, 'pathFullSize': pathFullSize})
                return

        # Should we check for a user cover?
        if not prefs.get(__name__, 'download-covers', PREFS_DFT_DOWNLOAD_COVERS)        \
            or prefs.get(__name__, 'prefer-user-covers', PREFS_DFT_PREFER_USER_COVERS):
                rawCover = self.getUserCover(os.path.dirname(track.getFilePath()))

        # Is it in our cache?
        if rawCover is None:
            rawCover = self.getFromCache(artist, album)

        # If we still don't have a cover, maybe we can try to download it
        if rawCover is None:
            modules.postMsg(consts.MSG_CMD_SET_COVER, {'track': track, 'pathThumbnail': None, 'pathFullSize': None})

            if prefs.get(__name__, 'download-covers', PREFS_DFT_DOWNLOAD_COVERS):
                rawCover = self.getFromInternet(artist, album)

        # If we still don't have a cover, too bad
        # Otherwise, generate a thumbnail and a full size cover, and add it to our cover map
        if rawCover is not None:
            thumbnail     = tempfile.mktemp() + '.png'
            fullSizeCover = tempfile.mktemp() + '.png'
            self.generateThumbnail(rawCover, thumbnail, 'PNG')
            self.generateFullSizeCover(rawCover, fullSizeCover, 'PNG')
            if os.path.exists(thumbnail) and os.path.exists(fullSizeCover):
                self.coverMap[coverKey] = (thumbnail, fullSizeCover)
                modules.postMsg(consts.MSG_CMD_SET_COVER, {'track': track, 'pathThumbnail': thumbnail, 'pathFullSize': fullSizeCover})
            else:
                modules.postMsg(consts.MSG_CMD_SET_COVER, {'track': track, 'pathThumbnail': None, 'pathFullSize': None})


    # --== Message handler ==--


    def handleMsg(self, msg, params):
        """ Handle messages sent to this module """
        if msg == consts.MSG_EVT_NEW_TRACK:
            self.onNewTrack(params['track'])
        elif msg in (consts.MSG_EVT_MOD_LOADED, consts.MSG_EVT_APP_STARTED):
            self.onModLoaded()
        elif msg in (consts.MSG_EVT_MOD_UNLOADED, consts.MSG_EVT_APP_QUIT):
            self.onModUnloaded()


    # --== Configuration ==--


    def configure(self, parent):
        """ Show the configuration window """
        if self.cfgWin is None:
            self.cfgWin = gui.window.Window('Covers.glade', 'vbox1', __name__, MOD_L10N, 320, 265)
            self.cfgWin.getWidget('btn-ok').connect('clicked', self.onBtnOk)
            self.cfgWin.getWidget('img-lastfm').set_from_file(os.path.join(consts.dirPix, 'audioscrobbler.png'))
            self.cfgWin.getWidget('btn-help').connect('clicked', self.onBtnHelp)
            self.cfgWin.getWidget('chk-downloadCovers').connect('toggled', self.onDownloadCoversToggled)
            self.cfgWin.getWidget('btn-cancel').connect('clicked', lambda btn: self.cfgWin.hide())

        if not self.cfgWin.isVisible():
            downloadCovers     = prefs.get(__name__, 'download-covers',      PREFS_DFT_DOWNLOAD_COVERS)
            preferUserCovers   = prefs.get(__name__, 'prefer-user-covers',   PREFS_DFT_PREFER_USER_COVERS)
            userCoverFilenames = prefs.get(__name__, 'user-cover-filenames', PREFS_DFT_USER_COVER_FILENAMES)

            self.cfgWin.getWidget('btn-ok').grab_focus()
            self.cfgWin.getWidget('txt-filenames').set_text(', '.join(userCoverFilenames))
            self.cfgWin.getWidget('chk-downloadCovers').set_active(downloadCovers)
            self.cfgWin.getWidget('chk-preferUserCovers').set_active(preferUserCovers)
            self.cfgWin.getWidget('chk-preferUserCovers').set_sensitive(downloadCovers)

        self.cfgWin.show()


    def onBtnOk(self, btn):
        """ Save configuration """
        downloadCovers     = self.cfgWin.getWidget('chk-downloadCovers').get_active()
        preferUserCovers   = self.cfgWin.getWidget('chk-preferUserCovers').get_active()
        userCoverFilenames = [word.strip() for word in self.cfgWin.getWidget('txt-filenames').get_text().split(',')]

        prefs.set(__name__, 'download-covers',      downloadCovers)
        prefs.set(__name__, 'prefer-user-covers',   preferUserCovers)
        prefs.set(__name__, 'user-cover-filenames', userCoverFilenames)

        self.cfgWin.hide()


    def onDownloadCoversToggled(self, downloadCovers):
        """ Toggle the "prefer user covers" checkbox according to the state of the "download covers" one """
        self.cfgWin.getWidget('chk-preferUserCovers').set_sensitive(downloadCovers.get_active())


    def onBtnHelp(self, btn):
        """ Display a small help message box """
        helpDlg = gui.help.HelpDlg(MOD_L10N)
        helpDlg.addSection(_('Description'),
                           _('This module displays the cover of the album the current track comes from. Covers '
                              'may be loaded from local pictures, located in the same directory as the current '
                              'track, or may be downloaded from the Internet.'))
        helpDlg.addSection(_('User Covers'),
                           _('A user cover is a picture located in the same directory as the current track. '
                             'When specifying filenames, you do not need to provide file extensions, supported '
                             'file formats (%s) are automatically used.' % ', '.join(ACCEPTED_FILE_FORMATS.iterkeys())))
        helpDlg.addSection(_('Internet Covers'),
                           _('Covers may be downloaded from the Internet, based on the tags of the current track. '
                             'You can ask to always prefer user covers to Internet ones. In this case, if a user '
                             'cover exists for the current track, it is used. If there is none, the cover is downloaded.'))
        helpDlg.show(self.cfgWin)