Move expand_tags for musicdir from winMain to nephilim_app.
[nephilim.git] / nephilim / plugins / AlbumCover.py
bloba62a701e35a367cbaaa52b9527394345a795696a
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
21 import os
23 from ..plugin import Plugin
24 from ..misc import APPNAME, expand_tags, generate_metadata_path
26 # FETCH MODES
27 AC_NO_FETCH = 0
28 AC_FETCH_LOCAL_DIR = 1
29 AC_FETCH_AMAZON = 2
31 class wgAlbumCover(QtGui.QLabel):
32 "cover - QPixmap or None"
33 cover = None
34 "is there a (non-default) cover loaded?"
35 cover_loaded = False
36 "plugin object"
37 plugin = None
38 "logger"
39 logger = None
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)
47 self.plugin = plugin
48 self.logger = plugin.logger
49 self.setAlignment(QtCore.Qt.AlignCenter)
51 # popup menu
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)
68 # MPD events
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):
76 event.accept()
77 self._menu.popup(event.globalPos())
79 def refresh(self):
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():
91 self.cover = None
92 self.cover_loaded = False
93 self.setPixmap(QtGui.QPixmap('gfx/no-cd-cover.png'))
94 self.plugin.emit(QtCore.SIGNAL('cover_changed'), None)
95 return
97 if song != self.plugin.mpclient().current_song():
98 return
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.')
110 else:
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
118 self.song = song
120 def run(self):
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()
126 if not song:
127 return self.emit(QtCore.SIGNAL('new_cover_fetched'), None, None)
129 thread = self.FetchThread(self, fetch_func, song)
130 thread.start()
132 def _fetch_auto(self, song):
133 """Autofetch cover for currently playing song."""
134 self.logger.info("autorefreshing cover")
136 # generate filenames
137 (self._cover_dirname, self._cover_filepath) = generate_metadata_path(song, self.plugin.settings().value(self.plugin.name() + '/coverdir').toString(),
138 self.plugin.settings().value(self.plugin.name() + '/covername').toString())
140 write = False
141 if not QtCore.QFile.exists(self._cover_filepath):
142 for i in (0, 1):
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)
148 else:
149 cover = QtGui.QImage()
151 if cover and not cover.isNull():
152 write = True
153 break
154 else:
155 cover = QtGui.QImage(self._cover_filepath)
157 return cover, write
159 def _fetch_local_manual(self):
160 song = self.plugin.mpclient().current_song()
161 if not 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)
168 if cover.isNull():
169 return None, False
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.')
174 # guess cover name
175 covers = ['cover', 'album', 'front']
177 exts = []
178 for ext in QtGui.QImageReader().supportedImageFormats():
179 exts.append('*.%s'%str(ext))
181 filter = []
182 for cover in covers:
183 for ext in exts:
184 filter.append('*.%s%s'%(cover,ext))
186 dir = QtCore.QDir(self._cover_dirname)
187 if not dir:
188 self.logger.error('Error opening directory' + self._cover_dirname)
189 return None
191 dir.setNameFilters(filter)
192 files = dir.entryList()
193 if files:
194 cover = QtGui.QImage(dir.filePath(files[0]))
195 if not cover.isNull():
196 self.logger.info('Found a cover.')
197 return cover
199 # if this failed, try any supported image
200 dir.setNameFilters(exts)
201 files = dir.entryList()
202 if files:
203 return QtGui.QImage(dir.filePath(files[0]))
204 self.logger.info('No matching cover found')
205 return None
207 def _fetch_amazon_manual(self, song):
208 cover = self._fetch_amazon(song)
209 if not cover:
210 return None, False
211 return cover, True
213 def _fetch_amazon(self, song):
214 if not song.artist() or not song.album():
215 return None
216 # get the url from amazon WS
217 coverURL = AmazonAlbumImage(song.artist(), song.album()).fetch()
218 self.logger.info('Fetching cover from Amazon')
219 if not coverURL:
220 self.logger.info('Cover not found on Amazon')
221 return None
223 img = urllib.urlopen(coverURL)
224 cover = QtGui.QImage()
225 cover.loadFromData(img.read())
226 return cover
228 def _view_cover(self):
229 if not self.cover_loaded:
230 return
231 win = QtGui.QLabel(self, QtCore.Qt.Window)
232 win.setScaledContents(True)
233 win.setPixmap(self.cover)
234 win.show()
236 def _save_cover(self):
237 if not self.cover_loaded:
238 return
240 cover = self.cover
241 file = QtGui.QFileDialog.getSaveFileName(None, '', os.path.expanduser('~'))
242 if file:
243 if not cover.save(file):
244 self.logger.error('Saving cover failed.')
246 class AlbumCover(Plugin):
247 o = None
248 DEFAULTS = {'coverdir' : '$musicdir/$songdir', 'covername' : '.cover_nephilim_$artist_$album',
249 'method0' : 1, 'method1' : 1, 'store' : True}
251 def _load(self):
252 self.o = wgAlbumCover(self)
253 def _unload(self):
254 self.o = None
255 def info(self):
256 return "Display the album cover of the currently playing album."
258 def refresh(self):
259 self.o.refresh() if self.o else self.logger.warning('Attemped to refresh when not loaded.')
261 def cover(self):
262 if not self.o:
263 return None
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):
270 methods = []
271 coverdir = None
272 covername = None
273 store = None
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())
294 # paths to covers
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'
300 %APPNAME)
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
334 import re
335 import urllib
337 AMAZON_AWS_ID = "0K4RZZKHSB5N2XYJWF02"
339 class AmazonAlbumImage(object):
340 awsurl = 'http://ecs.amazonaws.com/onca/xml'
341 def __init__(self, artist, album):
342 self.artist = artist
343 self.album = album
345 def fetch(self):
346 url = self._GetResultURL(self._SearchAmazon())
347 if not url:
348 return None
349 img_re = re.compile(r'''registerImage\("original_image", "([^"]+)"''')
350 try:
351 prod_data = urllib.urlopen(url).read()
352 except:
353 self.logger.warning('timeout opening %s'%(url))
354 return None
355 m = img_re.search(prod_data)
356 if not m:
357 return None
358 img_url = m.group(1)
359 return img_url
361 def _SearchAmazon(self):
362 data = {
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)))
376 return fd.read()
379 def _GetResultURL(self, xmldata):
380 if not xmldata:
381 return None
382 url_re = re.compile(r'<DetailPageURL>([^<]+)</DetailPageURL>')
383 m = url_re.search(xmldata)
384 return m and m.group(1)