# # Copyright (C) 2009 Anton Khirnov # # Nephilim 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 3 of the License, or # (at your option) any later version. # # Nephilim 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Nephilim. If not, see . # from PyQt5 import QtGui, QtWidgets, QtCore, QtNetwork from PyQt5.QtCore import pyqtSignal as Signal import os from ..plugin import Plugin from .. import common, metadata_fetcher, song from .. import icons class AlbumCoverWidget(QtWidgets.QLabel): "cover - QPixmap or None" cover = None "is there a (non-default) cover loaded?" cover_loaded = False "plugin object" plugin = None "logger" logger = None _menu = None # popup menu def __init__(self, plugin): QtWidgets.QLabel.__init__(self) self.plugin = plugin self.logger = plugin.logger self.setAlignment(QtCore.Qt.AlignCenter) # popup menu self._menu = QtWidgets.QMenu('album') self._menu.addAction('&Select cover file...', self.plugin.select_cover) self._menu.addAction('&Refresh cover.', self.plugin.refresh) self._menu.addAction('&View in a separate window.', self.__view_cover) self._menu.addAction('Save cover &as...', self.__save_cover) self._menu.addAction('&Clear cover.', self.__clear_cover) def contextMenuEvent(self, event): event.accept() self._menu.popup(event.globalPos()) 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 self.cover_loaded = False self.setPixmap(QtGui.QPixmap(':icons/nephilim.png')) self.plugin.cover_changed.emit(QtGui.QPixmap()) return if song != self.plugin.mpclient.cur_song: return self.cover = cover self.cover_loaded = True self.setPixmap(self.cover.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) self.plugin.cover_changed.emit(self.cover) self.logger.info('Cover set.') def __view_cover(self): if not self.cover_loaded: return win = QtWidgets.QLabel(self, QtCore.Qt.Window) win.setScaledContents(True) win.setPixmap(self.cover) win.show() def __save_cover(self): if not self.cover_loaded: return cover = self.cover file = QtWidgets.QFileDialog.getSaveFileName(None, '', QtCore.QDir.homePath()) if file: self.plugin.save_cover_file(cover, file) def __clear_cover(self): self.plugin.delete_cover_file() self.set_cover(None, None) self.plugin.refresh() class AlbumCover(Plugin): # public, constant info = 'Display the album cover of the currently playing album.' # public, read-only o = None # private DEFAULTS = {'coverdir' : '${musicdir}/${songdir}', 'covername' : '.cover_%s_${artist}_${album}'%common.APPNAME, 'fetchers': ['local', 'Last.fm'], 'store' : 1} "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 # SIGNALS cover_changed = Signal(QtGui.QPixmap) #### private #### def __init__(self, parent, mpclient, name): Plugin.__init__(self, parent, mpclient, name) self.__fetchers = [] self.available_fetchers = [FetcherLocal, 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 int(self.settings.value(self.name + '/store')): 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) def __abort_fetch(self): """Aborts all fetches currently in progress.""" for fetcher in self.__fetchers: fetcher.abort() class SettingsWidgetAlbumCover(Plugin.SettingsWidget): coverdir = None covername = None store = None fetcherlist = None def __init__(self, plugin): Plugin.SettingsWidget.__init__(self, plugin) self.settings.beginGroup(self.plugin.name) # store covers groupbox self.store = QtWidgets.QGroupBox('Store covers.') self.store.setToolTip('Should %s store its own copy of covers?'%common.APPNAME) self.store.setCheckable(True) self.store.setChecked(int(self.settings.value('store'))) self.store.setLayout(QtWidgets.QGridLayout()) # paths to covers self.coverdir = QtWidgets.QLineEdit(self.settings.value('coverdir')) self.coverdir.setToolTip('Where should %s store covers.\n' '${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' %common.APPNAME) self.covername = QtWidgets.QLineEdit(self.settings.value('covername')) 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.'%common.APPNAME) self.store.layout().addWidget(QtWidgets.QLabel('Cover directory'), 0, 0) self.store.layout().addWidget(self.coverdir, 0, 1) self.store.layout().addWidget(QtWidgets.QLabel('Cover filename'), 1, 0) self.store.layout().addWidget(self.covername, 1, 1) # sites list fetchers = self.settings.value('fetchers') self.fetcherlist = QtWidgets.QListWidget(self) self.fetcherlist.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) for site in fetchers: it = QtWidgets.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 = QtWidgets.QListWidgetItem(site.name) it.setCheckState(QtCore.Qt.Unchecked) self.fetcherlist.addItem(it) self.setLayout(QtWidgets.QVBoxLayout()) 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('coverdir', self.coverdir.text()) self.settings.setValue('covername', self.covername.text()) self.settings.setValue('store', int(self.store.isChecked())) fetchers = [] 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', fetchers) self.settings.endGroup() self.plugin.refresh_fetchers() self.plugin.refresh() #### public #### def _load(self): self.o = AlbumCoverWidget(self) self.mpclient.song_changed.connect(self.refresh) self.refresh_fetchers() self.refresh() def _unload(self): self.o = None self.mpclient.song_changed.disconnect(self.refresh) def refresh(self): self.logger.info('Autorefreshing cover.') self.__results = 0 self.__index = len(self.__fetchers) self.o.cover_loaded = False song = self.mpclient.cur_song if not song: self.__cover_dir = '' self.__cover_path = '' return self.o.set_cover(None, None) (self.__cover_dir, self.__cover_path) = common.generate_metadata_path(song, self.settings.value(self.name + '/coverdir'), self.settings.value(self.name + '/covername')) 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 as 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): for site in self.available_fetchers: if site.name == name: self.__fetchers.append(site(self)) self.__fetchers[-1].finished.connect(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 as e: self.logger.error('Error writing cover: %s', e) def delete_cover_file(self, song = None): """Delete a cover file for song. If song is not specified current song is used.""" if not song: path = self.__cover_path else: path = common.generate_metadata_path(song, self.settings.value(self.name + '/coverdir'), self.settings.value(self.name + '/covername')) if not QtCore.QFile.remove(path): self.logger.error('Error removing file %s.'%path) def select_cover(self): """Prompt user to manually select cover file for current song.""" song = self.mpclient.cur_song if not song: return self.__abort_fetch() f, _ = QtWidgets.QFileDialog.getOpenFileName(None, 'Select album cover for %s - %s'%(song['?artist'], song['?album']), self.__cover_dir, '') if not f: return cover = QtGui.QPixmap(f) if cover.isNull(): self.logger.error('Error opening cover file.') return if int(self.settings.value(self.name + '/store')): self.save_cover_file(cover) self.o.set_cover(song, cover) 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) def get_settings_widget(self): return self.SettingsWidgetAlbumCover(self) class FetcherLastfm(metadata_fetcher.MetadataFetcher): name = 'Last.fm' def fetch(self, song): self.song = song if not 'artist' in song or not 'album' in song: return self.finish() query = QtCore.QUrlQuery() query.setQueryItems([('api_key', 'beedb2a8a0178b8059cd6c7e57fbe428'), ('method', 'album.getInfo'), ('artist', song['artist']), ('album', song['album']), ('mbid', song['?MUSICBRAINZ_ALBUMID'])]) url = QtCore.QUrl('http://ws.audioscrobbler.com/2.0/') url.setQuery(query) self.fetch2(song, url) self.rep.finished.connect(self.__handle_search_res) def __handle_search_res(self): url = None xml = QtCore.QXmlStreamReader(self.rep) 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 try: url.setUrl(xml.readElementText()) except TypeError: #no text url = None 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.rep = self.nam.get(QtNetwork.QNetworkRequest(url)) self.rep.finished.connect(self.__handle_cover) def __handle_cover(self): data = self.rep.readAll() pixmap = QtGui.QPixmap() if pixmap.loadFromData(data): self.finish(pixmap) else: self.finish() class FetcherLocal(QtCore.QObject): """This fetcher tries to find cover files in the same directory as current song.""" #public, read-only name = 'local' logger = None settings = None # SIGNALS finished = Signal([song.Song, object]) 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)) dirname, filename = common.generate_metadata_path(song, '$musicdir/$songdir', '') dir = QtCore.QDir(dirname) if not dir: self.logger.error('Error opening directory %s.'%dirname) return self.finished.emit(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.finished.emit(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.finished.emit(song, cover) self.logger.info('No matching cover found') self.finished.emit(song, None) def abort(self): pass