Пример #1
0
class Covers(modules.ThreadedModule):
    def __init__(self):
        """ Constructor """
        handlers = {
            consts.MSG_EVT_APP_QUIT: self.onModUnloaded,
            consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
            consts.MSG_EVT_MOD_LOADED: self.onModLoaded,
            consts.MSG_EVT_APP_STARTED: self.onModLoaded,
            consts.MSG_EVT_MOD_UNLOADED: self.onModUnloaded,
        }

        modules.ThreadedModule.__init__(self, handlers)

    def _generateCover(self, inFile, outFile, format, max_width, max_height):
        from PIL import Image

        try:
            # Open the image
            cover = Image.open(inFile)
            width = cover.size[0]
            height = cover.size[1]
            newWidth, newHeight = tools.resize(width, height, max_width,
                                               max_height)

            cover = cover.resize((newWidth, newHeight), Image.ANTIALIAS)
            cover.save(outFile, format)
        except Exception:
            # This message will probably be displayed for the thumbnail and the big cover.
            logger.error(
                '[%s] An error occurred while generating the cover for "%s"\n\n%s'
                % (MOD_NAME, inFile, traceback.format_exc()))
            # Remove corrupted file.
            tools.remove(outFile)

    def generateFullSizeCover(self, inFile, outFile, format):
        """ Resize inFile if needed, and write it to outFile (outFile and inFile may be equal) """
        self._generateCover(inFile, outFile, format, FULL_SIZE_COVER_WIDTH,
                            FULL_SIZE_COVER_HEIGHT)

    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).
        """
        self._generateCover(inFile, outFile, format, THUMBNAIL_WIDTH,
                            THUMBNAIL_HEIGHT)

    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 next(iter(candidates.values()))

        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
        """
        import socket, urllib.request, urllib.error, urllib.parse

        # 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 = urllib.request.Request(
                url, headers={'User-Agent': USER_AGENT})
            stream = urllib.request.urlopen(request)
            data = stream.read().decode('utf-8')
        except urllib.error.HTTPError as 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 urllib.error.URLError:
            logger.info('[%s] Could not fetch cover. No internet connection.' %
                        MOD_NAME)
            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://',
                 'https://')) and coverFormat in ACCEPTED_FILE_FORMATS:
                malformed = False

        if malformed:
            ## Do not show the data in the log every time no cover is found
            if coverURL:
                logger.error('[%s] Received malformed data\n\n%s' %
                             (MOD_NAME, data))
            return None

        # Download the cover image
        try:
            request = urllib.request.Request(
                coverURL, headers={'User-Agent': USER_AGENT})
            stream = urllib.request.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' % MOD_NAME)
            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 """
        # If we already tried without success, don't try again
        if (artist, album) 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[(artist, album)] = None

        return cover

    # --== Message handlers ==--

    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.values():
            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 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()
        rawCover = None
        self.currTrack = track

        # Let's see whether we already have the cover
        if (artist, album) in self.coverMap:
            covers = self.coverMap[(artist, album)]
            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:
            import tempfile

            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[(artist, album)] = (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
                })

    # --== Configuration ==--

    def configure(self, parent):
        """ Show the configuration window """
        if self.cfgWin is None:
            from gui.window import Window

            self.cfgWin = Window('Covers.ui', 'vbox1', __name__,
                                 MOD_INFO[modules.MODINFO_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 """
        from gui import help

        helpDlg = help.HelpDlg(MOD_INFO[modules.MODINFO_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.keys())))
        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)
Пример #2
0
class IMStatus(modules.Module):

    def __init__(self):
        """ Constructor """
        handlers = {
                        consts.MSG_EVT_PAUSED:       self.onPaused,
                        consts.MSG_EVT_STOPPED:      self.onStopped,
                        consts.MSG_EVT_UNPAUSED:     self.onUnpaused,
                        consts.MSG_EVT_APP_QUIT:     self.onStopped,
                        consts.MSG_EVT_NEW_TRACK:    self.onNewTrack,
                        consts.MSG_EVT_MOD_LOADED:   self.onModLoaded,
                        consts.MSG_EVT_APP_STARTED:  self.onModLoaded,
                        consts.MSG_EVT_MOD_UNLOADED: self.onStopped,
                   }

        modules.Module.__init__(self, handlers)


    def __format(self, string, track):
        """ Replace the special fields in the given string by their corresponding value and sanitize the result """
        result = track.format(string)

        if len(prefs.get(__name__, 'sanitized-words', DEFAULT_SANITIZED_WORDS)) != 0:
            lowerResult = result.lower()
            for word in [w.lower() for w in prefs.get(__name__, 'sanitized-words', DEFAULT_SANITIZED_WORDS).split('\n') if len(w) > 2]:
                pos = lowerResult.find(word)
                while pos != -1:
                    result      = result[:pos+1] + ('*' * (len(word)-2)) + result[pos+len(word)-1:]
                    lowerResult = lowerResult[:pos+1] + ('*' * (len(word)-2)) + lowerResult[pos+len(word)-1:]
                    pos         = lowerResult.find(word)

        return result


    def setStatusMsg(self, status):
        """ Update the status of all accounts of all active IM clients """
        for client in self.clients:
            for account in client[IM_ACCOUNTS]:
                client[IM_INSTANCE].setStatusMsg(account, status)


   # --== Message handlers ==--


    def onModLoaded(self):
        """ Initialize the module """
        self.track     = None   # Current track
        self.status    = ''     # The currently used status
        self.paused    = False  # True if the current track is paused
        self.clients   = []     # Clients currently active
        self.cfgWindow = None   # Configuration window

        # Detect active clients
        try:
            import dbus

            session        = dbus.SessionBus()
            activeServices = session.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus').ListNames()
            for activeClient in [client for client in CLIENTS if client[IM_DBUS_SERVICE_NAME] in activeServices]:
                obj       = session.get_object(activeClient[IM_DBUS_SERVICE_NAME], activeClient[IM_DBUS_OBJECT_NAME])
                interface = dbus.Interface(obj, activeClient[IM_DBUS_INTERFACE_NAME])

                activeClient[IM_INSTANCE] = activeClient[IM_CLASS](interface)
                activeClient[IM_ACCOUNTS] = activeClient[IM_INSTANCE].listAccounts()

                logger.info('[%s] Found %s instance' % (MOD_NAME, activeClient[IM_NAME]))
                self.clients.append(activeClient)
        except:
            logger.error('[%s] Error while initializing\n\n%s' % (MOD_NAME, traceback.format_exc()))


    def onNewTrack(self, track):
        """ A new track is being played """
        self.track  = track
        self.status = self.__format(prefs.get(__name__, 'status-msg', DEFAULT_STATUS_MSG), track)
        self.paused = False
        self.setStatusMsg(self.status)


    def onStopped(self):
        """ The current track has been stopped """
        self.track  = None
        self.paused = False
        if prefs.get(__name__, 'stop-action', DEFAULT_STOP_ACTION) == STOP_SET_STATUS:
            self.setStatusMsg(prefs.get(__name__, 'stop-status', DEFAULT_STOP_STATUS))


    def onPaused(self):
        """ The current track has been paused """
        self.paused = True
        if prefs.get(__name__, 'update-on-paused', DEFAULT_UPDATE_ON_PAUSED):
            self.setStatusMsg(_('%(status)s [paused]') % {'status': self.status})


    def onUnpaused(self):
        """ The current track has been unpaused """
        self.paused = False
        self.setStatusMsg(self.status)


    # --== Configuration ==--


    def configure(self, parent):
        """ Show the configuration window """
        if self.cfgWindow is None:
            from gui.window import Window

            self.cfgWindow = Window('IMStatus.ui', 'vbox1', __name__, _(MOD_NAME), 440, 290)
            # GTK handlers
            self.cfgWindow.getWidget('rad-stopDoNothing').connect('toggled', self.onRadToggled)
            self.cfgWindow.getWidget('rad-stopSetStatus').connect('toggled', self.onRadToggled)
            self.cfgWindow.getWidget('btn-ok').connect('clicked', self.onBtnOk)
            self.cfgWindow.getWidget('btn-cancel').connect('clicked', lambda btn: self.cfgWindow.hide())
            self.cfgWindow.getWidget('btn-help').connect('clicked', self.onBtnHelp)

        if not self.cfgWindow.isVisible():
            self.cfgWindow.getWidget('txt-status').set_text(prefs.get(__name__, 'status-msg', DEFAULT_STATUS_MSG))
            self.cfgWindow.getWidget('chk-updateOnPaused').set_active(prefs.get(__name__, 'update-on-paused', DEFAULT_UPDATE_ON_PAUSED))
            self.cfgWindow.getWidget('chk-updateWhenAway').set_active(prefs.get(__name__, 'update-when-away', DEFAULT_UPDATE_WHEN_AWAY))
            self.cfgWindow.getWidget('rad-stopDoNothing').set_active(prefs.get(__name__, 'stop-action', DEFAULT_STOP_ACTION) == STOP_DO_NOTHING)
            self.cfgWindow.getWidget('rad-stopSetStatus').set_active(prefs.get(__name__, 'stop-action', DEFAULT_STOP_ACTION) == STOP_SET_STATUS)
            self.cfgWindow.getWidget('txt-stopStatus').set_sensitive(prefs.get(__name__, 'stop-action', DEFAULT_STOP_ACTION) == STOP_SET_STATUS)
            self.cfgWindow.getWidget('txt-stopStatus').set_text(prefs.get(__name__, 'stop-status', DEFAULT_STOP_STATUS))
            self.cfgWindow.getWidget('txt-sanitizedWords').get_buffer().set_text(prefs.get(__name__, 'sanitized-words', DEFAULT_SANITIZED_WORDS))
            self.cfgWindow.getWidget('btn-ok').grab_focus()

        self.cfgWindow.show()


    def onRadToggled(self, btn):
        """ A radio button has been toggled """
        self.cfgWindow.getWidget('txt-stopStatus').set_sensitive(self.cfgWindow.getWidget('rad-stopSetStatus').get_active())


    def onBtnOk(self, btn):
        """ Save new preferences """
        prefs.set(__name__, 'status-msg', self.cfgWindow.getWidget('txt-status').get_text())
        prefs.set(__name__, 'update-on-paused', self.cfgWindow.getWidget('chk-updateOnPaused').get_active())
        prefs.set(__name__, 'update-when-away', self.cfgWindow.getWidget('chk-updateWhenAway').get_active())
        (start, end) = self.cfgWindow.getWidget('txt-sanitizedWords').get_buffer().get_bounds()
        prefs.set(__name__, 'sanitized-words', self.cfgWindow.getWidget('txt-sanitizedWords').get_buffer().get_text(start, end).strip())
        if self.cfgWindow.getWidget('rad-stopDoNothing').get_active():
            prefs.set(__name__, 'stop-action', STOP_DO_NOTHING)
        else:
            prefs.set(__name__, 'stop-action', STOP_SET_STATUS)
            prefs.set(__name__, 'stop-status', self.cfgWindow.getWidget('txt-stopStatus').get_text())
        self.cfgWindow.hide()
        # Update status
        if self.track is not None:
            self.status = self.__format(prefs.get(__name__, 'status-msg', DEFAULT_STATUS_MSG), self.track)
            if self.paused: self.setStatusMsg(_('%(status)s [paused]') % {'status': self.status})
            else:           self.setStatusMsg(self.status)


    def onBtnHelp(self, btn):
        """ Display a small help message box """
        import gui.help, media.track

        helpDlg = gui.help.HelpDlg(_(MOD_NAME))
        helpDlg.addSection(_('Description'),
                           _('This module detects any running instant messenger and updates your status with regards to the track '
                             'you are listening to. Supported messengers are:')
                           + '\n\n * ' + '\n * '.join(sorted([client[IM_NAME] for client in CLIENTS])))
        helpDlg.addSection(_('Customizing the Status'),
                           _('You can set the status to any text you want. Before setting it, the module replaces all fields of '
                             'the form {field} by their corresponding value. Available fields are:')
                           + '\n\n' + media.track.getFormatSpecialFields(False))
        helpDlg.addSection(_('Markup'),
                           _('You can use the Pango markup language to format the text. More information on that language is '
                             'available on the following web page:')
                           + '\n\nhttp://www.pygtk.org/pygtk2reference/pango-markup-language.html')
        helpDlg.addSection(_('Sanitization'),
                           _('You can define some words that to sanitize before using them to set your status. In this '
                             'case, the middle characters of matching words is automatically replaced with asterisks '
                             '(e.g., "Metallica - Live S**t Binge & Purge"). Put one word per line.'))
        helpDlg.show(self.cfgWindow)