switch to PyQt4 API v2 for QStrings
[nephilim.git] / nephilim / plugins / AlbumCover.py
blob7ccdfa2c75dbc5b5000b1a30cc63f044d8900e14
2 # Copyright (C) 2009 Anton Khirnov <wyskas@gmail.com>
4 # Nephilim is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # Nephilim is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Nephilim. If not, see <http://www.gnu.org/licenses/>.
18 from PyQt4 import QtGui, QtCore, QtNetwork
19 from PyQt4.QtCore import QVariant
21 import os
23 from ..plugin import Plugin
24 from .. import common
25 from .. import icons
28 class AlbumCoverWidget(QtGui.QLabel):
29 "cover - QPixmap or None"
30 cover = None
31 "is there a (non-default) cover loaded?"
32 cover_loaded = False
33 "plugin object"
34 plugin = None
35 "logger"
36 logger = None
38 _menu = None # popup menu
40 def __init__(self, plugin):
41 QtGui.QLabel.__init__(self)
42 self.plugin = plugin
43 self.logger = plugin.logger
44 self.setAlignment(QtCore.Qt.AlignCenter)
46 # popup menu
47 self._menu = QtGui.QMenu('album')
48 self._menu.addAction('&Select cover file...', self.plugin.select_cover)
49 self._menu.addAction('&Refresh cover.', self.plugin.refresh)
50 self._menu.addAction('&View in a separate window.', self.__view_cover)
51 self._menu.addAction('Save cover &as...', self.__save_cover)
52 self._menu.addAction('&Clear cover.', self.__clear_cover)
54 def contextMenuEvent(self, event):
55 event.accept()
56 self._menu.popup(event.globalPos())
58 def set_cover(self, song, cover):
59 """Set cover for current song."""
60 self.logger.info('Setting cover')
61 if not cover or cover.isNull():
62 self.cover = None
63 self.cover_loaded = False
64 self.setPixmap(QtGui.QPixmap(':icons/nephilim.png'))
65 self.plugin.cover_changed.emit(QtGui.QPixmap())
66 return
68 if song != self.plugin.mpclient.current_song():
69 return
71 self.cover = cover
72 self.cover_loaded = True
73 self.setPixmap(self.cover.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
74 self.plugin.cover_changed.emit(self.cover)
75 self.logger.info('Cover set.')
77 def __view_cover(self):
78 if not self.cover_loaded:
79 return
80 win = QtGui.QLabel(self, QtCore.Qt.Window)
81 win.setScaledContents(True)
82 win.setPixmap(self.cover)
83 win.show()
85 def __save_cover(self):
86 if not self.cover_loaded:
87 return
88 cover = self.cover
89 file = QtGui.QFileDialog.getSaveFileName(None, '', QtCore.QDir.homePath())
90 if file:
91 self.plugin.save_cover_file(cover, file)
93 def __clear_cover(self):
94 self.plugin.delete_cover_file()
95 self.set_cover(None, None)
96 self.plugin.refresh()
98 class AlbumCover(Plugin):
99 # public, constant
100 info = 'Display the album cover of the currently playing album.'
102 # public, read-only
103 o = None
105 # private
106 DEFAULTS = {'coverdir' : '$musicdir/$songdir', 'covername' : '.cover_nephilim_$artist_$album',
107 'fetchers': ['local', 'Last.fm'], 'store' : True}
108 "implemented fetchers"
109 available_fetchers = None
110 "enabled fetchers, those with higher priority first"
111 __fetchers = None
112 "number of returned results from last refresh() call"
113 __results = None
114 "index/priority of current cover"
115 __index = None
116 "metadata paths"
117 __cover_dir = None
118 __cover_path = None
120 # SIGNALS
121 cover_changed = QtCore.pyqtSignal(QtGui.QPixmap)
123 #### private ####
124 def __init__(self, parent, mpclient, name):
125 Plugin.__init__(self, parent, mpclient, name)
127 self.__fetchers = []
128 self.available_fetchers = [self.FetcherLocal, self.FetcherLastfm]
130 def __new_cover_fetched(self, song, cover):
131 self.logger.info('Got new cover.')
132 self.__results += 1
134 i = self.__fetchers.index(self.sender())
135 if cover and i < self.__index:
136 if self.settings.value(self.name + '/store').toBool():
137 self.save_cover_file(cover)
138 self.__index = i
139 return self.o.set_cover(song, cover)
140 elif self.__results >= len(self.__fetchers) and not self.o.cover_loaded:
141 self.o.set_cover(song, None)
143 def __abort_fetch(self):
144 """Aborts all fetches currently in progress."""
145 for fetcher in self.__fetchers:
146 fetcher.abort()
148 class SettingsWidgetAlbumCover(Plugin.SettingsWidget):
149 coverdir = None
150 covername = None
151 store = None
152 fetcherlist = None
154 def __init__(self, plugin):
155 Plugin.SettingsWidget.__init__(self, plugin)
156 self.settings.beginGroup(self.plugin.name)
158 # store covers groupbox
159 self.store = QtGui.QGroupBox('Store covers.')
160 self.store.setToolTip('Should %s store its own copy of covers?'%common.APPNAME)
161 self.store.setCheckable(True)
162 self.store.setChecked(self.settings.value('store').toBool())
163 self.store.setLayout(QtGui.QGridLayout())
165 # paths to covers
166 self.coverdir = QtGui.QLineEdit(self.settings.value('coverdir').toString())
167 self.coverdir.setToolTip('Where should %s store covers.\n'
168 '$musicdir will be expanded to path to MPD music library (as set by user)\n'
169 '$songdir will be expanded to path to the song (relative to $musicdir\n'
170 'other tags same as in covername'
171 %common.APPNAME)
172 self.covername = QtGui.QLineEdit(self.settings.value('covername').toString())
173 self.covername.setToolTip('Filename for %s cover files.\n'
174 'All tags supported by MPD will be expanded to their\n'
175 'values for current song, e.g. $title, $track, $artist,\n'
176 '$album, $genre etc.'%common.APPNAME)
177 self.store.layout().addWidget(QtGui.QLabel('Cover directory'), 0, 0)
178 self.store.layout().addWidget(self.coverdir, 0, 1)
179 self.store.layout().addWidget(QtGui.QLabel('Cover filename'), 1, 0)
180 self.store.layout().addWidget(self.covername, 1, 1)
182 # sites list
183 fetchers = self.settings.value('fetchers').toStringList()
184 self.fetcherlist = QtGui.QListWidget(self)
185 self.fetcherlist.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
186 for site in fetchers:
187 it = QtGui.QListWidgetItem(site)
188 it.setCheckState(QtCore.Qt.Checked)
189 self.fetcherlist.addItem(it)
190 for site in self.plugin.available_fetchers:
191 if not site.name in fetchers:
192 it = QtGui.QListWidgetItem(site.name)
193 it.setCheckState(QtCore.Qt.Unchecked)
194 self.fetcherlist.addItem(it)
196 self.setLayout(QtGui.QVBoxLayout())
197 self.layout().addWidget(self.store)
198 self._add_widget(self.fetcherlist, label = 'Fetchers', tooltip = 'A list of sources used for fetching covers.\n'
199 'Use drag and drop to change their priority.')
201 self.settings.endGroup()
203 def save_settings(self):
204 self.settings.beginGroup(self.plugin.name)
205 self.settings.setValue('coverdir', QVariant(self.coverdir.text()))
206 self.settings.setValue('covername', QVariant(self.covername.text()))
207 self.settings.setValue('store', QVariant(self.store.isChecked()))
209 fetchers = []
210 for i in range(self.fetcherlist.count()):
211 it = self.fetcherlist.item(i)
212 if it.checkState() == QtCore.Qt.Checked:
213 fetchers.append(it.text())
214 self.settings.setValue('fetchers', QVariant(fetchers))
215 self.settings.endGroup()
216 self.plugin.refresh()
218 class FetcherLastfm(common.MetadataFetcher):
219 name = 'Last.fm'
221 def fetch(self, song):
222 url = QtCore.QUrl('http://ws.audioscrobbler.com/2.0/')
223 url.setQueryItems([('api_key', 'c325945c67b3e8327e01e3afb7cdcf35'),
224 ('method', 'album.getInfo'),
225 ('artist', song['artist']),
226 ('album', song['album']),
227 ('mbid', song['MUSICBRAINZ_ALBUMID'])])
228 self.fetch2(song, url)
229 self.rep.finished.connect(self.__handle_search_res)
231 def __handle_search_res(self):
232 url = None
233 xml = QtCore.QXmlStreamReader(self.rep)
235 while not xml.atEnd():
236 token = xml.readNext()
237 if token == QtCore.QXmlStreamReader.StartElement:
238 if xml.name() == 'image' and xml.attributes().value('size') == 'extralarge':
239 url = QtCore.QUrl() # the url is already percent-encoded
240 try:
241 url.setEncodedUrl(xml.readElementText())
242 except TypeError: #no text
243 url = None
244 if xml.hasError():
245 self.logger.error('Error parsing seach results: %s'%xml.errorString())
247 if not url:
248 self.logger.info('Didn\'t find the URL in %s search results.'%self.name)
249 return self.finish()
250 self.logger.info('Found %s song URL: %s.'%(self.name, url))
252 self.rep = self.nam.get(QtNetwork.QNetworkRequest(url))
253 self.rep.finished.connect(self.__handle_cover)
255 def __handle_cover(self):
256 data = self.rep.readAll()
257 pixmap = QtGui.QPixmap()
258 if pixmap.loadFromData(data):
259 self.finish(pixmap)
260 self.finish()
262 class FetcherLocal(QtCore.QObject):
263 """This fetcher tries to find cover files in the same directory as
264 current song."""
265 #public, read-only
266 name = 'local'
267 logger = None
268 settings = None
270 # SIGNALS
271 finished = QtCore.pyqtSignal('song', 'metadata')
273 def __init__(self, plugin):
274 QtCore.QObject.__init__(self, plugin)
275 self.logger = plugin.logger
276 self.settings = QtCore.QSettings()
278 def fetch(self, song):
279 self.logger.info('Trying to guess local cover name.')
280 # guess cover name
281 covers = ['cover', 'album', 'front']
283 exts = []
284 for ext in QtGui.QImageReader().supportedImageFormats():
285 exts.append('*.%s'%str(ext))
287 filter = []
288 for cover in covers:
289 for ext in exts:
290 filter.append('*.%s%s'%(cover,ext))
292 dirname, filename = common.generate_metadata_path(song, '$musicdir/$songdir', '')
293 dir = QtCore.QDir(dirname)
294 if not dir:
295 self.logger.error('Error opening directory %s.'%dirname)
296 return self.finished.emit(song, None)
298 dir.setNameFilters(filter)
299 files = dir.entryList()
300 if files:
301 cover = QtGui.QPixmap(dir.filePath(files[0]))
302 if not cover.isNull():
303 self.logger.info('Found a cover: %s'%dir.filePath(files[0]))
304 return self.finished.emit(song, cover)
306 # if this failed, try any supported image
307 dir.setNameFilters(exts)
308 files = dir.entryList()
309 if files:
310 cover = QtGui.QPixmap(dir.filePath(files[0]))
311 if not cover.isNull():
312 self.logger.info('Found a cover: %s'%dir.filePath(files[0]))
313 return self.finished.emit(song, cover)
314 self.logger.info('No matching cover found')
315 self.finished.emit(song, None)
317 def abort(self):
318 pass
320 #### public ####
321 def _load(self):
322 self.o = AlbumCoverWidget(self)
323 self.mpclient.song_changed.connect(self.refresh)
324 self.refresh_fetchers()
325 self.refresh()
326 def _unload(self):
327 self.o = None
328 self.mpclient.song_changed.disconnect(self.refresh)
330 def refresh(self):
331 self.logger.info('Autorefreshing cover.')
332 self.__results = 0
333 self.__index = len(self.__fetchers)
334 self.o.cover_loaded = False
335 song = self.mpclient.current_song()
336 if not song:
337 self.__cover_dir = ''
338 self.__cover_path = ''
339 return self.o.set_cover(None, None)
341 (self.__cover_dir, self.__cover_path) = common.generate_metadata_path(song,
342 self.settings.value(self.name + '/coverdir').toString(),
343 self.settings.value(self.name + '/covername').toString())
344 try:
345 self.logger.info('Trying to read cover from file %s.'%self.__cover_path)
346 cover = QtGui.QPixmap(self.__cover_path)
347 if not cover.isNull():
348 return self.o.set_cover(song, cover)
349 except IOError, e:
350 self.logger.info('Error reading cover file: %s.'%e)
352 for fetcher in self.__fetchers:
353 fetcher.fetch(song)
355 def refresh_fetchers(self):
356 """Refresh the list of available fetchers."""
357 self.__fetchers = []
358 # append fetchers in order they are stored in settings
359 for name in self.settings.value('%s/fetchers'%self.name).toStringList():
360 for site in self.available_fetchers:
361 if site.name == name:
362 self.__fetchers.append(site(self))
363 self.__fetchers[-1].finished.connect(self.__new_cover_fetched)
365 def save_cover_file(self, cover, path = None):
366 """Save cover to a file specified in path.
367 If path is None, then a default value is used."""
368 self.logger.info('Saving cover...')
369 try:
370 if not path:
371 path = self.__cover_path
372 cover.save(path, 'png')
373 self.logger.info('Cover successfully saved.')
374 except IOError, e:
375 self.logger.error('Error writing cover: %s', e)
377 def delete_cover_file(self, song = None):
378 """Delete a cover file for song. If song is not specified
379 current song is used."""
380 if not song:
381 path = self.__cover_path
382 else:
383 path = common.generate_metadata_path(song, self.settings.value(self.name + '/coverdir').toString(),
384 self.settings.value(self.name + '/covername').toString())
385 if not QtCore.QFile.remove(path):
386 self.logger.error('Error removing file %s.'%path)
388 def select_cover(self):
389 """Prompt user to manually select cover file for current song."""
390 song = self.mpclient.current_song()
391 if not song:
392 return
393 self.__abort_fetch()
395 file = QtGui.QFileDialog.getOpenFileName(None,
396 'Select album cover for %s - %s'%(song['artist'], song['album']),
397 self.__cover_dir, '')
398 if not file:
399 return
401 cover = QtGui.QPixmap(file)
402 if cover.isNull():
403 self.logger.error('Error opening cover file.')
404 return
406 if self.settings.value(self.name + '/store').toBool():
407 self.save_cover_file(cover)
408 self.o.set_cover(song, cover)
410 def cover(self):
411 if not self.o:
412 return None
413 return self.o.cover if self.o.cover_loaded else None
415 def _get_dock_widget(self):
416 return self._create_dock(self.o)
418 def get_settings_widget(self):
419 return self.SettingsWidgetAlbumCover(self)