Lyrics: use getArtist on lyricwiki
[nephilim.git] / nephilim / plugins / Lyrics.py
blobfe3e36dad52f4355ed2a37c9278e32efb043d4c9
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, read-only
105 o = None
107 # private
108 DEFAULTS = {'fetchers' : QtCore.QStringList(['Lyricwiki', 'Animelyrics']), 'lyricdir' : '$musicdir/$songdir',
109 'lyricname' : '.lyrics_nephilim_$artist_$album_$title', 'store' : True}
110 "implemented fetchers"
111 available_fetchers = None #XXX SettingsWidget currently uses it
112 "enabled fetchers, those with higher priority first"
113 __fetchers = None
114 "number of returned results from last refresh() call"
115 __results = None
116 "index/priority of current lyrics"
117 __index = None
118 "metadata paths"
119 __lyrics_dir = None
120 __lyrics_path = None
122 #### private ####
123 def __init__(self, parent, mpclient, name):
124 Plugin.__init__(self, parent, mpclient, name)
126 self.__fetchers = []
127 self.available_fetchers = [self.FetchLyricwiki, self.FetchAnimelyrics]
129 def __new_lyrics_fetched(self, song, lyrics):
130 self.logger.info('Got new lyrics.')
131 self.__results += 1
133 i = self.__fetchers.index(self.sender())
134 if lyrics and i < self.__index:
135 if self.settings.value(self.name + '/store').toBool():
136 self.save_lyrics_file(lyrics)
137 self.__index = i
138 return self.o.set_lyrics(song, lyrics)
139 elif self.__results >= len(self.__fetchers) and not self.o.lyrics_loaded:
140 self.o.set_lyrics(song, None)
142 class FetchLyricwiki(common.MetadataFetcher):
143 name = 'Lyricwiki'
145 def fetch(self, song):
146 url = QtCore.QUrl('http://lyricwiki.org/api.php')
147 url.setQueryItems([('func', 'getArtist'), ('artist', song.artist()),
148 ('fmt', 'xml')])
149 self.fetch2(song, url)
150 self.srep.finished.connect(self.__handle_artist_res)
152 def __handle_artist_res(self):
153 artist = None
154 xml = QtCore.QXmlStreamReader(self.srep)
155 while not xml.atEnd():
156 token = xml.readNext()
157 if token == QtCore.QXmlStreamReader.StartElement:
158 if xml.name() == 'artist':
159 artist = xml.readElementText()
160 xml.clear()
161 if not artist:
162 self.logger.error('Didn\'t find artist in %s artist search results.'%self.name)
163 return self.finish()
165 url = QtCore.QUrl('http://lyricwiki.org/api.php')
166 url.setQueryItems([('func', 'getSong'), ('artist', artist),
167 ('song', self.song.title()), ('fmt', 'xml')])
168 self.srep = self.nam.get(QtNetwork.QNetworkRequest(url))
169 self.srep.finished.connect(self.__handle_search_res)
171 def __handle_search_res(self):
172 url = None
173 xml = QtCore.QXmlStreamReader(self.srep)
174 while not xml.atEnd():
175 token = xml.readNext()
176 if token == QtCore.QXmlStreamReader.StartElement:
177 if xml.name() == 'url':
178 url = QtCore.QUrl() # the url is already percent-encoded
179 url.setEncodedUrl(xml.readElementText().toLatin1())
180 elif xml.name() == 'lyrics' and xml.readElementText() == 'Not found':
181 xml.clear()
182 return self.finish()
183 if xml.hasError():
184 self.logger.error('Error parsing seach results: %s'%xml.errorString())
186 if not url:
187 self.logger.error('Didn\'t find the URL in Lyricwiki search results.')
188 return self.finish()
189 self.logger.info('Found Lyricwiki song URL: %s.'%url)
191 self.mrep = self.nam.get(QtNetwork.QNetworkRequest(url))
192 self.mrep.finished.connect(self.__handle_lyrics)
194 def __handle_lyrics(self):
195 #TODO this should use Qt xml functions too
196 lyrics = ''
197 page = unicode(self.mrep.readAll(), encoding = 'utf-8')
198 page = re.sub('<br>|<br/>|<br />', '\n', page)
199 try:
200 html = etree.HTML(page)
201 except etree.XMLSyntaxError, e:
202 self.logger.error('Error parsing lyrics: %s' %e)
203 return self.finish()
205 for elem in html.iterfind('.//div'):
206 if elem.get('class') == 'lyricbox':
207 lyrics += etree.tostring(elem, method = 'text', encoding = 'utf-8')
208 self.finish(lyrics)
210 class FetchAnimelyrics(common.MetadataFetcher):
211 name = 'Animelyrics'
213 def fetch(self, song):
214 url = QtCore.QUrl('http://www.animelyrics.com/search.php')
215 url.setQueryItems([('t', 'performer'), ('q', song.artist())])
216 self.fetch2(song, url)
217 self.srep.finished.connect(self.__handle_search_res)
219 def __handle_search_res(self):
220 # TODO use Qt xml functions
221 try:
222 tree = etree.HTML(unicode(self.srep.readAll(), encoding = 'utf-8', errors='ignore'))
223 except etree.XMLSyntaxError, e:
224 self.logger.error('Error parsing lyrics: %s' %e)
225 return self.finish()
227 url = None
228 for elem in tree.iterfind('.//a'):
229 if ('href' in elem.attrib) and elem.text and (self.song.title() in elem.text):
230 url = QtCore.QUrl('http://www.animelyrics.com/%s'%elem.get('href'))
232 if not url:
233 self.logger.info('Didn\'t find the URL in Animelyrics search results.')
234 return self.finish()
235 self.logger.info('Found Animelyrics song URL: %s.'%url)
237 self.mrep = self.nam.get(QtNetwork.QNetworkRequest(url))
238 self.mrep.finished.connect(self.__handle_lyrics)
240 def __handle_lyrics(self):
241 lyrics = ''
242 try:
243 tree = etree.HTML(unicode(self.mrep.readAll(), encoding = 'utf-8'))
244 except etree.XMLSyntaxError, e:
245 self.logger.error('Error parsing lyrics: %s' %e)
246 return self.finish()
247 for elem in tree.iterfind('.//pre'):
248 if elem.get('class') == 'lyrics':
249 lyrics += '%s\n\n'%etree.tostring(elem, method = 'text', encoding = 'utf-8')
251 self.finish(lyrics)
253 class SettingsWidgetLyrics(Plugin.SettingsWidget):
254 # private
255 lyricdir = None
256 lyricname = None
257 store = None
258 fetcherlist = None
260 def __init__(self, plugin):
261 Plugin.SettingsWidget.__init__(self, plugin)
262 self.settings.beginGroup(self.plugin.name)
265 # store lyrics groupbox
266 self.store = QtGui.QGroupBox('Store lyrics.')
267 self.store.setToolTip('Should %s store its own copy of lyrics?'%common.APPNAME)
268 self.store.setCheckable(True)
269 self.store.setChecked(self.settings.value('store').toBool())
270 self.store.setLayout(QtGui.QGridLayout())
272 # paths to lyrics
273 self.lyricdir = QtGui.QLineEdit(self.settings.value('lyricdir').toString())
274 self.lyricdir.setToolTip('Where should %s store lyrics.\n'
275 '$musicdir will be expanded to path to MPD music library (as set by user)\n'
276 '$songdir will be expanded to path to the song (relative to $musicdir\n'
277 'other tags same as in lyricname'
278 %common.APPNAME)
279 self.lyricname = QtGui.QLineEdit(self.settings.value('lyricname').toString())
280 self.lyricname.setToolTip('Filename for %s lyricsfiles.\n'
281 'All tags supported by MPD will be expanded to their\n'
282 'values for current song, e.g. $title, $track, $artist,\n'
283 '$album, $genre etc.'%common.APPNAME)
284 self.store.layout().addWidget(QtGui.QLabel('Lyrics directory'), 0, 0)
285 self.store.layout().addWidget(self.lyricdir, 0, 1)
286 self.store.layout().addWidget(QtGui.QLabel('Lyrics filename'), 1, 0)
287 self.store.layout().addWidget(self.lyricname, 1, 1)
289 # fetchers list
290 fetchers = self.settings.value('fetchers').toStringList()
291 self.fetcherlist = QtGui.QListWidget(self)
292 self.fetcherlist.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
293 for fetcher in fetchers:
294 it = QtGui.QListWidgetItem(fetcher)
295 it.setCheckState(QtCore.Qt.Checked)
296 self.fetcherlist.addItem(it)
297 for fetcher in self.plugin.available_fetchers:
298 if not fetcher.name in fetchers:
299 it = QtGui.QListWidgetItem(fetcher.name)
300 it.setCheckState(QtCore.Qt.Unchecked)
301 self.fetcherlist.addItem(it)
303 self.setLayout(QtGui.QVBoxLayout())
304 self.layout().addWidget(self.store)
305 self._add_widget(self.fetcherlist, label = 'Sites', tooltip = 'A list of sources used for fetching lyrics.\n'
306 'Use drag and drop to change their priority.')
308 self.settings.endGroup()
310 def save_settings(self):
311 self.settings.beginGroup(self.plugin.name)
312 self.settings.setValue('lyricdir', QVariant(self.lyricdir.text()))
313 self.settings.setValue('lyricname', QVariant(self.lyricname.text()))
314 self.settings.setValue('store', QVariant(self.store.isChecked()))
316 fetchers = QtCore.QStringList()
317 for i in range(self.fetcherlist.count()):
318 it = self.fetcherlist.item(i)
319 if it.checkState() == QtCore.Qt.Checked:
320 fetchers.append(it.text())
321 self.settings.setValue('fetchers', QVariant(fetchers))
323 self.settings.endGroup()
324 self.plugin.refresh_fetchers()
325 self.plugin.refresh()
327 #### public ####
328 def _load(self):
329 self.refresh_fetchers()
330 self.o = LyricsWidget(self)
331 self.mpclient.song_changed.connect(self.refresh)
332 def _unload(self):
333 self.o = None
334 self.__fetchers = None
335 self.mpclient.song_changed.disconnect(self.refresh)
336 def info(self):
337 return "Show (and fetch) the lyrics of the currently playing song."
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)