mpclient: change info from function to var.
[nephilim.git] / nephilim / plugins / Lyrics.py
blob9217df84523ee2d008b829d33a875ed8fce0f0fc
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
21 import os
22 import re
23 from lxml import etree
25 from ..plugin import Plugin
26 from .. import common
28 class LyricsWidget(QtGui.QWidget):
29 #public
30 lyrics_loaded = None
32 # public, read-only
33 plugin = None # plugin
34 logger = None
36 # private
37 __text_view = None # text-object
38 __toolbar = None
39 __label = None
41 #### private
42 def __init__(self, plugin):
43 QtGui.QWidget.__init__(self)
44 self.plugin = plugin
45 self.logger = plugin.logger
46 self.curLyrics = ''
48 self.__label = QtGui.QLabel(self)
49 self.__label.setWordWrap(True)
51 # add text area
52 self.__text_view = QtGui.QTextEdit(self)
53 self.__text_view.setReadOnly(True)
55 # add toolbar
56 self.__toolbar = QtGui.QToolBar('Lyrics toolbar', self)
57 self.__toolbar.setOrientation(QtCore.Qt.Vertical)
59 self.__toolbar.addAction(QtGui.QIcon('gfx/refresh.png'), 'Refresh lyrics', self.plugin.refresh)
60 edit = self.__toolbar.addAction(QtGui.QIcon('gfx/edit.png'), 'Edit lyrics')
61 edit.setCheckable(True)
62 edit.toggled.connect(self.__toggle_editable)
64 self.__toolbar.addAction(QtGui.QIcon('gfx/save.png'), 'Save lyrics', self.__save_lyrics)
65 self.__toolbar.addAction(QtGui.QIcon('gfx/delete.png'), 'Delete stored file', self.plugin.del_lyrics_file)
67 self.setLayout(QtGui.QGridLayout())
68 self.layout().setSpacing(0)
69 self.layout().setMargin(0)
70 self.layout().addWidget(self.__toolbar, 0, 0, -1, 1, QtCore.Qt.AlignTop)
71 self.layout().addWidget(self.__label, 0, 1)
72 self.layout().addWidget(self.__text_view, 1, 1)
74 def __save_lyrics(self):
75 self.plugin.save_lyrics_file(unicode(self.__text_view.toPlainText()).encode('utf-8'))
77 def __toggle_editable(self, val):
78 self.__text_view.setReadOnly(not val)
80 #### public ####
81 def set_lyrics(self, song, lyrics, flags = 0):
82 """Set currently displayed lyrics for song. flags parameter is
83 unused now."""
84 if not song:
85 self.__label.clear()
86 return self.__text_view.clear()
88 # a late thread might call this for a previous song
89 if song != self.plugin.mpclient.current_song():
90 return
92 self.__text_view.clear()
93 self.__label.setText('<b>%s</b> by <u>%s</u> on <u>%s</u>'\
94 %(song.title(), song.artist(), song.album()))
95 if lyrics:
96 self.logger.info('Setting new lyrics.')
97 self.__text_view.insertPlainText(lyrics.decode('utf-8'))
98 self.lyrics_loaded = True
99 else:
100 self.logger.info('Lyrics not found.')
101 self.__text_view.insertPlainText('Lyrics not found.')
103 class Lyrics(Plugin):
104 # public, const
105 info = 'Show (and fetch) the lyrics of the currently playing song.'
107 # public, read-only
108 o = None
110 # private
111 DEFAULTS = {'fetchers' : QtCore.QStringList(['Lyricwiki', 'Animelyrics']), 'lyricdir' : '$musicdir/$songdir',
112 'lyricname' : '.lyrics_nephilim_$artist_$album_$title', 'store' : True}
113 "implemented fetchers"
114 available_fetchers = None #XXX SettingsWidget currently uses it
115 "enabled fetchers, those with higher priority first"
116 __fetchers = None
117 "number of returned results from last refresh() call"
118 __results = None
119 "index/priority of current lyrics"
120 __index = None
121 "metadata paths"
122 __lyrics_dir = None
123 __lyrics_path = None
125 #### private ####
126 def __init__(self, parent, mpclient, name):
127 Plugin.__init__(self, parent, mpclient, name)
129 self.__fetchers = []
130 self.available_fetchers = [self.FetchLyricwiki, self.FetchAnimelyrics]
132 def __new_lyrics_fetched(self, song, lyrics):
133 self.logger.info('Got new lyrics.')
134 self.__results += 1
136 i = self.__fetchers.index(self.sender())
137 if lyrics and i < self.__index:
138 if self.settings.value(self.name + '/store').toBool():
139 self.save_lyrics_file(lyrics)
140 self.__index = i
141 return self.o.set_lyrics(song, lyrics)
142 elif self.__results >= len(self.__fetchers) and not self.o.lyrics_loaded:
143 self.o.set_lyrics(song, None)
145 class FetchLyricwiki(common.MetadataFetcher):
146 name = 'Lyricwiki'
148 def fetch(self, song):
149 url = QtCore.QUrl('http://lyricwiki.org/api.php')
150 url.setQueryItems([('func', 'getArtist'), ('artist', song.artist()),
151 ('fmt', 'xml')])
152 self.fetch2(song, url)
153 self.rep.finished.connect(self.__handle_artist_res)
155 def __handle_artist_res(self):
156 artist = None
157 xml = QtCore.QXmlStreamReader(self.rep)
158 while not xml.atEnd():
159 token = xml.readNext()
160 if token == QtCore.QXmlStreamReader.StartElement:
161 if xml.name() == 'artist':
162 artist = xml.readElementText()
163 xml.clear()
164 if not artist:
165 self.logger.error('Didn\'t find artist in %s artist search results.'%self.name)
166 return self.finish()
168 url = QtCore.QUrl('http://lyricwiki.org/api.php')
169 url.setQueryItems([('func', 'getSong'), ('artist', artist),
170 ('song', self.song.title()), ('fmt', 'xml')])
171 self.rep = self.nam.get(QtNetwork.QNetworkRequest(url))
172 self.rep.finished.connect(self.__handle_search_res)
174 def __handle_search_res(self):
175 url = None
176 xml = QtCore.QXmlStreamReader(self.rep)
177 while not xml.atEnd():
178 token = xml.readNext()
179 if token == QtCore.QXmlStreamReader.StartElement:
180 if xml.name() == 'url':
181 url = QtCore.QUrl() # the url is already percent-encoded
182 url.setEncodedUrl(xml.readElementText().toLatin1())
183 elif xml.name() == 'lyrics' and xml.readElementText() == 'Not found':
184 xml.clear()
185 return self.finish()
186 if xml.hasError():
187 self.logger.error('Error parsing seach results: %s'%xml.errorString())
189 if not url:
190 self.logger.error('Didn\'t find the URL in Lyricwiki search results.')
191 return self.finish()
192 self.logger.info('Found Lyricwiki song URL: %s.'%url)
194 self.rep = self.nam.get(QtNetwork.QNetworkRequest(url))
195 self.rep.finished.connect(self.__handle_lyrics)
197 def __handle_lyrics(self):
198 #TODO this should use Qt xml functions too
199 lyrics = ''
200 page = unicode(self.rep.readAll(), encoding = 'utf-8')
201 page = re.sub('<br>|<br/>|<br />', '\n', page)
202 try:
203 html = etree.HTML(page)
204 except etree.XMLSyntaxError, e:
205 self.logger.error('Error parsing lyrics: %s' %e)
206 return self.finish()
208 for elem in html.iterfind('.//div'):
209 if elem.get('class') == 'lyricbox':
210 lyrics += etree.tostring(elem, method = 'text', encoding = 'utf-8')
211 self.finish(lyrics)
213 class FetchAnimelyrics(common.MetadataFetcher):
214 name = 'Animelyrics'
216 def fetch(self, song):
217 url = QtCore.QUrl('http://www.animelyrics.com/search.php')
218 url.setQueryItems([('t', 'performer'), ('q', song.artist())])
219 self.fetch2(song, url)
220 self.rep.finished.connect(self.__handle_search_res)
222 def __handle_search_res(self):
223 # TODO use Qt xml functions
224 try:
225 tree = etree.HTML(unicode(self.rep.readAll(), encoding = 'utf-8', errors='ignore'))
226 except etree.XMLSyntaxError, e:
227 self.logger.error('Error parsing lyrics: %s' %e)
228 return self.finish()
230 url = None
231 for elem in tree.iterfind('.//a'):
232 if ('href' in elem.attrib) and elem.text and (self.song.title() in elem.text):
233 url = QtCore.QUrl('http://www.animelyrics.com/%s'%elem.get('href'))
235 if not url:
236 self.logger.info('Didn\'t find the URL in Animelyrics search results.')
237 return self.finish()
238 self.logger.info('Found Animelyrics song URL: %s.'%url)
240 self.rep = self.nam.get(QtNetwork.QNetworkRequest(url))
241 self.rep.finished.connect(self.__handle_lyrics)
243 def __handle_lyrics(self):
244 lyrics = ''
245 try:
246 tree = etree.HTML(unicode(self.rep.readAll(), encoding = 'utf-8'))
247 except etree.XMLSyntaxError, e:
248 self.logger.error('Error parsing lyrics: %s' %e)
249 return self.finish()
250 for elem in tree.iterfind('.//pre'):
251 if elem.get('class') == 'lyrics':
252 lyrics += '%s\n\n'%etree.tostring(elem, method = 'text', encoding = 'utf-8')
254 self.finish(lyrics)
256 class SettingsWidgetLyrics(Plugin.SettingsWidget):
257 # private
258 lyricdir = None
259 lyricname = None
260 store = None
261 fetcherlist = None
263 def __init__(self, plugin):
264 Plugin.SettingsWidget.__init__(self, plugin)
265 self.settings.beginGroup(self.plugin.name)
268 # store lyrics groupbox
269 self.store = QtGui.QGroupBox('Store lyrics.')
270 self.store.setToolTip('Should %s store its own copy of lyrics?'%common.APPNAME)
271 self.store.setCheckable(True)
272 self.store.setChecked(self.settings.value('store').toBool())
273 self.store.setLayout(QtGui.QGridLayout())
275 # paths to lyrics
276 self.lyricdir = QtGui.QLineEdit(self.settings.value('lyricdir').toString())
277 self.lyricdir.setToolTip('Where should %s store lyrics.\n'
278 '$musicdir will be expanded to path to MPD music library (as set by user)\n'
279 '$songdir will be expanded to path to the song (relative to $musicdir\n'
280 'other tags same as in lyricname'
281 %common.APPNAME)
282 self.lyricname = QtGui.QLineEdit(self.settings.value('lyricname').toString())
283 self.lyricname.setToolTip('Filename for %s lyricsfiles.\n'
284 'All tags supported by MPD will be expanded to their\n'
285 'values for current song, e.g. $title, $track, $artist,\n'
286 '$album, $genre etc.'%common.APPNAME)
287 self.store.layout().addWidget(QtGui.QLabel('Lyrics directory'), 0, 0)
288 self.store.layout().addWidget(self.lyricdir, 0, 1)
289 self.store.layout().addWidget(QtGui.QLabel('Lyrics filename'), 1, 0)
290 self.store.layout().addWidget(self.lyricname, 1, 1)
292 # fetchers list
293 fetchers = self.settings.value('fetchers').toStringList()
294 self.fetcherlist = QtGui.QListWidget(self)
295 self.fetcherlist.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
296 for fetcher in fetchers:
297 it = QtGui.QListWidgetItem(fetcher)
298 it.setCheckState(QtCore.Qt.Checked)
299 self.fetcherlist.addItem(it)
300 for fetcher in self.plugin.available_fetchers:
301 if not fetcher.name in fetchers:
302 it = QtGui.QListWidgetItem(fetcher.name)
303 it.setCheckState(QtCore.Qt.Unchecked)
304 self.fetcherlist.addItem(it)
306 self.setLayout(QtGui.QVBoxLayout())
307 self.layout().addWidget(self.store)
308 self._add_widget(self.fetcherlist, label = 'Sites', tooltip = 'A list of sources used for fetching lyrics.\n'
309 'Use drag and drop to change their priority.')
311 self.settings.endGroup()
313 def save_settings(self):
314 self.settings.beginGroup(self.plugin.name)
315 self.settings.setValue('lyricdir', QVariant(self.lyricdir.text()))
316 self.settings.setValue('lyricname', QVariant(self.lyricname.text()))
317 self.settings.setValue('store', QVariant(self.store.isChecked()))
319 fetchers = QtCore.QStringList()
320 for i in range(self.fetcherlist.count()):
321 it = self.fetcherlist.item(i)
322 if it.checkState() == QtCore.Qt.Checked:
323 fetchers.append(it.text())
324 self.settings.setValue('fetchers', QVariant(fetchers))
326 self.settings.endGroup()
327 self.plugin.refresh_fetchers()
328 self.plugin.refresh()
330 #### public ####
331 def _load(self):
332 self.refresh_fetchers()
333 self.o = LyricsWidget(self)
334 self.mpclient.song_changed.connect(self.refresh)
335 def _unload(self):
336 self.o = None
337 self.__fetchers = None
338 self.mpclient.song_changed.disconnect(self.refresh)
339 def _get_dock_widget(self):
340 return self._create_dock(self.o)
342 def refresh(self):
343 """Attempt to automatically get lyrics first from a file, then from the internet."""
344 self.logger.info('Autorefreshing lyrics.')
345 self.__results = 0
346 self.__index = len(self.__fetchers)
347 self.o.lyrics_loaded = False
348 song = self.mpclient.current_song()
349 if not song:
350 self.__lyrics_dir = ''
351 self.__lyrics_path = ''
352 return self.o.set_lyrics(None, None)
354 (self.__lyrics_dir, self.__lyrics_path) = common.generate_metadata_path(song,
355 self.settings.value(self.name + '/lyricdir').toString(),
356 self.settings.value(self.name + '/lyricname').toString())
357 try:
358 self.logger.info('Trying to read lyrics from file %s.'%self.__lyrics_path)
359 file = open(self.__lyrics_path, 'r')
360 lyrics = file.read()
361 file.close()
362 if lyrics:
363 return self.o.set_lyrics(song, lyrics)
364 except IOError, e:
365 self.logger.info('Error reading lyrics file: %s.'%e)
367 for fetcher in self.__fetchers:
368 fetcher.fetch(song)
370 def save_lyrics_file(self, lyrics, path = None):
371 """Save lyrics to a file specified in path.
372 If path is None, then a default value is used."""
373 self.logger.info('Saving lyrics...')
374 try:
375 if path:
376 file = open(path, 'w')
377 else:
378 file = open(self.__lyrics_path, 'w')
379 file.write(lyrics)
380 file.close()
381 self.logger.info('Lyrics successfully saved.')
382 except IOError, e:
383 self.logger.error('Error writing lyrics: %s', e)
385 def del_lyrics_file(self, song = None):
386 """Delete a lyrics file for song. If song is not specified
387 current song is used."""
388 if not song:
389 path = self.__lyrics_path
390 else:
391 path = common.generate_metadata_path(song, self.settings.value(self.name + '/lyricdir').toString(),
392 self.settings.value(self.name + '/lyricname').toString())
394 try:
395 os.remove(path)
396 except IOError, e:
397 self.logger.error('Error removing lyrics file %s: %s'%(path, e))
399 def get_settings_widget(self):
400 return self.SettingsWidgetLyrics(self)
402 def refresh_fetchers(self):
403 """Refresh the list of available fetchers."""
404 self.__fetchers = []
405 # append fetchers in order they are stored in settings
406 for name in self.settings.value('%s/fetchers'%self.name).toStringList():
407 for fetcher in self.available_fetchers:
408 if fetcher.name == name:
409 self.__fetchers.append(fetcher(self))
410 self.__fetchers[-1].finished.connect(self.__new_lyrics_fetched)