Lyrics: cosmetics
[nephilim.git] / nephilim / plugins / Lyrics.py
blob91ee4d8dfb51b1371ccd941b7d5181fdfe954373
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
22 import os
23 import re
24 import urllib
25 from lxml import etree
27 from ..plugin import Plugin
28 from .. import misc
30 class LyricsWidget(QtGui.QWidget):
31 # public, read-only
32 plugin = None # plugin
33 logger = None
35 # private
36 __text_view = None # text-object
37 __toolbar = None
38 __label = None
39 def __init__(self, plugin):
40 QtGui.QWidget.__init__(self)
41 self.plugin = plugin
42 self.logger = plugin.logger
43 self.curLyrics = ''
45 self.__label = QtGui.QLabel(self)
47 # add text area
48 self.__text_view = QtGui.QTextEdit(self)
49 self.__text_view.setReadOnly(True)
51 # add toolbar
52 self.__toolbar = QtGui.QToolBar('Lyrics toolbar', self)
53 self.__toolbar.setOrientation(QtCore.Qt.Vertical)
55 self.__toolbar.addAction(QtGui.QIcon('gfx/refresh.png'), 'Refresh lyrics', self.plugin.refresh)
56 edit = self.__toolbar.addAction(QtGui.QIcon('gfx/edit.png'), 'Edit lyrics')
57 edit.setCheckable(True)
58 edit.connect(edit, QtCore.SIGNAL('toggled(bool)'), self.__toggle_editable)
60 self.__toolbar.addAction(QtGui.QIcon('gfx/save.png'), 'Save lyrics', self.__save_lyrics)
61 self.__toolbar.addAction(QtGui.QIcon('gfx/delete.png'), 'Delete stored file', self.plugin.del_lyrics_file)
63 self.setLayout(QtGui.QGridLayout())
64 self.layout().setSpacing(0)
65 self.layout().setMargin(0)
66 self.layout().addWidget(self.__toolbar, 0, 0, -1, 1, QtCore.Qt.AlignTop)
67 self.layout().addWidget(self.__label, 0, 1)
68 self.layout().addWidget(self.__text_view, 1, 1)
70 self.connect(self.plugin, QtCore.SIGNAL('new_lyrics_fetched'), self.set_lyrics)
72 def set_lyrics(self, song, lyrics, flags = 0):
73 """Set currently displayed lyrics for song. flags parameter is
74 unused now."""
75 if not song:
76 self.__label.clear()
77 return self.__text_view.clear()
79 # a late thread might call this for a previous song
80 if song != self.plugin.mpclient.current_song():
81 return
83 self.__text_view.clear()
84 self.__label.setText('<b>%s</b> by <u>%s</u> on <u>%s</u>'\
85 %(song.title(), song.artist(), song.album()))
86 if lyrics:
87 self.logger.info('Setting new lyrics.')
88 self.__text_view.insertPlainText(lyrics.decode('utf-8'))
89 else:
90 self.logger.info('Lyrics not found.')
91 self.__text_view.insertPlainText('Lyrics not found.')
93 def __save_lyrics(self):
94 self.plugin.save_lyrics_file(unicode(self.__text_view.toPlainText()).encode('utf-8'))
96 def __toggle_editable(self, val):
97 self.__text_view.setReadOnly(not val)
99 class Lyrics(Plugin):
100 # public, read-only
101 o = None
102 """A dict of { site name : function }. Function takes a song and returns lyrics
103 as a python string or None if not found."""
104 sites = {}
106 # private
107 DEFAULTS = {'sites' : QtCore.QStringList(['lyricwiki', 'animelyrics']), 'lyricdir' : '$musicdir/$songdir',
108 'lyricname' : '.lyrics_nephilim_$artist_$album_$title', 'store' : True}
109 __available_sites = {}
110 __lyrics_dir = None
111 __lyrics_path = None
113 def __init__(self, parent, mpclient, name):
114 Plugin.__init__(self, parent, mpclient, name)
116 self.__available_sites['lyricwiki'] = self.__fetch_lyricwiki
117 self.__available_sites['animelyrics'] = self.__fetch_animelyrics
119 def _load(self):
120 self.o = LyricsWidget(self)
121 for site in self.__available_sites:
122 if site in self.settings().value('%s/sites'%self.name).toStringList():
123 self.sites[site] = self.__available_sites[site]
124 self.connect(self.mpclient, QtCore.SIGNAL('song_changed'), self.refresh)
125 def _unload(self):
126 self.o = None
127 self.sites = []
128 self.disconnect(self.mpclient, QtCore.SIGNAL('song_changed'), self.refresh)
129 def info(self):
130 return "Show (and fetch) the lyrics of the currently playing song."
132 def _get_dock_widget(self):
133 return self._create_dock(self.o)
135 class FetchThread(QtCore.QThread):
136 def __init__(self, parent, fetch_func, song):
137 QtCore.QThread.__init__(self)
138 self.setParent(parent)
139 self.fetch_func = fetch_func
140 self.song = song
141 def run(self):
142 self.fetch_func(self.song)
144 def refresh(self):
145 """Attempt to automatically get lyrics first from a file, then from the internet."""
146 self.logger.info('Autorefreshing lyrics.')
147 song = self.mpclient.current_song()
148 if not song:
149 return self.o.set_lyrics(None, None)
151 (self.__lyrics_dir, self.__lyrics_path) = misc.generate_metadata_path(song,
152 self.settings().value(self.name + '/lyricdir').toString(),
153 self.settings().value(self.name + '/lyricname').toString())
154 try:
155 self.logger.info('Trying to read lyrics from file %s.'%self.__lyrics_path)
156 file = open(self.__lyrics_path, 'r')
157 lyrics = file.read()
158 file.close()
159 if lyrics:
160 return self.emit(QtCore.SIGNAL('new_lyrics_fetched'), song, lyrics)
161 except IOError, e:
162 self.logger.info('Error reading lyrics file: %s.'%e)
165 thread = self.FetchThread(self, self.__fetch_lyrics, song)
166 thread.start()
168 def save_lyrics_file(self, lyrics, path = None):
169 """Save lyrics to a file specified in path.
170 If path is None, then a default value is used."""
171 self.logger.info('Saving lyrics...')
172 try:
173 if path:
174 file = open(path, 'w')
175 else:
176 file = open(self.__lyrics_path, 'w')
177 file.write(lyrics)
178 file.close()
179 self.logger.info('Lyrics successfully saved.')
180 except IOError, e:
181 self.logger.error('Error writing lyrics: %s', e)
183 def del_lyrics_file(self, song = None):
184 """Delete a lyrics file for song. If song is not specified
185 current song is used."""
186 if not song:
187 path = self.__lyrics_path
188 else:
189 path = misc.generate_metadata_path(song, self.settings().value(self.name + '/lyricdir').toString(),
190 self.settings().value(self.name + '/lyricname').toString())
192 try:
193 os.remove(path)
194 except IOError, e:
195 self.logger.error('Error removing lyrics file %s: %s'%(path, e))
197 def __fetch_lyrics(self, song):
198 self.logger.info('Trying to download lyrics from internet.')
199 lyrics = None
200 for site in self.sites:
201 self.logger.info('Trying %s.'%site)
202 lyrics = self.sites[site](song)
203 if lyrics:
204 if self.settings().value(self.name + '/store').toBool():
205 self.save_lyrics_file(lyrics)
206 return self.emit(QtCore.SIGNAL('new_lyrics_fetched'), song, lyrics)
208 self.emit(QtCore.SIGNAL('new_lyrics_fetched'), song, None)
210 def __fetch_lyricwiki(self, song):
211 url = 'http://lyricwiki.org/api.php?%s' %urllib.urlencode({'func':'getSong',
212 'artist':song.artist().encode('utf-8'), 'song':song.title().encode('utf-8'),
213 'fmt':'xml'})
214 try:
215 # get url for lyrics
216 tree = etree.HTML(urllib.urlopen(url).read())
217 if tree.find('.//lyrics').text == 'Not found':
218 return None
219 #get page with lyrics and change <br> tags to newlines
220 url = tree.find('.//url').text
221 page = re.sub('<br>|<br/>|<br />', '\n', urllib.urlopen(url).read())
222 html = etree.HTML(page)
223 lyrics = ''
224 for elem in html.iterfind('.//div'):
225 if elem.get('class') == 'lyricbox':
226 lyrics += etree.tostring(elem, method = 'text', encoding = 'utf-8')
227 return lyrics
228 except (socket.error, IOError), e:
229 self.logger.error('Error downloading lyrics from LyricWiki: %s.'%e)
230 return None
232 def __fetch_animelyrics(self, song):
233 url = 'http://www.animelyrics.com/search.php?%s'%urllib.urlencode({'q':song.artist().encode('utf-8'),
234 't':'performer'})
235 try:
236 #get url for lyrics
237 self.logger.info('Searching Animelyrics: %s.'%url)
238 tree = etree.HTML(urllib.urlopen(url).read())
239 url = None
240 for elem in tree.iterfind('.//a'):
241 if ('href' in elem.attrib) and elem.text and (song.title() in elem.text):
242 url = 'http://www.animelyrics.com/%s'%elem.get('href')
243 if not url:
244 return None
245 #get lyrics
246 self.logger.info('Found song URL: %s.'%url)
247 tree = etree.HTML(urllib.urlopen(url).read())
248 ret = ''
249 for elem in tree.iterfind('.//pre'):
250 if elem.get('class') == 'lyrics':
251 ret += '%s\n\n'%etree.tostring(elem, method = 'text', encoding = 'utf-8')
252 return ret
253 except socket.error, e:
254 self.logger.error('Error downloading lyrics from Animelyrics: %s.'%e)
255 return None
256 except AttributeError:
257 # lyrics not found
258 return None
260 class SettingsWidgetLyrics(Plugin.SettingsWidget):
261 lyricdir = None
262 lyricname = None
263 store = None
265 def __init__(self, plugin):
266 Plugin.SettingsWidget.__init__(self, plugin)
267 self.settings().beginGroup(self.plugin.name)
270 # store lyrics groupbox
271 self.store = QtGui.QGroupBox('Store lyrics.')
272 self.store.setToolTip('Should %s store its own copy of lyrics?'%misc.APPNAME)
273 self.store.setCheckable(True)
274 self.store.setChecked(self.settings().value('store').toBool())
275 self.store.setLayout(QtGui.QGridLayout())
277 # paths to lyrics
278 self.lyricdir = QtGui.QLineEdit(self.settings().value('lyricdir').toString())
279 self.lyricdir.setToolTip('Where should %s store lyrics.\n'
280 '$musicdir will be expanded to path to MPD music library (as set by user)\n'
281 '$songdir will be expanded to path to the song (relative to $musicdir\n'
282 'other tags same as in lyricname'
283 %misc.APPNAME)
284 self.lyricname = QtGui.QLineEdit(self.settings().value('lyricname').toString())
285 self.lyricname.setToolTip('Filename for %s lyricsfiles.\n'
286 'All tags supported by MPD will be expanded to their\n'
287 'values for current song, e.g. $title, $track, $artist,\n'
288 '$album, $genre etc.'%misc.APPNAME)
289 self.store.layout().addWidget(QtGui.QLabel('Lyrics directory'), 0, 0)
290 self.store.layout().addWidget(self.lyricdir, 0, 1)
291 self.store.layout().addWidget(QtGui.QLabel('Lyrics filename'), 1, 0)
292 self.store.layout().addWidget(self.lyricname, 1, 1)
294 self.setLayout(QtGui.QVBoxLayout())
295 self.layout().addWidget(self.store)
297 self.settings().endGroup()
299 def save_settings(self):
300 self.settings().beginGroup(self.plugin.name)
301 self.settings().setValue('lyricdir', QVariant(self.lyricdir.text()))
302 self.settings().setValue('lyricname', QVariant(self.lyricname.text()))
303 self.settings().setValue('store', QVariant(self.store.isChecked()))
304 self.settings().endGroup()
305 self.plugin.refresh()
307 def get_settings_widget(self):
308 return self.SettingsWidgetLyrics(self)