2 # Copyright (C) 2008 jerous <jerous@gmail.com>
3 # Copyright (C) 2009 Anton Khirnov <wyskas@gmail.com>
5 # Nephilim is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # Nephilim is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with Nephilim. If not, see <http://www.gnu.org/licenses/>.
19 from PyQt4
import QtGui
, QtCore
20 from PyQt4
.QtCore
import QVariant
23 from ..plugin
import Plugin
24 from ..misc
import APPNAME
, expand_tags
, generate_metadata_path
28 AC_FETCH_LOCAL_DIR
= 1
31 class wgAlbumCover(QtGui
.QLabel
):
32 "cover - QPixmap or None"
34 "is there a (non-default) cover loaded?"
41 _cover_dirname
= None # Directory and full filepath where cover
42 _cover_filepath
= None # for current song should be stored.
43 _menu
= None # popup menu
45 def __init__(self
, plugin
):
46 QtGui
.QLabel
.__init
__(self
)
48 self
.logger
= plugin
.logger
49 self
.setAlignment(QtCore
.Qt
.AlignCenter
)
52 self
._menu
= QtGui
.QMenu("album")
53 refresh
= self
._menu
.addAction('&Refresh cover.')
54 refresh
.setObjectName('refresh')
55 select_file_action
= self
._menu
.addAction('&Select cover file...')
56 select_file_action
.setObjectName('select_file_action')
57 fetch_amazon_action
= self
._menu
.addAction('Fetch from &Amazon.')
58 fetch_amazon_action
.setObjectName('fetch_amazon_action')
59 view_action
= self
._menu
.addAction('&View in a separate window.')
60 save_action
= self
._menu
.addAction('Save cover &as...')
62 self
.connect(refresh
, QtCore
.SIGNAL('triggered()'), self
.refresh
)
63 self
.connect(select_file_action
, QtCore
.SIGNAL('triggered()'), self
._fetch
_local
_manual
)
64 self
.connect(fetch_amazon_action
, QtCore
.SIGNAL('triggered()'), self
.fetch_amazon
)
65 self
.connect(view_action
, QtCore
.SIGNAL('triggered()'), self
._view
_cover
)
66 self
.connect(save_action
, QtCore
.SIGNAL('triggered()'), self
._save
_cover
)
69 self
.connect(self
.plugin
.mpclient(), QtCore
.SIGNAL('song_changed'), self
.refresh
)
70 self
.connect(self
.plugin
.mpclient(), QtCore
.SIGNAL('disconnected'), self
.refresh
)
71 self
.connect(self
.plugin
.mpclient(), QtCore
.SIGNAL('state_changed'),self
.refresh
)
73 self
.connect(self
, QtCore
.SIGNAL('new_cover_fetched'), self
.set_cover
)
75 def contextMenuEvent(self
, event
):
77 self
._menu
.popup(event
.globalPos())
80 self
._fetch
_cover
(self
._fetch
_auto
)
82 def fetch_amazon(self
):
83 self
._fetch
_cover
(self
._fetch
_amazon
_manual
)
85 def set_cover(self
, song
, cover
, write
= False):
86 """Set cover for current song, attempt to write it to a file
87 if write is True and it's globally allowed."""
89 self
.logger
.info('Setting cover')
90 if not cover
or cover
.isNull():
92 self
.cover_loaded
= False
93 self
.setPixmap(QtGui
.QPixmap('gfx/no-cd-cover.png'))
94 self
.plugin
.emit(QtCore
.SIGNAL('cover_changed'), None)
97 if song
!= self
.plugin
.mpclient().current_song():
100 self
.cover
= QtGui
.QPixmap
.fromImage(cover
)
101 self
.cover_loaded
= True
102 self
.setPixmap(self
.cover
.scaled(self
.size(), QtCore
.Qt
.KeepAspectRatio
, QtCore
.Qt
.SmoothTransformation
))
103 self
.plugin
.emit(QtCore
.SIGNAL('cover_changed'), self
.cover
)
104 self
.logger
.info('Cover set.')
106 if (write
and self
.plugin
.settings().value(self
.plugin
.name() + '/store').toBool()
107 and self
._cover
_filepath
):
108 if self
.cover
.save(self
._cover
_filepath
, 'png'):
109 self
.logger
.info('Cover saved.')
111 self
.logger
.error('Error saving cover.')
113 class FetchThread(QtCore
.QThread
):
114 def __init__(self
, parent
, fetch_func
, song
):
115 QtCore
.QThread
.__init
__(self
)
116 self
.setParent(parent
)
117 self
.fetch_func
= fetch_func
121 cover
, write
= self
.fetch_func(self
.song
)
122 self
.parent().emit(QtCore
.SIGNAL('new_cover_fetched'), self
.song
, cover
, write
)
124 def _fetch_cover(self
, fetch_func
):
125 song
= self
.plugin
.mpclient().current_song()
127 return self
.emit(QtCore
.SIGNAL('new_cover_fetched'), None, None)
129 thread
= self
.FetchThread(self
, fetch_func
, song
)
132 def _fetch_auto(self
, song
):
133 """Autofetch cover for currently playing song."""
134 self
.logger
.info("autorefreshing cover")
137 (self
._cover
_dirname
, self
._cover
_filepath
) = generate_metadata_path(self
.plugin
.parent(), song
, self
.plugin
.settings().value(self
.plugin
.name() + '/coverdir').toString(),
138 self
.plugin
.settings().value(self
.plugin
.name() + '/covername').toString())
141 if not QtCore
.QFile
.exists(self
._cover
_filepath
):
143 src
= self
.plugin
.settings().value(self
.plugin
.name() + '/method%i'%i).toInt()[0]
144 if src
== AC_FETCH_LOCAL_DIR
and self
._cover
_dirname
:
145 cover
= self
._fetch
_local
(song
)
146 elif src
== AC_FETCH_AMAZON
:
147 cover
= self
._fetch
_amazon
(song
)
149 cover
= QtGui
.QImage()
151 if cover
and not cover
.isNull():
155 cover
= QtGui
.QImage(self
._cover
_filepath
)
159 def _fetch_local_manual(self
):
160 song
= self
.plugin
.mpclient().current_song()
162 return self
.emit(QtCore
.SIGNAL('new_cover_fetched'), None, None)
164 file = QtGui
.QFileDialog
.getOpenFileName(self
,
165 'Select album cover for %s - %s'%(song
.artist(), song
.album()),
166 self
._cover
_dirname
, '')
167 cover
= QtGui
.QImage(file)
170 self
.emit(QtCore
.SIGNAL('new_cover_fetched'), song
, cover
, True)
172 def _fetch_local(self
, song
):
173 self
.logger
.info('Trying to guess local cover name.')
175 covers
= ['cover', 'album', 'front']
178 for ext
in QtGui
.QImageReader().supportedImageFormats():
179 exts
.append('*.%s'%str
(ext
))
184 filter.append('*.%s%s'%(cover
,ext
))
186 dir = QtCore
.QDir(self
._cover
_dirname
)
188 self
.logger
.error('Error opening directory' + self
._cover
_dirname
)
191 dir.setNameFilters(filter)
192 files
= dir.entryList()
194 cover
= QtGui
.QImage(dir.filePath(files
[0]))
195 if not cover
.isNull():
196 self
.logger
.info('Found a cover.')
199 # if this failed, try any supported image
200 dir.setNameFilters(exts
)
201 files
= dir.entryList()
203 return QtGui
.QImage(dir.filePath(files
[0]))
204 self
.logger
.info('No matching cover found')
207 def _fetch_amazon_manual(self
, song
):
208 cover
= self
._fetch
_amazon
(song
)
213 def _fetch_amazon(self
, song
):
214 if not song
.artist() or not song
.album():
216 # get the url from amazon WS
217 coverURL
= AmazonAlbumImage(song
.artist(), song
.album()).fetch()
218 self
.logger
.info('Fetching cover from Amazon')
220 self
.logger
.info('Cover not found on Amazon')
223 img
= urllib
.urlopen(coverURL
)
224 cover
= QtGui
.QImage()
225 cover
.loadFromData(img
.read())
228 def _view_cover(self
):
229 if not self
.cover_loaded
:
231 win
= QtGui
.QLabel(self
, QtCore
.Qt
.Window
)
232 win
.setScaledContents(True)
233 win
.setPixmap(self
.cover
)
236 def _save_cover(self
):
237 if not self
.cover_loaded
:
241 file = QtGui
.QFileDialog
.getSaveFileName(None, '', os
.path
.expanduser('~'))
243 if not cover
.save(file):
244 self
.logger
.error('Saving cover failed.')
246 class AlbumCover(Plugin
):
248 DEFAULTS
= {'coverdir' : '$musicdir/$songdir', 'covername' : '.cover_nephilim_$artist_$album',
249 'method0' : 1, 'method1' : 1, 'store' : True}
252 self
.o
= wgAlbumCover(self
)
256 return "Display the album cover of the currently playing album."
259 self
.o
.refresh() if self
.o
else self
.logger
.warning('Attemped to refresh when not loaded.')
264 return self
.o
.cover
if self
.o
.cover_loaded
else None
266 def _get_dock_widget(self
):
267 return self
._create
_dock
(self
.o
)
269 class SettingsWidgetAlbumCover(Plugin
.SettingsWidget
):
275 def __init__(self
, plugin
):
276 Plugin
.SettingsWidget
.__init
__(self
, plugin
)
277 self
.settings().beginGroup(self
.plugin
.name())
279 # fetching methods comboboxes
280 self
.methods
= [QtGui
.QComboBox(), QtGui
.QComboBox()]
281 for i
,method
in enumerate(self
.methods
):
282 method
.addItem('No method.')
283 method
.addItem('Local dir')
284 method
.addItem('Amazon')
285 method
.setCurrentIndex(self
.settings().value('method' + str(i
)).toInt()[0])
287 # store covers groupbox
288 self
.store
= QtGui
.QGroupBox('Store covers.')
289 self
.store
.setToolTip('Should %s store its own copy of covers?'%APPNAME
)
290 self
.store
.setCheckable(True)
291 self
.store
.setChecked(self
.settings().value('store').toBool())
292 self
.store
.setLayout(QtGui
.QGridLayout())
295 self
.coverdir
= QtGui
.QLineEdit(self
.settings().value('coverdir').toString())
296 self
.coverdir
.setToolTip('Where should %s store covers.\n'
297 '$musicdir will be expanded to path to MPD music library (as set by user)\n'
298 '$songdir will be expanded to path to the song (relative to $musicdir\n'
299 'other tags same as in covername'
301 self
.covername
= QtGui
.QLineEdit(self
.settings().value('covername').toString())
302 self
.covername
.setToolTip('Filename for %s cover files.\n'
303 'All tags supported by MPD will be expanded to their\n'
304 'values for current song, e.g. $title, $track, $artist,\n'
305 '$album, $genre etc.'%APPNAME
)
306 self
.store
.layout().addWidget(QtGui
.QLabel('Cover directory'), 0, 0)
307 self
.store
.layout().addWidget(self
.coverdir
, 0, 1)
308 self
.store
.layout().addWidget(QtGui
.QLabel('Cover filename'), 1, 0)
309 self
.store
.layout().addWidget(self
.covername
, 1, 1)
311 self
.setLayout(QtGui
.QVBoxLayout())
312 self
._add
_widget
(self
.methods
[0], 'Method 0', 'Method to try first.')
313 self
._add
_widget
(self
.methods
[1], 'Method 1', 'Method to try if the first one fails.')
314 self
.layout().addWidget(self
.store
)
316 self
.settings().endGroup()
318 def save_settings(self
):
319 self
.settings().beginGroup(self
.plugin
.name())
320 self
.settings().setValue('method0', QVariant(self
.methods
[0].currentIndex()))
321 self
.settings().setValue('method1', QVariant(self
.methods
[1].currentIndex()))
322 self
.settings().setValue('coverdir', QVariant(self
.coverdir
.text()))
323 self
.settings().setValue('covername', QVariant(self
.covername
.text()))
324 self
.settings().setValue('store', QVariant(self
.store
.isChecked()))
325 self
.settings().endGroup()
326 self
.plugin
.o
.refresh()
328 def get_settings_widget(self
):
329 return self
.SettingsWidgetAlbumCover(self
)
332 # This is the amazon cover fetcher using their webservice api
333 # Thank you, http://www.semicomplete.com/scripts/albumcover.py
337 AMAZON_AWS_ID
= "0K4RZZKHSB5N2XYJWF02"
339 class AmazonAlbumImage(object):
340 awsurl
= 'http://ecs.amazonaws.com/onca/xml'
341 def __init__(self
, artist
, album
):
346 url
= self
._GetResultURL
(self
._SearchAmazon
())
349 img_re
= re
.compile(r
'''registerImage\("original_image", "([^"]+)"''')
351 prod_data
= urllib
.urlopen(url
).read()
353 self
.logger
.warning('timeout opening %s'%(url))
355 m
= img_re
.search(prod_data
)
361 def _SearchAmazon(self
):
363 'Service' : 'AWSECommerceService',
364 'Version' : '2005-03-23',
365 'Operation' : 'ItemSearch',
366 'ContentType' : 'text/xml',
367 'SubscriptionId': AMAZON_AWS_ID
,
368 'SearchIndex' : 'Music',
369 'ResponseGroup' : 'Small',
372 data
['Artist'] = self
.artist
.encode('utf-8')
373 data
['Keywords'] = self
.album
.encode('utf-8')
375 fd
= urllib
.urlopen('%s?%s' % (self
.awsurl
, urllib
.urlencode(data
)))
379 def _GetResultURL(self
, xmldata
):
382 url_re
= re
.compile(r
'<DetailPageURL>([^<]+)</DetailPageURL>')
383 m
= url_re
.search(xmldata
)
384 return m
and m
.group(1)