# # 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 from PyQt4.QtCore import QVariant import os import re from lxml import etree from ..plugin import Plugin from .. import common 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' : True} "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 self.settings.value(self.name + '/store').toBool() 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(self.settings.value('store').toBool()) self.store.setLayout(QtGui.QGridLayout()) # paths to lyrics self.lyricdir = QtGui.QLineEdit(self.settings.value('lyricdir').toString()) 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').toString()) 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').toStringList() 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', QVariant(self.lyricdir.text())) self.settings.setValue('lyricname', QVariant(self.lyricname.text())) self.settings.setValue('store', QVariant(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', QVariant(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.__lyrics_dir, self.__lyrics_path) = common.generate_metadata_path(song, self.settings.value(self.name + '/lyricdir').toString(), self.settings.value(self.name + '/lyricname').toString()) try: self.logger.info('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) 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').toString(), self.settings.value(self.name + '/lyricname').toString()) 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).toStringList(): 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(common.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(common.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)