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
23 from ..plugin
import Plugin
28 class AlbumCoverWidget(QtGui
.QLabel
):
29 "cover - QPixmap or None"
31 "is there a (non-default) cover loaded?"
38 _menu
= None # popup menu
40 def __init__(self
, plugin
):
41 QtGui
.QLabel
.__init
__(self
)
43 self
.logger
= plugin
.logger
44 self
.setAlignment(QtCore
.Qt
.AlignCenter
)
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
):
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():
63 self
.cover_loaded
= False
64 self
.setPixmap(QtGui
.QPixmap(':icons/nephilim.png'))
65 self
.plugin
.cover_changed
.emit(QtGui
.QPixmap())
68 if song
!= self
.plugin
.mpclient
.current_song():
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
:
80 win
= QtGui
.QLabel(self
, QtCore
.Qt
.Window
)
81 win
.setScaledContents(True)
82 win
.setPixmap(self
.cover
)
85 def __save_cover(self
):
86 if not self
.cover_loaded
:
89 file = QtGui
.QFileDialog
.getSaveFileName(None, '', QtCore
.QDir
.homePath())
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)
98 class AlbumCover(Plugin
):
100 info
= 'Display the album cover of the currently playing album.'
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"
112 "number of returned results from last refresh() call"
114 "index/priority of current cover"
121 cover_changed
= QtCore
.pyqtSignal(QtGui
.QPixmap
)
124 def __init__(self
, parent
, mpclient
, name
):
125 Plugin
.__init
__(self
, parent
, mpclient
, name
)
128 self
.available_fetchers
= [self
.FetcherLocal
, self
.FetcherLastfm
]
130 def __new_cover_fetched(self
, song
, cover
):
131 self
.logger
.info('Got new cover.')
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
)
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
:
148 class SettingsWidgetAlbumCover(Plugin
.SettingsWidget
):
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())
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'
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)
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()))
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
):
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
):
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
241 url
.setEncodedUrl(xml
.readElementText())
242 except TypeError: #no text
245 self
.logger
.error('Error parsing seach results: %s'%xml
.errorString())
248 self
.logger
.info('Didn\'t find the URL in %s search results.'%self
.name
)
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
):
262 class FetcherLocal(QtCore
.QObject
):
263 """This fetcher tries to find cover files in the same directory as
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.')
281 covers
= ['cover', 'album', 'front']
284 for ext
in QtGui
.QImageReader().supportedImageFormats():
285 exts
.append('*.%s'%str
(ext
))
290 filter.append('*.%s%s'%(cover
,ext
))
292 dirname
, filename
= common
.generate_metadata_path(song
, '$musicdir/$songdir', '')
293 dir = QtCore
.QDir(dirname
)
295 self
.logger
.error('Error opening directory %s.'%dirname
)
296 return self
.finished
.emit(song
, None)
298 dir.setNameFilters(filter)
299 files
= dir.entryList()
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()
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)
322 self
.o
= AlbumCoverWidget(self
)
323 self
.mpclient
.song_changed
.connect(self
.refresh
)
324 self
.refresh_fetchers()
328 self
.mpclient
.song_changed
.disconnect(self
.refresh
)
331 self
.logger
.info('Autorefreshing cover.')
333 self
.__index
= len(self
.__fetchers
)
334 self
.o
.cover_loaded
= False
335 song
= self
.mpclient
.current_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())
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
)
350 self
.logger
.info('Error reading cover file: %s.'%e)
352 for fetcher
in self
.__fetchers
:
355 def refresh_fetchers(self
):
356 """Refresh the list of available 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...')
371 path
= self
.__cover
_path
372 cover
.save(path
, 'png')
373 self
.logger
.info('Cover successfully saved.')
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."""
381 path
= self
.__cover
_path
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()
395 file = QtGui
.QFileDialog
.getOpenFileName(None,
396 'Select album cover for %s - %s'%(song
['artist'], song
['album']),
397 self
.__cover
_dir
, '')
401 cover
= QtGui
.QPixmap(file)
403 self
.logger
.error('Error opening cover file.')
406 if self
.settings
.value(self
.name
+ '/store').toBool():
407 self
.save_cover_file(cover
)
408 self
.o
.set_cover(song
, cover
)
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
)