# # Copyright (C) 2009 Anton Khirnov # # Nephilim is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Nephilim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Nephilim. If not, see . # from PyQt4 import QtGui, QtCore, QtNetwork import os import re from lxml import etree from ..plugin import Plugin from .. import common, metadata_fetcher from .. import icons class LyricsWidget(QtGui.QWidget): #public lyrics_loaded = None # public, read-only plugin = None # plugin logger = None # private __text_view = None # text-object __toolbar = None __label = None #### private def __init__(self, plugin): QtGui.QWidget.__init__(self) self.plugin = plugin self.logger = plugin.logger self.curLyrics = '' self.__label = QtGui.QLabel(self) self.__label.setWordWrap(True) # add text area self.__text_view = QtGui.QTextEdit(self) self.__text_view.setReadOnly(True) # add toolbar self.__toolbar = QtGui.QToolBar('Lyrics toolbar', self) self.__toolbar.setOrientation(QtCore.Qt.Vertical) self.__toolbar.addAction(QtGui.QIcon(':icons/refresh.png'), 'Refresh lyrics', self.plugin.refresh) edit = self.__toolbar.addAction(QtGui.QIcon(':icons/edit.png'), 'Edit lyrics') edit.setCheckable(True) edit.toggled.connect(self.__toggle_editable) self.__toolbar.addAction(QtGui.QIcon(':icons/save.png'), 'Save lyrics', self.__save_lyrics) self.__toolbar.addAction(QtGui.QIcon(':icons/delete.png'), 'Delete stored file', self.plugin.del_lyrics_file) self.setLayout(QtGui.QGridLayout()) self.layout().setSpacing(0) self.layout().setMargin(0) self.layout().addWidget(self.__toolbar, 0, 0, -1, 1, QtCore.Qt.AlignTop) self.layout().addWidget(self.__label, 0, 1) self.layout().addWidget(self.__text_view, 1, 1) def __save_lyrics(self): self.plugin.save_lyrics_file(self.__text_view.toPlainText()) def __toggle_editable(self, val): self.__text_view.setReadOnly(not val) #### public #### def set_lyrics(self, song, lyrics, flags = 0): """Set currently displayed lyrics (unicode string) for song. flags parameter is unused now.""" if not song: self.__label.clear() return self.__text_view.clear() # a late thread might call this for a previous song if song != self.plugin.mpclient.cur_song: return self.__text_view.clear() self.__label.setText('%s by %s on %s'\ %(song['?title'], song['?artist'], song['?album'])) if lyrics: self.logger.info('Setting new lyrics.') self.__text_view.insertPlainText(lyrics) self.lyrics_loaded = True else: self.logger.info('Lyrics not found.') self.__text_view.insertPlainText('Lyrics not found.') class Lyrics(Plugin): # public, const info = 'Show (and fetch) the lyrics of the currently playing song.' # public, read-only o = None # private DEFAULTS = {'fetchers' : ['Lyricwiki', 'Animelyrics'], 'lyricdir' : '${musicdir}/${songdir}', 'lyricname' : '.lyrics_%s_${artist}_${album}_${title}'%common.APPNAME, 'store' : 1} "implemented fetchers" available_fetchers = None #XXX SettingsWidget currently uses it "enabled fetchers, those with higher priority first" __fetchers = None "number of returned results from last refresh() call" __results = None "index/priority of current lyrics" __index = None "metadata paths" __lyrics_dir = None __lyrics_path = None #### private #### def __init__(self, parent, mpclient, name): Plugin.__init__(self, parent, mpclient, name) self.__fetchers = [] self.available_fetchers = [FetchLyricwiki, FetchAnimelyrics] def __new_lyrics_fetched(self, song, lyrics): self.logger.info('Got new lyrics.') self.__results += 1 i = self.__fetchers.index(self.sender()) if lyrics and i < self.__index: if int(self.settings.value(self.name + '/store')) and self.__lyrics_path: self.save_lyrics_file(lyrics) self.__index = i 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): # private lyricdir = None lyricname = None store = None fetcherlist = None def __init__(self, plugin): Plugin.SettingsWidget.__init__(self, plugin) self.settings.beginGroup(self.plugin.name) # store lyrics groupbox self.store = QtGui.QGroupBox('Store lyrics.') self.store.setToolTip('Should %s store its own copy of lyrics?'%common.APPNAME) self.store.setCheckable(True) self.store.setChecked(int(self.settings.value('store'))) self.store.setLayout(QtGui.QGridLayout()) # paths to lyrics self.lyricdir = QtGui.QLineEdit(self.settings.value('lyricdir')) self.lyricdir.setToolTip('Where should %s store lyrics.\n' '${musicdir} will be expanded to path to MPD music library (as set by user)\n' '${songdir} will be expanded to path to the song (relative to ${musicdir}\n' 'other tags same as in lyricname' %common.APPNAME) self.lyricname = QtGui.QLineEdit(self.settings.value('lyricname')) self.lyricname.setToolTip('Filename for %s lyricsfiles.\n' 'All tags supported by MPD will be expanded to their\n' 'values for current song, e.g. ${title}, ${track}, ${artist},\n' '${album}, ${genre} etc.'%common.APPNAME) self.store.layout().addWidget(QtGui.QLabel('Lyrics directory'), 0, 0) self.store.layout().addWidget(self.lyricdir, 0, 1) self.store.layout().addWidget(QtGui.QLabel('Lyrics filename'), 1, 0) self.store.layout().addWidget(self.lyricname, 1, 1) # fetchers list fetchers = self.settings.value('fetchers') self.fetcherlist = QtGui.QListWidget(self) self.fetcherlist.setDragDropMode(QtGui.QAbstractItemView.InternalMove) for fetcher in fetchers: it = QtGui.QListWidgetItem(fetcher) it.setCheckState(QtCore.Qt.Checked) self.fetcherlist.addItem(it) for fetcher in self.plugin.available_fetchers: if not fetcher.name in fetchers: it = QtGui.QListWidgetItem(fetcher.name) it.setCheckState(QtCore.Qt.Unchecked) self.fetcherlist.addItem(it) self.setLayout(QtGui.QVBoxLayout()) self.layout().addWidget(self.store) self._add_widget(self.fetcherlist, label = 'Sites', tooltip = 'A list of sources used for fetching lyrics.\n' 'Use drag and drop to change their priority.') self.settings.endGroup() def save_settings(self): self.settings.beginGroup(self.plugin.name) self.settings.setValue('lyricdir', self.lyricdir.text()) self.settings.setValue('lyricname', self.lyricname.text()) self.settings.setValue('store', int(self.store.isChecked())) fetchers = [] for i in range(self.fetcherlist.count()): it = self.fetcherlist.item(i) if it.checkState() == QtCore.Qt.Checked: fetchers.append(it.text()) self.settings.setValue('fetchers', fetchers) self.settings.endGroup() self.plugin.refresh_fetchers() self.plugin.refresh() #### public #### def _load(self): self.refresh_fetchers() self.o = LyricsWidget(self) self.mpclient.song_changed.connect(self.refresh) self.refresh() def _unload(self): self.o = None self.__fetchers = None self.mpclient.song_changed.disconnect(self.refresh) def _get_dock_widget(self): return self._create_dock(self.o) 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.__index = len(self.__fetchers) self.o.lyrics_loaded = False song = self.mpclient.cur_song if not song: self.__lyrics_dir = '' self.__lyrics_path = '' return self.o.set_lyrics(None, None) self.logger.info('Trying to read lyrics from stickers.') self.mpclient.sticker_get(song['file'], 'lyrics', callback = lambda lyrics: self._refresh2(song, lyrics)) def _refresh2(self, song, lyrics): if lyrics: lyrics = lyrics.replace('\\n', '\n') self.logger.info('Found the lyrics sticker.') return self.o.set_lyrics(song, lyrics) (self.__lyrics_dir, self.__lyrics_path) = common.generate_metadata_path(song, self.settings.value(self.name + '/lyricdir'), self.settings.value(self.name + '/lyricname')) try: self.logger.info('Nothing in the sticker database. Trying to read lyrics from file %s.'%self.__lyrics_path) file = open(self.__lyrics_path, 'r') lyrics = file.read().decode('utf-8') file.close() if lyrics: return self.o.set_lyrics(song, lyrics) except IOError, e: self.logger.info('Error reading lyrics file: %s.'%e) for fetcher in self.__fetchers: fetcher.fetch(song) def save_lyrics_file(self, lyrics, path = None): """Save lyrics (unicode string) to a file specified in path. If path is None, then a default value is used.""" self.logger.info('Saving lyrics...') try: if path: file = open(path, 'w') else: file = open(self.__lyrics_path, 'w') file.write(lyrics.encode('utf-8')) file.close() self.logger.info('Lyrics successfully saved.') except IOError, e: self.logger.error('Error writing lyrics: %s', e) self.mpclient.sticker_set(self.mpclient.cur_song['file'], 'lyrics', lyrics.replace('\n', '\\n')) def del_lyrics_file(self, song = None): """Delete a lyrics file for song. If song is not specified current song is used.""" if not song: path = self.__lyrics_path else: path = common.generate_metadata_path(song, self.settings.value(self.name + '/lyricdir'), self.settings.value(self.name + '/lyricname')) try: os.remove(path) except (IOError, OSError), e: self.logger.error('Error removing lyrics file %s: %s'%(path, e)) def get_settings_widget(self): return self.SettingsWidgetLyrics(self) def refresh_fetchers(self): """Refresh the list of available fetchers.""" self.__fetchers = [] # append fetchers in order they are stored in settings for name in self.settings.value('%s/fetchers'%self.name): for fetcher in self.available_fetchers: if fetcher.name == name: self.__fetchers.append(fetcher(self)) self.__fetchers[-1].finished.connect(self.__new_lyrics_fetched) class FetchLyricwiki(metadata_fetcher.MetadataFetcher): name = 'Lyricwiki' __apiaddress = 'http://lyrics.wikia.com/api.php' def fetch(self, song): url = QtCore.QUrl(self.__apiaddress) url.setQueryItems([('func', 'getArtist'), ('artist', song['?artist']), ('fmt', 'xml'), ('action', 'lyrics')]) self.fetch2(song, url) self.rep.finished.connect(self.__handle_artist_res) def __handle_artist_res(self): artist = None xml = QtCore.QXmlStreamReader(self.rep) while not xml.atEnd(): token = xml.readNext() if token == QtCore.QXmlStreamReader.StartElement: if xml.name() == 'artist': artist = xml.readElementText() xml.clear() if not artist: self.logger.info('Didn\'t find artist in %s artist search results.'%self.name) return self.finish() self.logger.info('Found artist: %s'%artist) url = QtCore.QUrl(self.__apiaddress) url.setQueryItems([('action', 'lyrics'), ('func', 'getSong'), ('artist', artist), ('song', self.song['?title']), ('fmt', 'xml')]) self.rep = self.nam.get(QtNetwork.QNetworkRequest(url)) self.rep.finished.connect(self.__handle_search_res) self.rep.error.connect(self.handle_error) def __handle_search_res(self): url = None # the page is borked utf-8 as of nov 2009, qxmlstreamreader chokes # on it => use regexps match = re.search('(.*)', str(self.rep.readAll()).decode('utf-8', 'replace'), re.DOTALL|re.IGNORECASE) if match and not 'action=edit' in match.group(1): url = QtCore.QUrl() # the url is already percent-encoded url.setEncodedUrl(match.group(1)) if not url: self.logger.info('Didn\'t find the song on Lyricwiki.') return self.finish() self.logger.info('Found Lyricwiki song URL: %s.'%url.toString()) req = QtNetwork.QNetworkRequest(url) self.rep = self.nam.get(req) self.rep.finished.connect(self.__handle_lyrics) self.rep.error.connect(self.handle_error) def __handle_lyrics(self): # the page isn't valid xml, so use regexps lyrics = '' for it in re.finditer('
(?:.*?
)?(.*?)(?:.*?)?', str(self.rep.readAll()).decode('utf-8'), re.DOTALL): gr = re.sub('
', '\n', it.group(1)) gr = re.sub(re.compile('<.*>', re.DOTALL), '', gr) lyrics += gr + '\n' self.finish(common.decode_htmlentities(lyrics)) class FetchAnimelyrics(metadata_fetcher.MetadataFetcher): name = 'Animelyrics' def fetch(self, song): url = QtCore.QUrl('http://www.animelyrics.com/search.php') url.setQueryItems([('t', 'performer'), ('q', song['?artist'])]) self.fetch2(song, url) self.rep.finished.connect(self.__handle_search_res) def __handle_search_res(self): # TODO use Qt xml functions try: tree = etree.HTML(unicode(self.rep.readAll(), encoding = 'utf-8', errors='ignore')) except etree.XMLSyntaxError, e: self.logger.error('Error parsing lyrics: %s' %e) return self.finish() 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.rep = self.nam.get(QtNetwork.QNetworkRequest(url)) self.rep.finished.connect(self.__handle_lyrics) self.rep.error.connect(self.handle_error) def __handle_lyrics(self): lyrics = '' try: tree = etree.HTML(unicode(self.rep.readAll(), encoding = 'utf-8')) except etree.XMLSyntaxError, e: self.logger.error('Error parsing lyrics: %s' %e) return self.finish() 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)