From bec4257daa05a7c71678330e7f9747ecdfe5b2ca Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Wed, 19 Aug 2009 06:50:30 +0200 Subject: 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. --- TODO | 1 + nephilim/plugins/Lyrics.py | 254 +++++++++++++++++++++++++++++---------------- 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 . # -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
tags to newlines - url = tree.find('.//url').text - page = re.sub('
|
|
', '\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('
|
|
', '\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) + -- cgit v1.2.3