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
23 from lxml
import etree
25 from ..plugin
import Plugin
28 class LyricsWidget(QtGui
.QWidget
):
33 plugin
= None # plugin
37 __text_view
= None # text-object
42 def __init__(self
, plugin
):
43 QtGui
.QWidget
.__init
__(self
)
45 self
.logger
= plugin
.logger
48 self
.__label
= QtGui
.QLabel(self
)
49 self
.__label
.setWordWrap(True)
52 self
.__text
_view
= QtGui
.QTextEdit(self
)
53 self
.__text
_view
.setReadOnly(True)
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
)
81 def set_lyrics(self
, song
, lyrics
, flags
= 0):
82 """Set currently displayed lyrics for song. flags parameter is
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():
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()))
96 self
.logger
.info('Setting new lyrics.')
97 self
.__text
_view
.insertPlainText(lyrics
.decode('utf-8'))
98 self
.lyrics_loaded
= True
100 self
.logger
.info('Lyrics not found.')
101 self
.__text
_view
.insertPlainText('Lyrics not found.')
104 class Lyrics(Plugin
):
107 """A dict of { site name : fetcher object }. The fetcher object provides a
108 fetch(song)Function takes a song and emits finished(song, lyrics) signal
109 when finished. Lyrics is either a python unicode string, QString
110 or None if not found."""
113 DEFAULTS
= {'sites' : QtCore
.QStringList(['lyricwiki', 'animelyrics']), 'lyricdir' : '$musicdir/$songdir',
114 'lyricname' : '.lyrics_nephilim_$artist_$album_$title', 'store' : True}
115 __available_sites
= {}
122 def __init__(self
, parent
, mpclient
, name
):
123 Plugin
.__init
__(self
, parent
, mpclient
, name
)
125 self
.__available
_sites
['lyricwiki'] = self
.FetchLyricwiki
126 self
.__available
_sites
['animelyrics'] = self
.FetchAnimelyrics
128 def __new_lyrics_fetched(self
, song
, lyrics
):
129 self
.logger
.info('Got new lyrics.')
131 if lyrics
and self
.settings().value(self
.name
+ '/store').toBool():
132 self
.save_lyrics_file(lyrics
)
133 return self
.o
.set_lyrics(song
, lyrics
)
134 elif self
.__results
>= len(self
.__fetchers
) and not self
.o
.lyrics_loaded
:
135 self
.o
.set_lyrics(song
, None)
137 class Fetcher(QtCore
.QObject
):
138 """A basic class for lyrics fetchers. Provides a fetch(song) function,
139 emits a finished(song, lyrics) signal when done; lyrics is either a QString,
140 Python unicode string or None if not found."""
145 nam
= None # NetworkAccessManager
146 srep
= None # search results NetworkReply
147 lrep
= None # lyrics page NetworkReply
148 song
= None # current song
150 def __init__(self
, plugin
):
151 QtCore
.QObject
.__init
__(self
, plugin
)
153 self
.nam
= QtNetwork
.QNetworkAccessManager()
154 self
.logger
= plugin
.logger
155 def fetch(self
, song
):
156 """Reimplement this in subclasses."""
159 def finish(self
, lyrics
= None):
160 """A private convenience function to clean up and emit finished().
161 Feel free to reimplement/not use it."""
164 self
.emit(QtCore
.SIGNAL('finished'), self
.song
, lyrics
)
167 class FetchLyricwiki(Fetcher
):
169 def fetch(self
, song
):
170 # abort any existing connections
179 url
= QtCore
.QUrl('http://lyricwiki.org/api.php')
180 url
.setQueryItems([('func', 'getSong'), ('artist', song
.artist()),
181 ('song', song
.title()), ('fmt', 'xml')])
183 self
.logger
.info('Searching Lyricwiki: %s.'%url
)
184 self
.srep
= self
.nam
.get(QtNetwork
.QNetworkRequest(url
))
185 self
.connect(self
.srep
, QtCore
.SIGNAL('finished()'), self
.__handle
_search
_res
)
187 def __handle_search_res(self
):
189 xml
= QtCore
.QXmlStreamReader(self
.srep
)
190 while not xml
.atEnd():
191 token
= xml
.readNext()
192 if token
== QtCore
.QXmlStreamReader
.StartElement
:
193 if xml
.name() == 'url':
194 url
= QtCore
.QUrl() # the url is already percent-encoded
195 url
.setEncodedUrl(xml
.readElementText().toLatin1())
196 elif xml
.name() == 'lyrics' and xml
.readElementText() == 'Not found':
200 self
.logger
.error('Error parsing seach results.%s'%xml
.errorString())
203 self
.logger
.error('Didn\'t find the URL in Lyricwiki search results.')
205 self
.logger
.info('Found Lyricwiki song URL: %s.'%url
)
207 self
.lrep
= self
.nam
.get(QtNetwork
.QNetworkRequest(url
))
208 self
.connect(self
.lrep
, QtCore
.SIGNAL('finished()'), self
.__handle
_lyrics
)
210 def __handle_lyrics(self
):
211 #TODO this should use Qt xml functions too
213 page
= unicode(self
.lrep
.readAll(), encoding
= 'utf-8')
214 page
= re
.sub('<br>|<br/>|<br />', '\n', page
)
215 html
= etree
.HTML(page
)
216 for elem
in html
.iterfind('.//div'):
217 if elem
.get('class') == 'lyricbox':
218 lyrics
+= etree
.tostring(elem
, method
= 'text', encoding
= 'utf-8')
221 class FetchAnimelyrics(Fetcher
):
223 def fetch(self
, song
):
224 # abort any existing connections
233 url
= QtCore
.QUrl('http://www.animelyrics.com/search.php')
234 url
.setQueryItems([('t', 'performer'), ('q', self
.song
.artist())])
236 self
.logger
.info('Searching Animelyrics: %s.'%url
)
237 self
.srep
= self
.nam
.get(QtNetwork
.QNetworkRequest(url
))
238 self
.connect(self
.srep
, QtCore
.SIGNAL('finished()'), self
.__handle
_search
_res
)
240 def __handle_search_res(self
):
241 # TODO use Qt xml functions
242 tree
= etree
.HTML(unicode(self
.srep
.readAll(), encoding
= 'utf-8', errors
='ignore'))
246 for elem
in tree
.iterfind('.//a'):
247 if ('href' in elem
.attrib
) and elem
.text
and (self
.song
.title() in elem
.text
):
248 url
= QtCore
.QUrl('http://www.animelyrics.com/%s'%elem
.get('href'))
251 self
.logger
.info('Didn\'t find the URL in Animelyrics search results.')
253 self
.logger
.info('Found Animelyrics song URL: %s.'%url
)
255 self
.lrep
= self
.nam
.get(QtNetwork
.QNetworkRequest(url
))
256 self
.connect(self
.lrep
, QtCore
.SIGNAL('finished()'), self
.__handle
_lyrics
)
258 def __handle_lyrics(self
):
260 tree
= etree
.HTML(unicode(self
.lrep
.readAll(), encoding
= 'utf-8'))
261 for elem
in tree
.iterfind('.//pre'):
262 if elem
.get('class') == 'lyrics':
263 lyrics
+= '%s\n\n'%etree
.tostring(elem
, method
= 'text', encoding
= 'utf-8')
267 class SettingsWidgetLyrics(Plugin
.SettingsWidget
):
272 def __init__(self
, plugin
):
273 Plugin
.SettingsWidget
.__init
__(self
, plugin
)
274 self
.settings().beginGroup(self
.plugin
.name
)
277 # store lyrics groupbox
278 self
.store
= QtGui
.QGroupBox('Store lyrics.')
279 self
.store
.setToolTip('Should %s store its own copy of lyrics?'%misc
.APPNAME
)
280 self
.store
.setCheckable(True)
281 self
.store
.setChecked(self
.settings().value('store').toBool())
282 self
.store
.setLayout(QtGui
.QGridLayout())
285 self
.lyricdir
= QtGui
.QLineEdit(self
.settings().value('lyricdir').toString())
286 self
.lyricdir
.setToolTip('Where should %s store lyrics.\n'
287 '$musicdir will be expanded to path to MPD music library (as set by user)\n'
288 '$songdir will be expanded to path to the song (relative to $musicdir\n'
289 'other tags same as in lyricname'
291 self
.lyricname
= QtGui
.QLineEdit(self
.settings().value('lyricname').toString())
292 self
.lyricname
.setToolTip('Filename for %s lyricsfiles.\n'
293 'All tags supported by MPD will be expanded to their\n'
294 'values for current song, e.g. $title, $track, $artist,\n'
295 '$album, $genre etc.'%misc
.APPNAME
)
296 self
.store
.layout().addWidget(QtGui
.QLabel('Lyrics directory'), 0, 0)
297 self
.store
.layout().addWidget(self
.lyricdir
, 0, 1)
298 self
.store
.layout().addWidget(QtGui
.QLabel('Lyrics filename'), 1, 0)
299 self
.store
.layout().addWidget(self
.lyricname
, 1, 1)
301 self
.setLayout(QtGui
.QVBoxLayout())
302 self
.layout().addWidget(self
.store
)
304 self
.settings().endGroup()
306 def save_settings(self
):
307 self
.settings().beginGroup(self
.plugin
.name
)
308 self
.settings().setValue('lyricdir', QVariant(self
.lyricdir
.text()))
309 self
.settings().setValue('lyricname', QVariant(self
.lyricname
.text()))
310 self
.settings().setValue('store', QVariant(self
.store
.isChecked()))
311 self
.settings().endGroup()
312 self
.plugin
.refresh()
316 for site
in self
.__available
_sites
:
317 if site
in self
.settings().value('%s/sites'%self
.name
).toStringList():
318 self
.__fetchers
[site
] = self
.__available
_sites
[site
](self
)
319 self
.o
= LyricsWidget(self
)
320 for fetcher
in self
.__fetchers
:
321 self
.connect(self
.__fetchers
[fetcher
], QtCore
.SIGNAL('finished'), self
.__new
_lyrics
_fetched
)
322 self
.connect(self
.mpclient
, QtCore
.SIGNAL('song_changed'), self
.refresh
)
326 self
.disconnect(self
.mpclient
, QtCore
.SIGNAL('song_changed'), self
.refresh
)
328 return "Show (and fetch) the lyrics of the currently playing song."
330 def _get_dock_widget(self
):
331 return self
._create
_dock
(self
.o
)
334 """Attempt to automatically get lyrics first from a file, then from the internet."""
335 self
.logger
.info('Autorefreshing lyrics.')
337 self
.o
.lyrics_loaded
= False
338 song
= self
.mpclient
.current_song()
340 self
.__lyrics
_dir
= ''
341 self
.__lyrics
_path
= ''
342 return self
.o
.set_lyrics(None, None)
344 (self
.__lyrics
_dir
, self
.__lyrics
_path
) = misc
.generate_metadata_path(song
,
345 self
.settings().value(self
.name
+ '/lyricdir').toString(),
346 self
.settings().value(self
.name
+ '/lyricname').toString())
348 self
.logger
.info('Trying to read lyrics from file %s.'%self
.__lyrics
_path
)
349 file = open(self
.__lyrics
_path
, 'r')
353 return self
.o
.set_lyrics(song
, lyrics
)
355 self
.logger
.info('Error reading lyrics file: %s.'%e)
357 for fetcher
in self
.__fetchers
.values():
360 def save_lyrics_file(self
, lyrics
, path
= None):
361 """Save lyrics to a file specified in path.
362 If path is None, then a default value is used."""
363 self
.logger
.info('Saving lyrics...')
366 file = open(path
, 'w')
368 file = open(self
.__lyrics
_path
, 'w')
371 self
.logger
.info('Lyrics successfully saved.')
373 self
.logger
.error('Error writing lyrics: %s', e
)
375 def del_lyrics_file(self
, song
= None):
376 """Delete a lyrics file for song. If song is not specified
377 current song is used."""
379 path
= self
.__lyrics
_path
381 path
= misc
.generate_metadata_path(song
, self
.settings().value(self
.name
+ '/lyricdir').toString(),
382 self
.settings().value(self
.name
+ '/lyricname').toString())
387 self
.logger
.error('Error removing lyrics file %s: %s'%(path
, e
))
389 def get_settings_widget(self
):
390 return self
.SettingsWidgetLyrics(self
)