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
27 class AlbumCoverWidget(QtGui
.QLabel
):
28 "cover - QPixmap or None"
30 "is there a (non-default) cover loaded?"
37 _menu
= None # popup menu
39 def __init__(self
, plugin
):
40 QtGui
.QLabel
.__init
__(self
)
42 self
.logger
= plugin
.logger
43 self
.setAlignment(QtCore
.Qt
.AlignCenter
)
46 self
._menu
= QtGui
.QMenu('album')
47 self
._menu
.addAction('&Select cover file...', self
.plugin
.select_cover
)
48 self
._menu
.addAction('&Refresh cover.', self
.plugin
.refresh
)
49 self
._menu
.addAction('&View in a separate window.', self
.__view
_cover
)
50 self
._menu
.addAction('Save cover &as...', self
.__save
_cover
)
51 self
._menu
.addAction('&Clear cover.', self
.__clear
_cover
)
53 def contextMenuEvent(self
, event
):
55 self
._menu
.popup(event
.globalPos())
57 def set_cover(self
, song
, cover
):
58 """Set cover for current song."""
59 self
.logger
.info('Setting cover')
60 if not cover
or cover
.isNull():
62 self
.cover_loaded
= False
63 self
.setPixmap(QtGui
.QPixmap('gfx/no-cd-cover.png'))
64 self
.plugin
.cover_changed
.emit(QtGui
.QPixmap())
67 if song
!= self
.plugin
.mpclient
.current_song():
71 self
.cover_loaded
= True
72 self
.setPixmap(self
.cover
.scaled(self
.size(), QtCore
.Qt
.KeepAspectRatio
, QtCore
.Qt
.SmoothTransformation
))
73 self
.plugin
.cover_changed
.emit(self
.cover
)
74 self
.logger
.info('Cover set.')
76 def __view_cover(self
):
77 if not self
.cover_loaded
:
79 win
= QtGui
.QLabel(self
, QtCore
.Qt
.Window
)
80 win
.setScaledContents(True)
81 win
.setPixmap(self
.cover
)
84 def __save_cover(self
):
85 if not self
.cover_loaded
:
88 file = QtGui
.QFileDialog
.getSaveFileName(None, '', QtCore
.QDir
.homePath())
90 self
.plugin
.save_cover_file(cover
, file)
92 def __clear_cover(self
):
93 self
.plugin
.delete_cover_file()
94 self
.set_cover(None, None)
97 class AlbumCover(Plugin
):
99 info
= 'Display the album cover of the currently playing album.'
105 DEFAULTS
= {'coverdir' : '$musicdir/$songdir', 'covername' : '.cover_nephilim_$artist_$album',
106 'fetchers': QtCore
.QStringList(['local', 'Last.fm']), 'store' : True}
107 "implemented fetchers"
108 available_fetchers
= None
109 "enabled fetchers, those with higher priority first"
111 "number of returned results from last refresh() call"
113 "index/priority of current cover"
120 cover_changed
= QtCore
.pyqtSignal(QtGui
.QPixmap
)
123 def __init__(self
, parent
, mpclient
, name
):
124 Plugin
.__init
__(self
, parent
, mpclient
, name
)
127 self
.available_fetchers
= [self
.FetcherLocal
, self
.FetcherLastfm
]
129 def __new_cover_fetched(self
, song
, cover
):
130 self
.logger
.info('Got new cover.')
133 i
= self
.__fetchers
.index(self
.sender())
134 if cover
and i
< self
.__index
:
135 if self
.settings
.value(self
.name
+ '/store').toBool():
136 self
.save_cover_file(cover
)
138 return self
.o
.set_cover(song
, cover
)
139 elif self
.__results
>= len(self
.__fetchers
) and not self
.o
.cover_loaded
:
140 self
.o
.set_cover(song
, None)
142 def __abort_fetch(self
):
143 """Aborts all fetches currently in progress."""
144 for fetcher
in self
.__fetchers
:
147 class SettingsWidgetAlbumCover(Plugin
.SettingsWidget
):
153 def __init__(self
, plugin
):
154 Plugin
.SettingsWidget
.__init
__(self
, plugin
)
155 self
.settings
.beginGroup(self
.plugin
.name
)
157 # store covers groupbox
158 self
.store
= QtGui
.QGroupBox('Store covers.')
159 self
.store
.setToolTip('Should %s store its own copy of covers?'%common
.APPNAME
)
160 self
.store
.setCheckable(True)
161 self
.store
.setChecked(self
.settings
.value('store').toBool())
162 self
.store
.setLayout(QtGui
.QGridLayout())
165 self
.coverdir
= QtGui
.QLineEdit(self
.settings
.value('coverdir').toString())
166 self
.coverdir
.setToolTip('Where should %s store covers.\n'
167 '$musicdir will be expanded to path to MPD music library (as set by user)\n'
168 '$songdir will be expanded to path to the song (relative to $musicdir\n'
169 'other tags same as in covername'
171 self
.covername
= QtGui
.QLineEdit(self
.settings
.value('covername').toString())
172 self
.covername
.setToolTip('Filename for %s cover files.\n'
173 'All tags supported by MPD will be expanded to their\n'
174 'values for current song, e.g. $title, $track, $artist,\n'
175 '$album, $genre etc.'%common
.APPNAME
)
176 self
.store
.layout().addWidget(QtGui
.QLabel('Cover directory'), 0, 0)
177 self
.store
.layout().addWidget(self
.coverdir
, 0, 1)
178 self
.store
.layout().addWidget(QtGui
.QLabel('Cover filename'), 1, 0)
179 self
.store
.layout().addWidget(self
.covername
, 1, 1)
182 fetchers
= self
.settings
.value('fetchers').toStringList()
183 self
.fetcherlist
= QtGui
.QListWidget(self
)
184 self
.fetcherlist
.setDragDropMode(QtGui
.QAbstractItemView
.InternalMove
)
185 for site
in fetchers
:
186 it
= QtGui
.QListWidgetItem(site
)
187 it
.setCheckState(QtCore
.Qt
.Checked
)
188 self
.fetcherlist
.addItem(it
)
189 for site
in self
.plugin
.available_fetchers
:
190 if not site
.name
in fetchers
:
191 it
= QtGui
.QListWidgetItem(site
.name
)
192 it
.setCheckState(QtCore
.Qt
.Unchecked
)
193 self
.fetcherlist
.addItem(it
)
195 self
.setLayout(QtGui
.QVBoxLayout())
196 self
.layout().addWidget(self
.store
)
197 self
._add
_widget
(self
.fetcherlist
, label
= 'Fetchers', tooltip
= 'A list of sources used for fetching covers.\n'
198 'Use drag and drop to change their priority.')
200 self
.settings
.endGroup()
202 def save_settings(self
):
203 self
.settings
.beginGroup(self
.plugin
.name
)
204 self
.settings
.setValue('coverdir', QVariant(self
.coverdir
.text()))
205 self
.settings
.setValue('covername', QVariant(self
.covername
.text()))
206 self
.settings
.setValue('store', QVariant(self
.store
.isChecked()))
208 fetchers
= QtCore
.QStringList()
209 for i
in range(self
.fetcherlist
.count()):
210 it
= self
.fetcherlist
.item(i
)
211 if it
.checkState() == QtCore
.Qt
.Checked
:
212 fetchers
.append(it
.text())
213 self
.settings
.setValue('fetchers', QVariant(fetchers
))
214 self
.settings
.endGroup()
215 self
.plugin
.refresh()
217 class FetcherLastfm(common
.MetadataFetcher
):
220 def fetch(self
, song
):
221 url
= QtCore
.QUrl('http://ws.audioscrobbler.com/2.0/')
222 url
.setQueryItems([('api_key', 'c325945c67b3e8327e01e3afb7cdcf35'),
223 ('method', 'album.getInfo'),
224 ('artist', song
['artist']),
225 ('album', song
['album']),
226 ('mbid', song
['MUSICBRAINZ_ALBUMID'])])
227 self
.fetch2(song
, url
)
228 self
.rep
.finished
.connect(self
.__handle
_search
_res
)
230 def __handle_search_res(self
):
232 xml
= QtCore
.QXmlStreamReader(self
.rep
)
234 while not xml
.atEnd():
235 token
= xml
.readNext()
236 if token
== QtCore
.QXmlStreamReader
.StartElement
:
237 if xml
.name() == 'image' and xml
.attributes().value('size') == 'extralarge':
238 url
= QtCore
.QUrl() # the url is already percent-encoded
239 url
.setEncodedUrl(xml
.readElementText().toLatin1())
241 self
.logger
.error('Error parsing seach results: %s'%xml
.errorString())
244 self
.logger
.info('Didn\'t find the URL in %s search results.'%self
.name
)
246 self
.logger
.info('Found %s song URL: %s.'%(self
.name
, url
))
248 self
.rep
= self
.nam
.get(QtNetwork
.QNetworkRequest(url
))
249 self
.rep
.finished
.connect(self
.__handle
_cover
)
251 def __handle_cover(self
):
252 data
= self
.rep
.readAll()
253 pixmap
= QtGui
.QPixmap()
254 if pixmap
.loadFromData(data
):
258 class FetcherLocal(QtCore
.QObject
):
259 """This fetcher tries to find cover files in the same directory as
267 finished
= QtCore
.pyqtSignal('song', 'metadata')
269 def __init__(self
, plugin
):
270 QtCore
.QObject
.__init
__(self
, plugin
)
271 self
.logger
= plugin
.logger
272 self
.settings
= QtCore
.QSettings()
274 def fetch(self
, song
):
275 self
.logger
.info('Trying to guess local cover name.')
277 covers
= ['cover', 'album', 'front']
280 for ext
in QtGui
.QImageReader().supportedImageFormats():
281 exts
.append('*.%s'%str
(ext
))
286 filter.append('*.%s%s'%(cover
,ext
))
288 dirname
, filename
= common
.generate_metadata_path(song
, '$musicdir/$songdir', '')
289 dir = QtCore
.QDir(dirname
)
291 self
.logger
.error('Error opening directory %s.'%dirname
)
292 return self
.finished
.emit(song
, None)
294 dir.setNameFilters(filter)
295 files
= dir.entryList()
297 cover
= QtGui
.QPixmap(dir.filePath(files
[0]))
298 if not cover
.isNull():
299 self
.logger
.info('Found a cover: %s'%dir.filePath(files
[0]))
300 return self
.finished
.emit(song
, cover
)
302 # if this failed, try any supported image
303 dir.setNameFilters(exts
)
304 files
= dir.entryList()
306 cover
= QtGui
.QPixmap(dir.filePath(files
[0]))
307 if not cover
.isNull():
308 self
.logger
.info('Found a cover: %s'%dir.filePath(files
[0]))
309 return self
.finished
.emit(song
, cover
)
310 self
.logger
.info('No matching cover found')
311 self
.finished
.emit(song
, None)
318 self
.o
= AlbumCoverWidget(self
)
319 self
.mpclient
.song_changed
.connect(self
.refresh
)
320 self
.refresh_fetchers()
323 self
.mpclient
.song_changed
.disconnect(self
.refresh
)
326 self
.logger
.info('Autorefreshing cover.')
328 self
.__index
= len(self
.__fetchers
)
329 self
.o
.cover_loaded
= False
330 song
= self
.mpclient
.current_song()
332 self
.__cover
_dir
= ''
333 self
.__cover
_path
= ''
334 return self
.o
.set_cover(None, None)
336 (self
.__cover
_dir
, self
.__cover
_path
) = common
.generate_metadata_path(song
,
337 self
.settings
.value(self
.name
+ '/coverdir').toString(),
338 self
.settings
.value(self
.name
+ '/covername').toString())
340 self
.logger
.info('Trying to read cover from file %s.'%self
.__cover
_path
)
341 cover
= QtGui
.QPixmap(self
.__cover
_path
)
342 if not cover
.isNull():
343 return self
.o
.set_cover(song
, cover
)
345 self
.logger
.info('Error reading cover file: %s.'%e)
347 for fetcher
in self
.__fetchers
:
350 def refresh_fetchers(self
):
351 """Refresh the list of available fetchers."""
353 # append fetchers in order they are stored in settings
354 for name
in self
.settings
.value('%s/fetchers'%self
.name
).toStringList():
355 for site
in self
.available_fetchers
:
356 if site
.name
== name
:
357 self
.__fetchers
.append(site(self
))
358 self
.__fetchers
[-1].finished
.connect(self
.__new
_cover
_fetched
)
360 def save_cover_file(self
, cover
, path
= None):
361 """Save cover to a file specified in path.
362 If path is None, then a default value is used."""
363 self
.logger
.info('Saving cover...')
366 path
= self
.__cover
_path
367 cover
.save(path
, 'png')
368 self
.logger
.info('Cover successfully saved.')
370 self
.logger
.error('Error writing cover: %s', e
)
372 def delete_cover_file(self
, song
= None):
373 """Delete a cover file for song. If song is not specified
374 current song is used."""
376 path
= self
.__cover
_path
378 path
= common
.generate_metadata_path(song
, self
.settings
.value(self
.name
+ '/coverdir').toString(),
379 self
.settings
.value(self
.name
+ '/covername').toString())
380 if not QtCore
.QFile
.remove(path
):
381 self
.logger
.error('Error removing file %s.'%path
)
383 def select_cover(self
):
384 """Prompt user to manually select cover file for current song."""
385 song
= self
.mpclient
.current_song()
390 file = QtGui
.QFileDialog
.getOpenFileName(None,
391 'Select album cover for %s - %s'%(song
['artist'], song
['album']),
392 self
.__cover
_dir
, '')
396 cover
= QtGui
.QPixmap(file)
398 self
.logger
.error('Error opening cover file.')
401 if self
.settings
.value(self
.name
+ '/store').toBool():
402 self
.save_cover_file(cover
)
403 self
.o
.set_cover(song
, cover
)
408 return self
.o
.cover
if self
.o
.cover_loaded
else None
410 def _get_dock_widget(self
):
411 return self
._create
_dock
(self
.o
)
413 def get_settings_widget(self
):
414 return self
.SettingsWidgetAlbumCover(self
)