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
):
102 DEFAULTS
= {'coverdir' : '$musicdir/$songdir', 'covername' : '.cover_nephilim_$artist_$album',
103 'fetchers': QtCore
.QStringList(['local', 'Last.fm']), 'store' : True}
104 "implemented fetchers"
105 available_fetchers
= None
106 "enabled fetchers, those with higher priority first"
108 "number of returned results from last refresh() call"
110 "index/priority of current cover"
117 cover_changed
= QtCore
.pyqtSignal(QtGui
.QPixmap
)
120 def __init__(self
, parent
, mpclient
, name
):
121 Plugin
.__init
__(self
, parent
, mpclient
, name
)
124 self
.available_fetchers
= [self
.FetcherLocal
, self
.FetcherLastfm
]
126 def __new_cover_fetched(self
, song
, cover
):
127 self
.logger
.info('Got new cover.')
130 i
= self
.__fetchers
.index(self
.sender())
131 if cover
and i
< self
.__index
:
132 if self
.settings
.value(self
.name
+ '/store').toBool():
133 self
.save_cover_file(cover
)
135 return self
.o
.set_cover(song
, cover
)
136 elif self
.__results
>= len(self
.__fetchers
) and not self
.o
.cover_loaded
:
137 self
.o
.set_cover(song
, None)
139 def __abort_fetch(self
):
140 """Aborts all fetches currently in progress."""
141 for fetcher
in self
.__fetchers
:
144 class SettingsWidgetAlbumCover(Plugin
.SettingsWidget
):
150 def __init__(self
, plugin
):
151 Plugin
.SettingsWidget
.__init
__(self
, plugin
)
152 self
.settings
.beginGroup(self
.plugin
.name
)
154 # store covers groupbox
155 self
.store
= QtGui
.QGroupBox('Store covers.')
156 self
.store
.setToolTip('Should %s store its own copy of covers?'%common
.APPNAME
)
157 self
.store
.setCheckable(True)
158 self
.store
.setChecked(self
.settings
.value('store').toBool())
159 self
.store
.setLayout(QtGui
.QGridLayout())
162 self
.coverdir
= QtGui
.QLineEdit(self
.settings
.value('coverdir').toString())
163 self
.coverdir
.setToolTip('Where should %s store covers.\n'
164 '$musicdir will be expanded to path to MPD music library (as set by user)\n'
165 '$songdir will be expanded to path to the song (relative to $musicdir\n'
166 'other tags same as in covername'
168 self
.covername
= QtGui
.QLineEdit(self
.settings
.value('covername').toString())
169 self
.covername
.setToolTip('Filename for %s cover files.\n'
170 'All tags supported by MPD will be expanded to their\n'
171 'values for current song, e.g. $title, $track, $artist,\n'
172 '$album, $genre etc.'%common
.APPNAME
)
173 self
.store
.layout().addWidget(QtGui
.QLabel('Cover directory'), 0, 0)
174 self
.store
.layout().addWidget(self
.coverdir
, 0, 1)
175 self
.store
.layout().addWidget(QtGui
.QLabel('Cover filename'), 1, 0)
176 self
.store
.layout().addWidget(self
.covername
, 1, 1)
179 fetchers
= self
.settings
.value('fetchers').toStringList()
180 self
.fetcherlist
= QtGui
.QListWidget(self
)
181 self
.fetcherlist
.setDragDropMode(QtGui
.QAbstractItemView
.InternalMove
)
182 for site
in fetchers
:
183 it
= QtGui
.QListWidgetItem(site
)
184 it
.setCheckState(QtCore
.Qt
.Checked
)
185 self
.fetcherlist
.addItem(it
)
186 for site
in self
.plugin
.available_fetchers
:
187 if not site
.name
in fetchers
:
188 it
= QtGui
.QListWidgetItem(site
.name
)
189 it
.setCheckState(QtCore
.Qt
.Unchecked
)
190 self
.fetcherlist
.addItem(it
)
192 self
.setLayout(QtGui
.QVBoxLayout())
193 self
.layout().addWidget(self
.store
)
194 self
._add
_widget
(self
.fetcherlist
, label
= 'Fetchers', tooltip
= 'A list of sources used for fetching covers.\n'
195 'Use drag and drop to change their priority.')
197 self
.settings
.endGroup()
199 def save_settings(self
):
200 self
.settings
.beginGroup(self
.plugin
.name
)
201 self
.settings
.setValue('coverdir', QVariant(self
.coverdir
.text()))
202 self
.settings
.setValue('covername', QVariant(self
.covername
.text()))
203 self
.settings
.setValue('store', QVariant(self
.store
.isChecked()))
205 fetchers
= QtCore
.QStringList()
206 for i
in range(self
.fetcherlist
.count()):
207 it
= self
.fetcherlist
.item(i
)
208 if it
.checkState() == QtCore
.Qt
.Checked
:
209 fetchers
.append(it
.text())
210 self
.settings
.setValue('fetchers', QVariant(fetchers
))
211 self
.settings
.endGroup()
212 self
.plugin
.refresh()
214 class FetcherLastfm(common
.MetadataFetcher
):
217 def fetch(self
, song
):
218 url
= QtCore
.QUrl('http://ws.audioscrobbler.com/2.0/')
219 url
.setQueryItems([('api_key', 'c325945c67b3e8327e01e3afb7cdcf35'),
220 ('method', 'album.getInfo'),
221 ('artist', song
.artist()),
222 ('album', song
.album())])
223 self
.fetch2(song
, url
)
224 self
.srep
.finished
.connect(self
.__handle
_search
_res
)
226 def __handle_search_res(self
):
228 xml
= QtCore
.QXmlStreamReader(self
.srep
)
230 while not xml
.atEnd():
231 token
= xml
.readNext()
232 if token
== QtCore
.QXmlStreamReader
.StartElement
:
233 if xml
.name() == 'image' and xml
.attributes().value('size') == 'extralarge':
234 url
= QtCore
.QUrl() # the url is already percent-encoded
235 url
.setEncodedUrl(xml
.readElementText().toLatin1())
237 self
.logger
.error('Error parsing seach results: %s'%xml
.errorString())
240 self
.logger
.info('Didn\'t find the URL in %s search results.'%self
.name
)
242 self
.logger
.info('Found %s song URL: %s.'%(self
.name
, url
))
244 self
.mrep
= self
.nam
.get(QtNetwork
.QNetworkRequest(url
))
245 self
.mrep
.finished
.connect(self
.__handle
_cover
)
247 def __handle_cover(self
):
248 data
= self
.mrep
.readAll()
249 pixmap
= QtGui
.QPixmap()
250 if pixmap
.loadFromData(data
):
254 class FetcherLocal(QtCore
.QObject
):
255 """This fetcher tries to find cover files in the same directory as
263 finished
= QtCore
.pyqtSignal('song', 'metadata')
265 def __init__(self
, plugin
):
266 QtCore
.QObject
.__init
__(self
, plugin
)
267 self
.logger
= plugin
.logger
268 self
.settings
= QtCore
.QSettings()
270 def fetch(self
, song
):
271 self
.logger
.info('Trying to guess local cover name.')
273 covers
= ['cover', 'album', 'front']
276 for ext
in QtGui
.QImageReader().supportedImageFormats():
277 exts
.append('*.%s'%str
(ext
))
282 filter.append('*.%s%s'%(cover
,ext
))
284 dir = QtCore
.QDir('%s/%s'%(self
.settings
.value('MPD/music_dir').toString(),
285 os
.path
.dirname(song
.filepath())))
287 self
.logger
.error('Error opening directory' + self
.__cover
_dir
)
288 return self
.finished
.emit(song
, None)
290 dir.setNameFilters(filter)
291 files
= dir.entryList()
293 cover
= QtGui
.QPixmap(dir.filePath(files
[0]))
294 if not cover
.isNull():
295 self
.logger
.info('Found a cover: %s'%dir.filePath(files
[0]))
296 return self
.finished
.emit(song
, cover
)
298 # if this failed, try any supported image
299 dir.setNameFilters(exts
)
300 files
= dir.entryList()
302 cover
= QtGui
.QPixmap(dir.filePath(files
[0]))
303 if not cover
.isNull():
304 self
.logger
.info('Found a cover: %s'%dir.filePath(files
[0]))
305 return self
.finished
.emit(song
, cover
)
306 self
.logger
.info('No matching cover found')
307 self
.finished
.emit(song
, None)
314 self
.o
= AlbumCoverWidget(self
)
315 self
.mpclient
.song_changed
.connect(self
.refresh
)
316 self
.refresh_fetchers()
319 self
.mpclient
.song_changed
.disconnect(self
.refresh
)
321 return "Display the album cover of the currently playing album."
324 self
.logger
.info('Autorefreshing cover.')
326 self
.__index
= len(self
.__fetchers
)
327 self
.o
.cover_loaded
= False
328 song
= self
.mpclient
.current_song()
330 self
.__cover
_dir
= ''
331 self
.__cover
_path
= ''
332 return self
.o
.set_cover(None, None)
334 (self
.__cover
_dir
, self
.__cover
_path
) = common
.generate_metadata_path(song
,
335 self
.settings
.value(self
.name
+ '/coverdir').toString(),
336 self
.settings
.value(self
.name
+ '/covername').toString())
338 self
.logger
.info('Trying to read cover from file %s.'%self
.__cover
_path
)
339 cover
= QtGui
.QPixmap(self
.__cover
_path
)
340 if not cover
.isNull():
341 return self
.o
.set_cover(song
, cover
)
343 self
.logger
.info('Error reading cover file: %s.'%e)
345 for fetcher
in self
.__fetchers
:
348 def refresh_fetchers(self
):
349 """Refresh the list of available fetchers."""
351 # append fetchers in order they are stored in settings
352 for name
in self
.settings
.value('%s/fetchers'%self
.name
).toStringList():
353 for site
in self
.available_fetchers
:
354 if site
.name
== name
:
355 self
.__fetchers
.append(site(self
))
356 self
.__fetchers
[-1].finished
.connect(self
.__new
_cover
_fetched
)
358 def save_cover_file(self
, cover
, path
= None):
359 """Save cover to a file specified in path.
360 If path is None, then a default value is used."""
361 self
.logger
.info('Saving cover...')
364 path
= self
.__cover
_path
365 cover
.save(path
, 'png')
366 self
.logger
.info('Cover successfully saved.')
368 self
.logger
.error('Error writing cover: %s', e
)
370 def delete_cover_file(self
, song
= None):
371 """Delete a cover file for song. If song is not specified
372 current song is used."""
374 path
= self
.__cover
_path
376 path
= common
.generate_metadata_path(song
, self
.settings
.value(self
.name
+ '/coverdir').toString(),
377 self
.settings
.value(self
.name
+ '/covername').toString())
378 if not QtCore
.QFile
.remove(path
):
379 self
.logger
.error('Error removing file %s.'%path
)
381 def select_cover(self
):
382 """Prompt user to manually select cover file for current song."""
383 song
= self
.mpclient
.current_song()
388 file = QtGui
.QFileDialog
.getOpenFileName(None,
389 'Select album cover for %s - %s'%(song
.artist(), song
.album()),
390 self
.__cover
_dir
, '')
394 cover
= QtGui
.QPixmap(file)
396 self
.logger
.error('Error opening cover file.')
399 if self
.settings
.value(self
.name
+ '/store').toBool():
400 self
.save_cover_file(cover
)
401 self
.o
.set_cover(song
, cover
)
406 return self
.o
.cover
if self
.o
.cover_loaded
else None
408 def _get_dock_widget(self
):
409 return self
._create
_dock
(self
.o
)
411 def get_settings_widget(self
):
412 return self
.SettingsWidgetAlbumCover(self
)