Lyrics: use lxml for parsing animelyrics pages.
[nephilim.git] / nephilim / plugins / Lyrics.py
blob8b57f6e0b2d74339b3e0725353f866a436d5d2a2
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
19 from PyQt4.QtCore import QVariant
21 import socket
23 from ..plugin import Plugin
24 from .. import misc
26 _available_sites = []
27 try:
28 import re
29 import urllib
30 from lxml import etree
31 _available_sites.append('lyricwiki')
32 _available_sites.append('animelyrics')
33 except ImportError:
34 print 'Lyrics: error importing LyricWiki. Make sure that lxml is installed.'
36 class wgLyrics(QtGui.QWidget):
37 txtView = None # text-object
38 p = None # plugin
39 logger = None
40 def __init__(self, p, parent=None):
41 QtGui.QWidget.__init__(self, parent)
42 self.p = p
43 self.logger = p.logger
44 self.curLyrics = ''
46 self.txtView = QtGui.QTextEdit(self)
47 self.txtView.setReadOnly(True)
49 self.setLayout(QtGui.QVBoxLayout())
50 self.layout().setSpacing(0)
51 self.layout().setMargin(0)
52 self.layout().addWidget(self.txtView)
54 self.connect(self.p, QtCore.SIGNAL('new_lyrics_fetched'), self.set_lyrics)
56 def set_lyrics(self, song, lyrics, flags = 0):
57 if not song:
58 return self.txtView.clear()
60 if song != self.p.mpclient.current_song():
61 return
63 self.txtView.clear()
64 if song:
65 self.txtView.insertHtml('<b>%s</b>\n<br /><u>%s</u><br />'\
66 '<br />\n\n'%(song.title(), song.artist()))
67 else:
68 return
70 if lyrics:
71 self.logger.info('Setting new lyrics.')
72 self.txtView.insertPlainText(lyrics.decode('utf-8'))
73 else:
74 self.logger.info('Lyrics not found.')
75 self.txtView.insertPlainText('Lyrics not found.')
77 class Lyrics(Plugin):
78 o = None
79 sites = []
80 lyrics_dir = None
81 lyrics_path = None
83 DEFAULTS = {'sites' : QtCore.QStringList(['lyricwiki', 'animelyrics']), 'lyricdir' : '$musicdir/$songdir',
84 'lyricname' : '.lyrics_nephilim_$artist_$album_$title', 'store' : True}
86 def _load(self):
87 self.o = wgLyrics(self)
88 for site in _available_sites:
89 if site in self.settings().value('%s/sites'%self.name).toStringList():
90 self.sites.append(site)
91 self.connect(self.mpclient, QtCore.SIGNAL('song_changed'), self.refresh)
92 def _unload(self):
93 self.o = None
94 self.sites = []
95 self.disconnect(self.mpclient, QtCore.SIGNAL('song_changed'), self.refresh)
96 def info(self):
97 return "Show (and fetch) the lyrics of the currently playing song."
99 def _get_dock_widget(self):
100 return self._create_dock(self.o)
102 class FetchThread(QtCore.QThread):
103 def __init__(self, parent, fetch_func, song):
104 QtCore.QThread.__init__(self)
105 self.setParent(parent)
106 self.fetch_func = fetch_func
107 self.song = song
108 def run(self):
109 self.fetch_func(self.song)
111 def refresh(self):
112 self.logger.info('Autorefreshing lyrics.')
113 song = self.mpclient.current_song()
114 if not song:
115 return self.o.set_lyrics(None, None)
117 (self.lyrics_dir, self.lyrics_path) = misc.generate_metadata_path(song,
118 self.settings().value(self.name + '/lyricdir').toString(),
119 self.settings().value(self.name + '/lyricname').toString())
120 try:
121 self.logger.info('Trying to read lyrics from file %s.'%self.lyrics_path)
122 file = open(self.lyrics_path, 'r')
123 lyrics = file.read()
124 file.close()
125 if lyrics:
126 return self.emit(QtCore.SIGNAL('new_lyrics_fetched'), song, lyrics)
127 except IOError, e:
128 self.logger.info('Error reading lyrics file: %s.'%e)
131 thread = self.FetchThread(self, self._fetch_lyrics, song)
132 thread.start()
134 def _fetch_lyrics(self, song):
135 self.logger.info('Trying to download lyrics from internet.')
136 lyrics = None
137 for site in self.sites:
138 self.logger.info('Trying %s.'%site)
139 lyrics = eval('self.fetch_%s(song)'%site)
140 if lyrics:
141 try:
142 file = open(self.lyrics_path, 'w')
143 file.write(lyrics)
144 file.close()
145 except IOError, e:
146 self.logger.error('Error saving lyrics: %s'%e)
147 return self.emit(QtCore.SIGNAL('new_lyrics_fetched'), song, lyrics)
149 self.emit(QtCore.SIGNAL('new_lyrics_fetched'), song, None)
151 def fetch_lyricwiki(self, song):
152 url = 'http://lyricwiki.org/api.php?%s' %urllib.urlencode({'func':'getSong',
153 'artist':song.artist().encode('utf-8'), 'song':song.title().encode('utf-8'),
154 'fmt':'xml'})
155 try:
156 # get url for lyrics
157 tree = etree.HTML(urllib.urlopen(url).read())
158 if tree.find('.//lyrics').text == 'Not found':
159 return None
160 #get page with lyrics and change <br> tags to newlines
161 url = tree.find('.//url').text
162 page = re.sub('<br>|<br/>|<br />', '\n', urllib.urlopen(url).read())
163 html = etree.HTML(page)
164 lyrics = ''
165 for elem in html.iterfind('.//div'):
166 if elem.get('class') == 'lyricbox':
167 lyrics += etree.tostring(elem, method = 'text', encoding = 'utf-8')
168 return lyrics
169 except socket.error, e:
170 self.logger.error('Error downloading lyrics from LyricWiki: %s.'%e)
171 return None
173 def fetch_animelyrics(self, song):
174 url = 'http://www.animelyrics.com/search.php?%s'%urllib.urlencode({'q':song.artist().encode('utf-8'),
175 't':'performer'})
176 try:
177 #get url for lyrics
178 self.logger.info('Searching Animelyrics: %s.'%url)
179 tree = etree.HTML(urllib.urlopen(url).read())
180 url = None
181 for elem in tree.iterfind('.//a'):
182 if ('href' in elem.attrib) and elem.text and (song.title() in elem.text):
183 url = 'http://www.animelyrics.com/%s'%elem.get('href')
184 print url
185 if not url:
186 return None
187 #get lyrics
188 self.logger.info('Found song URL: %s.'%url)
189 tree = etree.HTML(urllib.urlopen(url).read())
190 ret = ''
191 for elem in tree.iterfind('.//pre'):
192 if elem.get('class') == 'lyrics':
193 ret += '%s\n\n'%etree.tostring(elem, method = 'text', encoding = 'utf-8')
194 return ret
195 except socket.error, e:
196 self.logger.error('Error downloading lyrics from Animelyrics: %s.'%e)
197 return None
198 except AttributeError:
199 # lyrics not found
200 return None
202 class SettingsWidgetLyrics(Plugin.SettingsWidget):
203 lyricdir = None
204 lyricname = None
205 store = None
207 def __init__(self, plugin):
208 Plugin.SettingsWidget.__init__(self, plugin)
209 self.settings().beginGroup(self.plugin.name)
212 # store lyrics groupbox
213 self.store = QtGui.QGroupBox('Store lyrics.')
214 self.store.setToolTip('Should %s store its own copy of lyrics?'%misc.APPNAME)
215 self.store.setCheckable(True)
216 self.store.setChecked(self.settings().value('store').toBool())
217 self.store.setLayout(QtGui.QGridLayout())
219 # paths to lyrics
220 self.lyricdir = QtGui.QLineEdit(self.settings().value('lyricdir').toString())
221 self.lyricdir.setToolTip('Where should %s store lyrics.\n'
222 '$musicdir will be expanded to path to MPD music library (as set by user)\n'
223 '$songdir will be expanded to path to the song (relative to $musicdir\n'
224 'other tags same as in lyricname'
225 %misc.APPNAME)
226 self.lyricname = QtGui.QLineEdit(self.settings().value('lyricname').toString())
227 self.lyricname.setToolTip('Filename for %s lyricsfiles.\n'
228 'All tags supported by MPD will be expanded to their\n'
229 'values for current song, e.g. $title, $track, $artist,\n'
230 '$album, $genre etc.'%misc.APPNAME)
231 self.store.layout().addWidget(QtGui.QLabel('Lyrics directory'), 0, 0)
232 self.store.layout().addWidget(self.lyricdir, 0, 1)
233 self.store.layout().addWidget(QtGui.QLabel('Lyrics filename'), 1, 0)
234 self.store.layout().addWidget(self.lyricname, 1, 1)
236 self.setLayout(QtGui.QVBoxLayout())
237 self.layout().addWidget(self.store)
239 self.settings().endGroup()
241 def save_settings(self):
242 self.settings().beginGroup(self.plugin.name)
243 self.settings().setValue('lyricdir', QVariant(self.lyricdir.text()))
244 self.settings().setValue('lyricname', QVariant(self.lyricname.text()))
245 self.settings().setValue('store', QVariant(self.store.isChecked()))
246 self.settings().endGroup()
247 self.plugin.refresh()
249 def get_settings_widget(self):
250 return self.SettingsWidgetLyrics(self)