diff options
author | Anton Khirnov <wyskas@gmail.com> | 2009-08-20 13:48:29 +0200 |
---|---|---|
committer | Anton Khirnov <wyskas@gmail.com> | 2009-08-20 13:55:30 +0200 |
commit | 368cf0ee36cce1c40b9299adf056d17ebca32e89 (patch) | |
tree | 525d0d0ae9acd50aa1b7f511f74f46c915ca4c39 /nephilim/plugins/AlbumCover.py | |
parent | 16be49f2deb42293f85b90b149516c643cc4f395 (diff) |
AlbumCover: rewrite to use same design as Lyrics
some features got removed in the process, should be put back soon.
Diffstat (limited to 'nephilim/plugins/AlbumCover.py')
-rw-r--r-- | nephilim/plugins/AlbumCover.py | 544 |
1 files changed, 287 insertions, 257 deletions
diff --git a/nephilim/plugins/AlbumCover.py b/nephilim/plugins/AlbumCover.py index 8321944..e5345d1 100644 --- a/nephilim/plugins/AlbumCover.py +++ b/nephilim/plugins/AlbumCover.py @@ -1,5 +1,4 @@ # -# Copyright (C) 2008 jerous <jerous@gmail.com> # Copyright (C) 2009 Anton Khirnov <wyskas@gmail.com> # # Nephilim is free software: you can redistribute it and/or modify @@ -16,19 +15,16 @@ # along with Nephilim. If not, see <http://www.gnu.org/licenses/>. # -from PyQt4 import QtGui, QtCore +from PyQt4 import QtGui, QtCore, QtNetwork from PyQt4.QtCore import QVariant + import os from ..plugin import Plugin -from ..misc import APPNAME, expand_tags, generate_metadata_path +from .. import misc -# FETCH MODES -AC_NO_FETCH = 0 -AC_FETCH_LOCAL_DIR = 1 -AC_FETCH_AMAZON = 2 -class wgAlbumCover(QtGui.QLabel): +class AlbumCoverWidget(QtGui.QLabel): "cover - QPixmap or None" cover = None "is there a (non-default) cover loaded?" @@ -38,8 +34,6 @@ class wgAlbumCover(QtGui.QLabel): "logger" logger = None - _cover_dirname = None # Directory and full filepath where cover - _cover_filepath = None # for current song should be stored. _menu = None # popup menu def __init__(self, plugin): @@ -51,41 +45,19 @@ class wgAlbumCover(QtGui.QLabel): # popup menu self._menu = QtGui.QMenu("album") refresh = self._menu.addAction('&Refresh cover.') - refresh.setObjectName('refresh') - select_file_action = self._menu.addAction('&Select cover file...') - select_file_action.setObjectName('select_file_action') - fetch_amazon_action = self._menu.addAction('Fetch from &Amazon.') - fetch_amazon_action.setObjectName('fetch_amazon_action') view_action = self._menu.addAction('&View in a separate window.') save_action = self._menu.addAction('Save cover &as...') - self.connect(refresh, QtCore.SIGNAL('triggered()'), self.refresh) - self.connect(select_file_action, QtCore.SIGNAL('triggered()'), self._fetch_local_manual) - self.connect(fetch_amazon_action, QtCore.SIGNAL('triggered()'), self.fetch_amazon) - self.connect(view_action, QtCore.SIGNAL('triggered()'), self._view_cover) - self.connect(save_action, QtCore.SIGNAL('triggered()'), self._save_cover) - - # MPD events - self.connect(self.plugin.mpclient, QtCore.SIGNAL('song_changed'), self.refresh) - self.connect(self.plugin.mpclient, QtCore.SIGNAL('disconnected'), self.refresh) - self.connect(self.plugin.mpclient, QtCore.SIGNAL('state_changed'),self.refresh) - - self.connect(self, QtCore.SIGNAL('new_cover_fetched'), self.set_cover) + self.connect(refresh, QtCore.SIGNAL('triggered()'), self.plugin.refresh) + self.connect(view_action, QtCore.SIGNAL('triggered()'), self.__view_cover) + self.connect(save_action, QtCore.SIGNAL('triggered()'), self.__save_cover) def contextMenuEvent(self, event): event.accept() self._menu.popup(event.globalPos()) - def refresh(self): - self._fetch_cover(self._fetch_auto) - - def fetch_amazon(self): - self._fetch_cover(self._fetch_amazon_manual) - - def set_cover(self, song, cover, write = False): - """Set cover for current song, attempt to write it to a file - if write is True and it's globally allowed.""" - + def set_cover(self, song, cover): + """Set cover for current song.""" self.logger.info('Setting cover') if not cover or cover.isNull(): self.cover = None @@ -97,135 +69,13 @@ class wgAlbumCover(QtGui.QLabel): if song != self.plugin.mpclient.current_song(): return - self.cover = QtGui.QPixmap.fromImage(cover) + self.cover = cover self.cover_loaded = True self.setPixmap(self.cover.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) self.plugin.emit(QtCore.SIGNAL('cover_changed'), self.cover) self.logger.info('Cover set.') - if (write and self.plugin.settings.value(self.plugin.name + '/store').toBool() - and self._cover_filepath): - if self.cover.save(self._cover_filepath, 'png'): - self.logger.info('Cover saved.') - else: - self.logger.error('Error saving cover.') - - class FetchThread(QtCore.QThread): - def __init__(self, parent, fetch_func, song): - QtCore.QThread.__init__(self) - self.setParent(parent) - self.fetch_func = fetch_func - self.song = song - - def run(self): - cover, write = self.fetch_func(self.song) - self.parent().emit(QtCore.SIGNAL('new_cover_fetched'), self.song, cover, write) - - def _fetch_cover(self, fetch_func): - song = self.plugin.mpclient.current_song() - if not song: - return self.emit(QtCore.SIGNAL('new_cover_fetched'), None, None) - - thread = self.FetchThread(self, fetch_func, song) - thread.start() - - def _fetch_auto(self, song): - """Autofetch cover for currently playing song.""" - self.logger.info("autorefreshing cover") - - # generate filenames - (self._cover_dirname, self._cover_filepath) = generate_metadata_path(song, self.plugin.settings.value(self.plugin.name + '/coverdir').toString(), - self.plugin.settings.value(self.plugin.name + '/covername').toString()) - - write = False - if not QtCore.QFile.exists(self._cover_filepath): - for i in (0, 1): - src = self.plugin.settings.value(self.plugin.name + '/method%i'%i).toInt()[0] - if src == AC_FETCH_LOCAL_DIR and self._cover_dirname: - cover = self._fetch_local(song) - elif src == AC_FETCH_AMAZON: - cover = self._fetch_amazon(song) - else: - cover = QtGui.QImage() - - if cover and not cover.isNull(): - write = True - break - else: - cover = QtGui.QImage(self._cover_filepath) - - return cover, write - - def _fetch_local_manual(self): - song = self.plugin.mpclient.current_song() - if not song: - return self.emit(QtCore.SIGNAL('new_cover_fetched'), None, None) - - file = QtGui.QFileDialog.getOpenFileName(self, - 'Select album cover for %s - %s'%(song.artist(), song.album()), - self._cover_dirname, '') - cover = QtGui.QImage(file) - if cover.isNull(): - return None, False - self.emit(QtCore.SIGNAL('new_cover_fetched'), song, cover, True) - - def _fetch_local(self, song): - self.logger.info('Trying to guess local cover name.') - # guess cover name - covers = ['cover', 'album', 'front'] - - exts = [] - for ext in QtGui.QImageReader().supportedImageFormats(): - exts.append('*.%s'%str(ext)) - - filter = [] - for cover in covers: - for ext in exts: - filter.append('*.%s%s'%(cover,ext)) - - dir = QtCore.QDir(self._cover_dirname) - if not dir: - self.logger.error('Error opening directory' + self._cover_dirname) - return None - - dir.setNameFilters(filter) - files = dir.entryList() - if files: - cover = QtGui.QImage(dir.filePath(files[0])) - if not cover.isNull(): - self.logger.info('Found a cover.') - return cover - - # if this failed, try any supported image - dir.setNameFilters(exts) - files = dir.entryList() - if files: - return QtGui.QImage(dir.filePath(files[0])) - self.logger.info('No matching cover found') - return None - - def _fetch_amazon_manual(self, song): - cover = self._fetch_amazon(song) - if not cover: - return None, False - return cover, True - - def _fetch_amazon(self, song): - if not song.artist() or not song.album(): - return None - # get the url from amazon WS - coverURL = AmazonAlbumImage(song.artist(), song.album()).fetch() - self.logger.info('Fetching cover from Amazon') - if not coverURL: - self.logger.info('Cover not found on Amazon') - return None - - img = urllib.urlopen(coverURL) - cover = QtGui.QImage() - cover.loadFromData(img.read()) - return cover - - def _view_cover(self): + def __view_cover(self): if not self.cover_loaded: return win = QtGui.QLabel(self, QtCore.Qt.Window) @@ -233,60 +83,66 @@ class wgAlbumCover(QtGui.QLabel): win.setPixmap(self.cover) win.show() - def _save_cover(self): + def __save_cover(self): if not self.cover_loaded: return - cover = self.cover - file = QtGui.QFileDialog.getSaveFileName(None, '', os.path.expanduser('~')) + file = QtGui.QFileDialog.getSaveFileName(None, '', QtCore.QDir.homePath()) if file: - if not cover.save(file): - self.logger.error('Saving cover failed.') + self.plugin.save_cover_file(cover, file) class AlbumCover(Plugin): + # public, read-only o = None - DEFAULTS = {'coverdir' : '$musicdir/$songdir', 'covername' : '.cover_nephilim_$artist_$album', - 'method0' : 1, 'method1' : 1, 'store' : True} - - def _load(self): - self.o = wgAlbumCover(self) - def _unload(self): - self.o = None - def info(self): - return "Display the album cover of the currently playing album." - - def refresh(self): - self.o.refresh() if self.o else self.logger.warning('Attemped to refresh when not loaded.') - - def cover(self): - if not self.o: - return None - return self.o.cover if self.o.cover_loaded else None - def _get_dock_widget(self): - return self._create_dock(self.o) + # private + DEFAULTS = {'coverdir' : '$musicdir/$songdir', 'covername' : '.cover_nephilim_$artist_$album', + 'fetchers': QtCore.QStringList(['local', 'Last.fm']), 'store' : True} + "implemented fetchers" + available_fetchers = None + "enabled fetchers, those with higher priority first" + __fetchers = None + "number of returned results from last refresh() call" + __results = None + "index/priority of current cover" + __index = None + "metadata paths" + __cover_dir = None + __cover_path = None + + #### private #### + def __init__(self, parent, mpclient, name): + Plugin.__init__(self, parent, mpclient, name) + + self.__fetchers = [] + self.available_fetchers = [self.FetcherLocal, self.FetcherLastfm] + + def __new_cover_fetched(self, song, cover): + self.logger.info('Got new cover.') + self.__results += 1 + + i = self.__fetchers.index(self.sender()) + if cover and i < self.__index: + if self.settings.value(self.name + '/store').toBool(): + self.save_cover_file(cover) + self.__index = i + return self.o.set_cover(song, cover) + elif self.__results >= len(self.__fetchers) and not self.o.cover_loaded: + self.o.set_cover(song, None) class SettingsWidgetAlbumCover(Plugin.SettingsWidget): - methods = [] - coverdir = None - covername = None - store = None + coverdir = None + covername = None + store = None + fetcherlist = None def __init__(self, plugin): Plugin.SettingsWidget.__init__(self, plugin) self.settings.beginGroup(self.plugin.name) - # fetching methods comboboxes - self.methods = [QtGui.QComboBox(), QtGui.QComboBox()] - for i,method in enumerate(self.methods): - method.addItem('No method.') - method.addItem('Local dir') - method.addItem('Amazon') - method.setCurrentIndex(self.settings.value('method' + str(i)).toInt()[0]) - # store covers groupbox self.store = QtGui.QGroupBox('Store covers.') - self.store.setToolTip('Should %s store its own copy of covers?'%APPNAME) + self.store.setToolTip('Should %s store its own copy of covers?'%misc.APPNAME) self.store.setCheckable(True) self.store.setChecked(self.settings.value('store').toBool()) self.store.setLayout(QtGui.QGridLayout()) @@ -297,88 +153,262 @@ class AlbumCover(Plugin): '$musicdir will be expanded to path to MPD music library (as set by user)\n' '$songdir will be expanded to path to the song (relative to $musicdir\n' 'other tags same as in covername' - %APPNAME) + %misc.APPNAME) self.covername = QtGui.QLineEdit(self.settings.value('covername').toString()) self.covername.setToolTip('Filename for %s cover files.\n' 'All tags supported by MPD will be expanded to their\n' 'values for current song, e.g. $title, $track, $artist,\n' - '$album, $genre etc.'%APPNAME) + '$album, $genre etc.'%misc.APPNAME) self.store.layout().addWidget(QtGui.QLabel('Cover directory'), 0, 0) self.store.layout().addWidget(self.coverdir, 0, 1) self.store.layout().addWidget(QtGui.QLabel('Cover filename'), 1, 0) self.store.layout().addWidget(self.covername, 1, 1) + # sites list + fetchers = self.settings.value('fetchers').toStringList() + self.fetcherlist = QtGui.QListWidget(self) + self.fetcherlist.setDragDropMode(QtGui.QAbstractItemView.InternalMove) + for site in fetchers: + it = QtGui.QListWidgetItem(site) + it.setCheckState(QtCore.Qt.Checked) + self.fetcherlist.addItem(it) + for site in self.plugin.available_fetchers: + if not site.name in fetchers: + it = QtGui.QListWidgetItem(site.name) + it.setCheckState(QtCore.Qt.Unchecked) + self.fetcherlist.addItem(it) + self.setLayout(QtGui.QVBoxLayout()) - self._add_widget(self.methods[0], 'Method 0', 'Method to try first.') - self._add_widget(self.methods[1], 'Method 1', 'Method to try if the first one fails.') self.layout().addWidget(self.store) + self._add_widget(self.fetcherlist, label = 'Fetchers', tooltip = 'A list of sources used for fetching covers.\n' + 'Use drag and drop to change their priority.') self.settings.endGroup() def save_settings(self): self.settings.beginGroup(self.plugin.name) - self.settings.setValue('method0', QVariant(self.methods[0].currentIndex())) - self.settings.setValue('method1', QVariant(self.methods[1].currentIndex())) self.settings.setValue('coverdir', QVariant(self.coverdir.text())) self.settings.setValue('covername', QVariant(self.covername.text())) self.settings.setValue('store', QVariant(self.store.isChecked())) + + fetchers = QtCore.QStringList() + for i in range(self.fetcherlist.count()): + it = self.fetcherlist.item(i) + if it.checkState() == QtCore.Qt.Checked: + fetchers.append(it.text()) + self.settings.setValue('fetchers', QVariant(fetchers)) self.settings.endGroup() - self.plugin.o.refresh() + self.plugin.refresh() + + class Fetcher(QtCore.QObject): + """A basic class for cover fetchers. Provides a fetch(song) function, + emits a finished(song, cover) signal when done; cover is either a QPixmap + or None if not found.""" + #public, read-only + logger = None + name = '' + + #private + nam = None # NetworkAccessManager + srep = None # search results NetworkReply + crep = None # cover page NetworkReply + song = None # current song + + #### private #### + def __init__(self, plugin): + QtCore.QObject.__init__(self, plugin) + + self.nam = QtNetwork.QNetworkAccessManager() + self.logger = plugin.logger + + def fetch2(self, song, url): + """A private convenience function to initiate fetch process.""" + # abort any existing connections + if self.srep: + self.srep.abort() + self.srep = None + if self.crep: + self.crep.abort() + self.crep = None + self.song = song + + self.logger.info('Searching %s: %s.'%(self. name, url)) + self.srep = self.nam.get(QtNetwork.QNetworkRequest(url)) + + def finish(self, cover = None): + """A private convenience function to clean up and emit finished(). + Feel free to reimplement/not use it.""" + self.srep = None + self.crep = None + self.emit(QtCore.SIGNAL('finished'), self.song, cover) + self.song = None + + #### public #### + def fetch(self, song): + """Reimplement this in subclasses.""" + pass + + class FetcherLastfm(Fetcher): + name = 'Last.fm' + + def fetch(self, song): + url = QtCore.QUrl('http://ws.audioscrobbler.com/2.0/') + url.setQueryItems([('api_key', 'c325945c67b3e8327e01e3afb7cdcf35'), + ('method', 'album.getInfo'), + ('artist', song.artist()), + ('album', song.album())]) + self.fetch2(song, url) + self.connect(self.srep, QtCore.SIGNAL('finished()'), self.__handle_search_res) + + def __handle_search_res(self): + url = None + xml = QtCore.QXmlStreamReader(self.srep) + + while not xml.atEnd(): + token = xml.readNext() + if token == QtCore.QXmlStreamReader.StartElement: + if xml.name() == 'image' and xml.attributes().value('size') == 'extralarge': + url = QtCore.QUrl() # the url is already percent-encoded + url.setEncodedUrl(xml.readElementText().toLatin1()) + if xml.hasError(): + self.logger.error('Error parsing seach results: %s'%xml.errorString()) + + if not url: + self.logger.info('Didn\'t find the URL in %s search results.'%self.name) + return self.finish() + self.logger.info('Found %s song URL: %s.'%(self.name, url)) + + self.crep = self.nam.get(QtNetwork.QNetworkRequest(url)) + self.connect(self.crep, QtCore.SIGNAL('finished()'), self.__handle_cover) + + def __handle_cover(self): + data = self.crep.readAll() + pixmap = QtGui.QPixmap() + if pixmap.loadFromData(data): + self.finish(pixmap) + self.finish() + + class FetcherLocal(QtCore.QObject): + """This fetcher tries to find cover files in the same directory as + current song.""" + name = 'local' + logger = None + settings = None - def get_settings_widget(self): - return self.SettingsWidgetAlbumCover(self) + def __init__(self, plugin): + QtCore.QObject.__init__(self, plugin) + self.logger = plugin.logger + self.settings = QtCore.QSettings() + + def fetch(self, song): + self.logger.info('Trying to guess local cover name.') + # guess cover name + covers = ['cover', 'album', 'front'] + + exts = [] + for ext in QtGui.QImageReader().supportedImageFormats(): + exts.append('*.%s'%str(ext)) + + filter = [] + for cover in covers: + for ext in exts: + filter.append('*.%s%s'%(cover,ext)) + + dir = QtCore.QDir('%s/%s'%(self.settings.value('MPD/music_dir').toString(), + os.path.dirname(song.filepath()))) + if not dir: + self.logger.error('Error opening directory' + self.__cover_dir) + return self.emit(QtCore.SIGNAL('finished'), song, None) + + dir.setNameFilters(filter) + files = dir.entryList() + if files: + cover = QtGui.QPixmap(dir.filePath(files[0])) + if not cover.isNull(): + self.logger.info('Found a cover: %s'%dir.filePath(files[0])) + return self.emit(QtCore.SIGNAL('finished'), song, cover) + + # if this failed, try any supported image + dir.setNameFilters(exts) + files = dir.entryList() + if files: + cover = QtGui.QPixmap(dir.filePath(files[0])) + if not cover.isNull(): + self.logger.info('Found a cover: %s'%dir.filePath(files[0])) + return self.emit(QtCore.SIGNAL('finished'), song, cover) + self.logger.info('No matching cover found') + self.emit(QtCore.SIGNAL('finished'), song, None) + + #### public #### + def _load(self): + self.o = AlbumCoverWidget(self) + self.connect(self.mpclient, QtCore.SIGNAL('song_changed'), self.refresh) + self.connect(self.mpclient, QtCore.SIGNAL('disconnected'), self.refresh) + self.refresh_fetchers() + def _unload(self): + self.o = None + self.disconnect(self.mpclient, QtCore.SIGNAL('song_changed'), self.refresh) + self.disconnect(self.mpclient, QtCore.SIGNAL('disconnected'), self.refresh) + self.disconnect(self.mpclient, QtCore.SIGNAL('state_changed'),self.refresh) + def info(self): + return "Display the album cover of the currently playing album." + + def refresh(self): + self.logger.info('Autorefreshing cover.') + self.__results = 0 + self.__index = len(self.__fetchers) + self.o.cover_loaded = False + song = self.mpclient.current_song() + if not song: + self.__cover_dir = '' + self.__cover_path = '' + return self.o.set_cover(None, None) + (self.__cover_dir, self.__cover_path) = misc.generate_metadata_path(song, + self.settings.value(self.name + '/coverdir').toString(), + self.settings.value(self.name + '/covername').toString()) + try: + self.logger.info('Trying to read cover from file %s.'%self.__cover_path) + cover = QtGui.QPixmap(self.__cover_path) + if not cover.isNull(): + return self.o.set_cover(song, cover) + except IOError, e: + self.logger.info('Error reading cover file: %s.'%e) + + for fetcher in self.__fetchers: + fetcher.fetch(song) + + def refresh_fetchers(self): + """Refresh the list of available fetchers.""" + self.__fetchers = [] + # append fetchers in order they are stored in settings + for name in self.settings.value('%s/fetchers'%self.name).toStringList(): + for site in self.available_fetchers: + if site.name == name: + self.__fetchers.append(site(self)) + self.connect(self.__fetchers[-1], QtCore.SIGNAL('finished'), self.__new_cover_fetched) + + def save_cover_file(self, cover, path = None): + """Save cover to a file specified in path. + If path is None, then a default value is used.""" + self.logger.info('Saving cover...') + try: + if not path: + path = self.__cover_path + cover.save(path, 'png') + self.logger.info('Cover successfully saved.') + except IOError, e: + self.logger.error('Error writing cover: %s', e) + + def cover(self): + if not self.o: + return None + return self.o.cover if self.o.cover_loaded else None -# This is the amazon cover fetcher using their webservice api -# Thank you, http://www.semicomplete.com/scripts/albumcover.py -import re -import urllib + def _get_dock_widget(self): + return self._create_dock(self.o) -AMAZON_AWS_ID = "0K4RZZKHSB5N2XYJWF02" + def get_settings_widget(self): + return self.SettingsWidgetAlbumCover(self) -class AmazonAlbumImage(object): - awsurl = 'http://ecs.amazonaws.com/onca/xml' - def __init__(self, artist, album): - self.artist = artist - self.album = album - def fetch(self): - url = self._GetResultURL(self._SearchAmazon()) - if not url: - return None - img_re = re.compile(r'''registerImage\("original_image", "([^"]+)"''') - try: - prod_data = urllib.urlopen(url).read() - except: - self.logger.warning('timeout opening %s'%(url)) - return None - m = img_re.search(prod_data) - if not m: - return None - img_url = m.group(1) - return img_url - - def _SearchAmazon(self): - data = { - 'Service' : 'AWSECommerceService', - 'Version' : '2005-03-23', - 'Operation' : 'ItemSearch', - 'ContentType' : 'text/xml', - 'SubscriptionId': AMAZON_AWS_ID, - 'SearchIndex' : 'Music', - 'ResponseGroup' : 'Small', - } - - data['Artist'] = self.artist.encode('utf-8') - data['Keywords'] = self.album.encode('utf-8') - - fd = urllib.urlopen('%s?%s' % (self.awsurl, urllib.urlencode(data))) - return fd.read() - - - def _GetResultURL(self, xmldata): - if not xmldata: - return None - url_re = re.compile(r'<DetailPageURL>([^<]+)</DetailPageURL>') - m = url_re.search(xmldata) - return m and m.group(1) |