summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Khirnov <wyskas@gmail.com>2009-08-19 06:50:30 +0200
committerAnton Khirnov <wyskas@gmail.com>2009-08-19 09:38:34 +0200
commitbec4257daa05a7c71678330e7f9747ecdfe5b2ca (patch)
treeb16a78ab53a9469181a5f3b028066d697bb864a4
parentad26c5faa9ce8d15f369bc1b0e4b27f358536e80 (diff)
Lyrics: rewrite the system of fetchers to be more flexible.
also use Qt's networking module instead of urllib - which removes the need for explicit multithreading here.
-rw-r--r--TODO1
-rw-r--r--nephilim/plugins/Lyrics.py254
2 files changed, 165 insertions, 90 deletions
diff --git a/TODO b/TODO
index 379a66e..8271283 100644
--- a/TODO
+++ b/TODO
@@ -13,3 +13,4 @@ other TODO:
- split library by letters (like Amarok)
- icons could use some improving
- waste less memory (library and playlist are main suspects)
+- Lyrics - should use only Qt's xml functions and allow user to select lyrics
diff --git a/nephilim/plugins/Lyrics.py b/nephilim/plugins/Lyrics.py
index 381009a..960c400 100644
--- a/nephilim/plugins/Lyrics.py
+++ b/nephilim/plugins/Lyrics.py
@@ -15,22 +15,23 @@
# along with Nephilim. If not, see <http://www.gnu.org/licenses/>.
#
-from PyQt4 import QtGui, QtCore
+from PyQt4 import QtGui, QtCore, QtNetwork
from PyQt4.QtCore import QVariant
-import socket
import os
import re
-import urllib
from lxml import etree
from ..plugin import Plugin
from .. import misc
class LyricsWidget(QtGui.QWidget):
+ #public
+ lyrics_loaded = None
+
# public, read-only
- plugin = None # plugin
- logger = None
+ plugin = None # plugin
+ logger = None
# private
__text_view = None # text-object
@@ -68,8 +69,6 @@ class LyricsWidget(QtGui.QWidget):
self.layout().addWidget(self.__label, 0, 1)
self.layout().addWidget(self.__text_view, 1, 1)
- self.connect(self.plugin, QtCore.SIGNAL('new_lyrics_fetched'), self.set_lyrics)
-
def set_lyrics(self, song, lyrics, flags = 0):
"""Set currently displayed lyrics for song. flags parameter is
unused now."""
@@ -87,6 +86,7 @@ class LyricsWidget(QtGui.QWidget):
if lyrics:
self.logger.info('Setting new lyrics.')
self.__text_view.insertPlainText(lyrics.decode('utf-8'))
+ self.lyrics_loaded = True
else:
self.logger.info('Lyrics not found.')
self.__text_view.insertPlainText('Lyrics not found.')
@@ -100,32 +100,37 @@ class LyricsWidget(QtGui.QWidget):
class Lyrics(Plugin):
# public, read-only
o = None
- """A dict of { site name : function }. Function takes a song and returns lyrics
- as a python string or None if not found."""
- sites = {}
+ """A dict of { site name : fetcher object }. The fetcher object provides a
+ fetch(song)Function takes a song and emits finished(song, lyrics) signal
+ when finished. Lyrics is either a python unicode string, QString
+ or None if not found."""
# private
DEFAULTS = {'sites' : QtCore.QStringList(['lyricwiki', 'animelyrics']), 'lyricdir' : '$musicdir/$songdir',
'lyricname' : '.lyrics_nephilim_$artist_$album_$title', 'store' : True}
__available_sites = {}
+ __fetchers = {}
+ __results = 0
__lyrics_dir = None
__lyrics_path = None
def __init__(self, parent, mpclient, name):
Plugin.__init__(self, parent, mpclient, name)
- self.__available_sites['lyricwiki'] = self.__fetch_lyricwiki
- self.__available_sites['animelyrics'] = self.__fetch_animelyrics
+ self.__available_sites['lyricwiki'] = self.FetchLyricwiki
+ self.__available_sites['animelyrics'] = self.FetchAnimelyrics
def _load(self):
- self.o = LyricsWidget(self)
for site in self.__available_sites:
if site in self.settings().value('%s/sites'%self.name).toStringList():
- self.sites[site] = self.__available_sites[site]
+ self.__fetchers[site] = self.__available_sites[site](self)
+ self.o = LyricsWidget(self)
+ for fetcher in self.__fetchers:
+ self.connect(self.__fetchers[fetcher], QtCore.SIGNAL('finished'), self.__new_lyrics_fetched)
self.connect(self.mpclient, QtCore.SIGNAL('song_changed'), self.refresh)
def _unload(self):
self.o = None
- self.sites = []
+ self.sites = {}
self.disconnect(self.mpclient, QtCore.SIGNAL('song_changed'), self.refresh)
def info(self):
return "Show (and fetch) the lyrics of the currently playing song."
@@ -133,18 +138,11 @@ class Lyrics(Plugin):
def _get_dock_widget(self):
return self._create_dock(self.o)
- class FetchThread(QtCore.QThread):
- def __init__(self, parent, fetch_func, song):
- QtCore.QThread.__init__(self)
- self.setParent(parent)
- self.fetch_func = fetch_func
- self.song = song
- def run(self):
- self.fetch_func(self.song)
-
def refresh(self):
"""Attempt to automatically get lyrics first from a file, then from the internet."""
self.logger.info('Autorefreshing lyrics.')
+ self.__results = 0
+ self.o.lyrics_loaded = False
song = self.mpclient.current_song()
if not song:
self.__lyrics_dir = ''
@@ -160,13 +158,12 @@ class Lyrics(Plugin):
lyrics = file.read()
file.close()
if lyrics:
- return self.emit(QtCore.SIGNAL('new_lyrics_fetched'), song, lyrics)
+ return self.o.set_lyrics(song, lyrics)
except IOError, e:
self.logger.info('Error reading lyrics file: %s.'%e)
-
- thread = self.FetchThread(self, self.__fetch_lyrics, song)
- thread.start()
+ for fetcher in self.__fetchers.values():
+ fetcher.fetch(song)
def save_lyrics_file(self, lyrics, path = None):
"""Save lyrics to a file specified in path.
@@ -197,68 +194,14 @@ class Lyrics(Plugin):
except IOError, e:
self.logger.error('Error removing lyrics file %s: %s'%(path, e))
- def __fetch_lyrics(self, song):
- self.logger.info('Trying to download lyrics from internet.')
- lyrics = None
- for site in self.sites:
- self.logger.info('Trying %s.'%site)
- lyrics = self.sites[site](song)
- if lyrics:
- if self.settings().value(self.name + '/store').toBool():
- self.save_lyrics_file(lyrics)
- return self.emit(QtCore.SIGNAL('new_lyrics_fetched'), song, lyrics)
-
- self.emit(QtCore.SIGNAL('new_lyrics_fetched'), song, None)
-
- def __fetch_lyricwiki(self, song):
- url = 'http://lyricwiki.org/api.php?%s' %urllib.urlencode({'func':'getSong',
- 'artist':song.artist().encode('utf-8'), 'song':song.title().encode('utf-8'),
- 'fmt':'xml'})
- try:
- # get url for lyrics
- tree = etree.HTML(urllib.urlopen(url).read())
- if tree.find('.//lyrics').text == 'Not found':
- return None
- #get page with lyrics and change <br> tags to newlines
- url = tree.find('.//url').text
- page = re.sub('<br>|<br/>|<br />', '\n', urllib.urlopen(url).read())
- html = etree.HTML(page)
- lyrics = ''
- for elem in html.iterfind('.//div'):
- if elem.get('class') == 'lyricbox':
- lyrics += etree.tostring(elem, method = 'text', encoding = 'utf-8')
- return lyrics
- except (socket.error, IOError), e:
- self.logger.error('Error downloading lyrics from LyricWiki: %s.'%e)
- return None
-
- def __fetch_animelyrics(self, song):
- url = 'http://www.animelyrics.com/search.php?%s'%urllib.urlencode({'q':song.artist().encode('utf-8'),
- 't':'performer'})
- try:
- #get url for lyrics
- self.logger.info('Searching Animelyrics: %s.'%url)
- tree = etree.HTML(urllib.urlopen(url).read())
- url = None
- for elem in tree.iterfind('.//a'):
- if ('href' in elem.attrib) and elem.text and (song.title() in elem.text):
- url = 'http://www.animelyrics.com/%s'%elem.get('href')
- if not url:
- return None
- #get lyrics
- self.logger.info('Found song URL: %s.'%url)
- tree = etree.HTML(urllib.urlopen(url).read())
- ret = ''
- for elem in tree.iterfind('.//pre'):
- if elem.get('class') == 'lyrics':
- ret += '%s\n\n'%etree.tostring(elem, method = 'text', encoding = 'utf-8')
- return ret
- except socket.error, e:
- self.logger.error('Error downloading lyrics from Animelyrics: %s.'%e)
- return None
- except AttributeError:
- # lyrics not found
- return None
+ def __new_lyrics_fetched(self, song, lyrics):
+ self.logger.info('Got new lyrics.')
+ self.__results += 1
+ if lyrics and self.settings().value(self.name + '/store').toBool():
+ self.save_lyrics_file(lyrics)
+ return self.o.set_lyrics(song, lyrics)
+ elif self.__results >= len(self.__fetchers) and not self.o.lyrics_loaded:
+ self.o.set_lyrics(song, None)
class SettingsWidgetLyrics(Plugin.SettingsWidget):
lyricdir = None
@@ -309,3 +252,134 @@ class Lyrics(Plugin):
def get_settings_widget(self):
return self.SettingsWidgetLyrics(self)
+
+ class Fetcher(QtCore.QObject):
+ """A basic class for lyrics fetchers. Provides a fetch(song) function,
+ emits a finished(song, lyrics) signal when done; lyrics is either a QString,
+ Python unicode string or None if not found."""
+ #public, read-only
+ logger = None
+
+ #private
+ nam = None # NetworkAccessManager
+ srep = None # search results NetworkReply
+ lrep = None # lyrics page NetworkReply
+ song = None # current song
+
+ def __init__(self, plugin):
+ QtCore.QObject.__init__(self, plugin)
+
+ self.nam = QtNetwork.QNetworkAccessManager()
+ self.logger = plugin.logger
+ def fetch(self, song):
+ """Reimplement this in subclasses."""
+ pass
+
+ def finish(self, lyrics = None):
+ """A private convenience function to clean up and emit finished().
+ Feel free to reimplement/not use it."""
+ self.srep = None
+ self.lrep = None
+ self.emit(QtCore.SIGNAL('finished'), self.song, lyrics)
+ self.song = None
+
+ class FetchLyricwiki(Fetcher):
+
+ def fetch(self, song):
+ # abort any existing connections
+ if self.srep:
+ self.srep.abort()
+ self.srep = None
+ if self.lrep:
+ self.lrep.abort()
+ self.lrep = None
+ self.song = song
+
+ url = QtCore.QUrl('http://lyricwiki.org/api.php')
+ url.setQueryItems([('func', 'getSong'), ('artist', song.artist()),
+ ('song', song.title()), ('fmt', 'xml')])
+
+ self.logger.info('Searching Lyricwiki: %s.'%url)
+ self.srep = self.nam.get(QtNetwork.QNetworkRequest(url))
+ self.connect(self.srep, QtCore.SIGNAL('finished()'), self.__handle_search_res)
+
+ def __handle_search_res(self):
+ url = None
+ xml = QtCore.QXmlStreamReader(self.srep)
+ while not xml.atEnd():
+ token = xml.readNext()
+ if token == QtCore.QXmlStreamReader.StartElement:
+ if xml.name() == 'url':
+ url = QtCore.QUrl() # the url is already percent-encoded
+ url.setEncodedUrl(xml.readElementText().toLatin1())
+ elif xml.name() == 'lyrics' and xml.readElementText() == 'Not found':
+ xml.clear()
+ return self.finish()
+ if xml.hasError():
+ self.logger.error('Error parsing seach results.%s'%xml.errorString())
+
+ if not url:
+ self.logger.error('Didn\'t find the URL in Lyricwiki search results.')
+ return self.finish()
+ self.logger.info('Found Lyricwiki song URL: %s.'%url)
+
+ self.lrep = self.nam.get(QtNetwork.QNetworkRequest(url))
+ self.connect(self.lrep, QtCore.SIGNAL('finished()'), self.__handle_lyrics)
+
+ def __handle_lyrics(self):
+ #TODO this should use Qt xml functions too
+ lyrics = ''
+ page = unicode(self.lrep.readAll(), encoding = 'utf-8')
+ page = re.sub('<br>|<br/>|<br />', '\n', page)
+ html = etree.HTML(page)
+ for elem in html.iterfind('.//div'):
+ if elem.get('class') == 'lyricbox':
+ lyrics += etree.tostring(elem, method = 'text', encoding = 'utf-8')
+ self.finish(lyrics)
+
+ class FetchAnimelyrics(Fetcher):
+
+ def fetch(self, song):
+ # abort any existing connections
+ if self.srep:
+ self.srep.abort()
+ self.srep = None
+ if self.lrep:
+ self.lrep.abort()
+ self.lrep = None
+ self.song = song
+
+ url = QtCore.QUrl('http://www.animelyrics.com/search.php')
+ url.setQueryItems([('t', 'performer'), ('q', self.song.artist())])
+
+ self.logger.info('Searching Animelyrics: %s.'%url)
+ self.srep = self.nam.get(QtNetwork.QNetworkRequest(url))
+ self.connect(self.srep, QtCore.SIGNAL('finished()'), self.__handle_search_res)
+
+ def __handle_search_res(self):
+ # TODO use Qt xml functions
+ tree = etree.HTML(unicode(self.srep.readAll(), encoding = 'utf-8', errors='ignore'))
+ self.srep = None
+
+ url = None
+ for elem in tree.iterfind('.//a'):
+ if ('href' in elem.attrib) and elem.text and (self.song.title() in elem.text):
+ url = QtCore.QUrl('http://www.animelyrics.com/%s'%elem.get('href'))
+
+ if not url:
+ self.logger.info('Didn\'t find the URL in Animelyrics search results.')
+ return self.finish()
+ self.logger.info('Found Animelyrics song URL: %s.'%url)
+
+ self.lrep = self.nam.get(QtNetwork.QNetworkRequest(url))
+ self.connect(self.lrep, QtCore.SIGNAL('finished()'), self.__handle_lyrics)
+
+ def __handle_lyrics(self):
+ lyrics = ''
+ tree = etree.HTML(unicode(self.lrep.readAll(), encoding = 'utf-8'))
+ for elem in tree.iterfind('.//pre'):
+ if elem.get('class') == 'lyrics':
+ lyrics += '%s\n\n'%etree.tostring(elem, method = 'text', encoding = 'utf-8')
+
+ self.finish(lyrics)
+