rename misc->common
[nephilim.git] / nephilim / plugins / Lyrics.py
blob15160d8396e2c0be0ca1db2ccba021d19e592f60
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.connect(edit, QtCore.SIGNAL('toggled(bool)'), 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.')
104 class Lyrics(Plugin):
105 # public, read-only
106 o = None
108 # private
109 DEFAULTS = {'fetchers' : QtCore.QStringList(['Lyricwiki', 'Animelyrics']), 'lyricdir' : '$musicdir/$songdir',
110 'lyricname' : '.lyrics_nephilim_$artist_$album_$title', 'store' : True}
111 "implemented fetchers"
112 available_fetchers = None #XXX SettingsWidget currently uses it
113 "enabled fetchers, those with higher priority first"
114 __fetchers = None
115 "number of returned results from last refresh() call"
116 __results = None
117 "index/priority of current lyrics"
118 __index = None
119 "metadata paths"
120 __lyrics_dir = None
121 __lyrics_path = None
123 #### private ####
124 def __init__(self, parent, mpclient, name):
125 Plugin.__init__(self, parent, mpclient, name)
127 self.__fetchers = []
128 self.available_fetchers = [self.FetchLyricwiki, self.FetchAnimelyrics]
130 def __new_lyrics_fetched(self, song, lyrics):
131 self.logger.info('Got new lyrics.')
132 self.__results += 1
134 i = self.__fetchers.index(self.sender())
135 if lyrics and i < self.__index:
136 if self.settings.value(self.name + '/store').toBool():
137 self.save_lyrics_file(lyrics)
138 self.__index = i
139 return self.o.set_lyrics(song, lyrics)
140 elif self.__results >= len(self.__fetchers) and not self.o.lyrics_loaded:
141 self.o.set_lyrics(song, None)
143 class FetchLyricwiki(common.MetadataFetcher):
144 name = 'Lyricwiki'
146 def fetch(self, song):
147 url = QtCore.QUrl('http://lyricwiki.org/api.php')
148 url.setQueryItems([('func', 'getSong'), ('artist', song.artist()),
149 ('song', song.title()), ('fmt', 'xml')])
150 self.fetch2(song, url)
151 self.srep.finished.connect(self.__handle_search_res)
153 def __handle_search_res(self):
154 url = None
155 xml = QtCore.QXmlStreamReader(self.srep)
156 while not xml.atEnd():
157 token = xml.readNext()
158 if token == QtCore.QXmlStreamReader.StartElement:
159 if xml.name() == 'url':
160 url = QtCore.QUrl() # the url is already percent-encoded
161 url.setEncodedUrl(xml.readElementText().toLatin1())
162 elif xml.name() == 'lyrics' and xml.readElementText() == 'Not found':
163 xml.clear()
164 return self.finish()
165 if xml.hasError():
166 self.logger.error('Error parsing seach results.%s'%xml.errorString())
168 if not url:
169 self.logger.error('Didn\'t find the URL in Lyricwiki search results.')
170 return self.finish()
171 self.logger.info('Found Lyricwiki song URL: %s.'%url)
173 self.mrep = self.nam.get(QtNetwork.QNetworkRequest(url))
174 self.mrep.finished.connect(self.__handle_lyrics)
176 def __handle_lyrics(self):
177 #TODO this should use Qt xml functions too
178 lyrics = ''
179 page = unicode(self.mrep.readAll(), encoding = 'utf-8')
180 page = re.sub('<br>|<br/>|<br />', '\n', page)
181 try:
182 html = etree.HTML(page)
183 except etree.XMLSyntaxError, e:
184 self.logger.error('Error parsing lyrics: %s' %e)
185 return self.finish()
187 for elem in html.iterfind('.//div'):
188 if elem.get('class') == 'lyricbox':
189 lyrics += etree.tostring(elem, method = 'text', encoding = 'utf-8')
190 self.finish(lyrics)
192 class FetchAnimelyrics(common.MetadataFetcher):
193 name = 'Animelyrics'
195 def fetch(self, song):
196 url = QtCore.QUrl('http://www.animelyrics.com/search.php')
197 url.setQueryItems([('t', 'performer'), ('q', song.artist())])
198 self.fetch2(song, url)
199 self.connect(self.srep, QtCore.SIGNAL('finished()'), self.__handle_search_res)
201 def __handle_search_res(self):
202 # TODO use Qt xml functions
203 try:
204 tree = etree.HTML(unicode(self.srep.readAll(), encoding = 'utf-8', errors='ignore'))
205 except etree.XMLSyntaxError, e:
206 self.logger.error('Error parsing lyrics: %s' %e)
207 return self.finish()
209 url = None
210 for elem in tree.iterfind('.//a'):
211 if ('href' in elem.attrib) and elem.text and (self.song.title() in elem.text):
212 url = QtCore.QUrl('http://www.animelyrics.com/%s'%elem.get('href'))
214 if not url:
215 self.logger.info('Didn\'t find the URL in Animelyrics search results.')
216 return self.finish()
217 self.logger.info('Found Animelyrics song URL: %s.'%url)
219 self.mrep = self.nam.get(QtNetwork.QNetworkRequest(url))
220 self.connect(self.mrep, QtCore.SIGNAL('finished()'), self.__handle_lyrics)
222 def __handle_lyrics(self):
223 lyrics = ''
224 try:
225 tree = etree.HTML(unicode(self.mrep.readAll(), encoding = 'utf-8'))
226 except etree.XMLSyntaxError, e:
227 self.logger.error('Error parsing lyrics: %s' %e)
228 return self.finish()
229 for elem in tree.iterfind('.//pre'):
230 if elem.get('class') == 'lyrics':
231 lyrics += '%s\n\n'%etree.tostring(elem, method = 'text', encoding = 'utf-8')
233 self.finish(lyrics)
235 class SettingsWidgetLyrics(Plugin.SettingsWidget):
236 # private
237 lyricdir = None
238 lyricname = None
239 store = None
240 fetcherlist = None
242 def __init__(self, plugin):
243 Plugin.SettingsWidget.__init__(self, plugin)
244 self.settings.beginGroup(self.plugin.name)
247 # store lyrics groupbox
248 self.store = QtGui.QGroupBox('Store lyrics.')
249 self.store.setToolTip('Should %s store its own copy of lyrics?'%common.APPNAME)
250 self.store.setCheckable(True)
251 self.store.setChecked(self.settings.value('store').toBool())
252 self.store.setLayout(QtGui.QGridLayout())
254 # paths to lyrics
255 self.lyricdir = QtGui.QLineEdit(self.settings.value('lyricdir').toString())
256 self.lyricdir.setToolTip('Where should %s store lyrics.\n'
257 '$musicdir will be expanded to path to MPD music library (as set by user)\n'
258 '$songdir will be expanded to path to the song (relative to $musicdir\n'
259 'other tags same as in lyricname'
260 %common.APPNAME)
261 self.lyricname = QtGui.QLineEdit(self.settings.value('lyricname').toString())
262 self.lyricname.setToolTip('Filename for %s lyricsfiles.\n'
263 'All tags supported by MPD will be expanded to their\n'
264 'values for current song, e.g. $title, $track, $artist,\n'
265 '$album, $genre etc.'%common.APPNAME)
266 self.store.layout().addWidget(QtGui.QLabel('Lyrics directory'), 0, 0)
267 self.store.layout().addWidget(self.lyricdir, 0, 1)
268 self.store.layout().addWidget(QtGui.QLabel('Lyrics filename'), 1, 0)
269 self.store.layout().addWidget(self.lyricname, 1, 1)
271 # fetchers list
272 fetchers = self.settings.value('fetchers').toStringList()
273 self.fetcherlist = QtGui.QListWidget(self)
274 self.fetcherlist.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
275 for fetcher in fetchers:
276 it = QtGui.QListWidgetItem(fetcher)
277 it.setCheckState(QtCore.Qt.Checked)
278 self.fetcherlist.addItem(it)
279 for fetcher in self.plugin.available_fetchers:
280 if not fetcher.name in fetchers:
281 it = QtGui.QListWidgetItem(fetcher.name)
282 it.setCheckState(QtCore.Qt.Unchecked)
283 self.fetcherlist.addItem(it)
285 self.setLayout(QtGui.QVBoxLayout())
286 self.layout().addWidget(self.store)
287 self._add_widget(self.fetcherlist, label = 'Sites', tooltip = 'A list of sources used for fetching lyrics.\n'
288 'Use drag and drop to change their priority.')
290 self.settings.endGroup()
292 def save_settings(self):
293 self.settings.beginGroup(self.plugin.name)
294 self.settings.setValue('lyricdir', QVariant(self.lyricdir.text()))
295 self.settings.setValue('lyricname', QVariant(self.lyricname.text()))
296 self.settings.setValue('store', QVariant(self.store.isChecked()))
298 fetchers = QtCore.QStringList()
299 for i in range(self.fetcherlist.count()):
300 it = self.fetcherlist.item(i)
301 if it.checkState() == QtCore.Qt.Checked:
302 fetchers.append(it.text())
303 self.settings.setValue('fetchers', QVariant(fetchers))
305 self.settings.endGroup()
306 self.plugin.refresh_fetchers()
307 self.plugin.refresh()
309 #### public ####
310 def _load(self):
311 self.refresh_fetchers()
312 self.o = LyricsWidget(self)
313 self.connect(self.mpclient, QtCore.SIGNAL('song_changed'), self.refresh)
314 def _unload(self):
315 self.o = None
316 self.__fetchers = None
317 self.disconnect(self.mpclient, QtCore.SIGNAL('song_changed'), self.refresh)
318 def info(self):
319 return "Show (and fetch) the lyrics of the currently playing song."
321 def _get_dock_widget(self):
322 return self._create_dock(self.o)
324 def refresh(self):
325 """Attempt to automatically get lyrics first from a file, then from the internet."""
326 self.logger.info('Autorefreshing lyrics.')
327 self.__results = 0
328 self.__index = len(self.__fetchers)
329 self.o.lyrics_loaded = False
330 song = self.mpclient.current_song()
331 if not song:
332 self.__lyrics_dir = ''
333 self.__lyrics_path = ''
334 return self.o.set_lyrics(None, None)
336 (self.__lyrics_dir, self.__lyrics_path) = common.generate_metadata_path(song,
337 self.settings.value(self.name + '/lyricdir').toString(),
338 self.settings.value(self.name + '/lyricname').toString())
339 try:
340 self.logger.info('Trying to read lyrics from file %s.'%self.__lyrics_path)
341 file = open(self.__lyrics_path, 'r')
342 lyrics = file.read()
343 file.close()
344 if lyrics:
345 return self.o.set_lyrics(song, lyrics)
346 except IOError, e:
347 self.logger.info('Error reading lyrics file: %s.'%e)
349 for fetcher in self.__fetchers:
350 fetcher.fetch(song)
352 def save_lyrics_file(self, lyrics, path = None):
353 """Save lyrics to a file specified in path.
354 If path is None, then a default value is used."""
355 self.logger.info('Saving lyrics...')
356 try:
357 if path:
358 file = open(path, 'w')
359 else:
360 file = open(self.__lyrics_path, 'w')
361 file.write(lyrics)
362 file.close()
363 self.logger.info('Lyrics successfully saved.')
364 except IOError, e:
365 self.logger.error('Error writing lyrics: %s', e)
367 def del_lyrics_file(self, song = None):
368 """Delete a lyrics file for song. If song is not specified
369 current song is used."""
370 if not song:
371 path = self.__lyrics_path
372 else:
373 path = common.generate_metadata_path(song, self.settings.value(self.name + '/lyricdir').toString(),
374 self.settings.value(self.name + '/lyricname').toString())
376 try:
377 os.remove(path)
378 except IOError, e:
379 self.logger.error('Error removing lyrics file %s: %s'%(path, e))
381 def get_settings_widget(self):
382 return self.SettingsWidgetLyrics(self)
384 def refresh_fetchers(self):
385 """Refresh the list of available fetchers."""
386 self.__fetchers = []
387 # append fetchers in order they are stored in settings
388 for name in self.settings.value('%s/fetchers'%self.name).toStringList():
389 for fetcher in self.available_fetchers:
390 if fetcher.name == name:
391 self.__fetchers.append(fetcher(self))
392 self.connect(self.__fetchers[-1], QtCore.SIGNAL('finished'), self.__new_lyrics_fetched)