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
19 from PyQt4
.QtCore
import QVariant
25 from lxml
import etree
27 from ..plugin
import Plugin
30 class LyricsWidget(QtGui
.QWidget
):
32 plugin
= None # plugin
36 __text_view
= None # text-object
39 def __init__(self
, plugin
):
40 QtGui
.QWidget
.__init
__(self
)
42 self
.logger
= plugin
.logger
45 self
.__label
= QtGui
.QLabel(self
)
48 self
.__text
_view
= QtGui
.QTextEdit(self
)
49 self
.__text
_view
.setReadOnly(True)
52 self
.__toolbar
= QtGui
.QToolBar('Lyrics toolbar', self
)
53 self
.__toolbar
.setOrientation(QtCore
.Qt
.Vertical
)
55 self
.__toolbar
.addAction(QtGui
.QIcon('gfx/refresh.png'), 'Refresh lyrics', self
.plugin
.refresh
)
56 edit
= self
.__toolbar
.addAction(QtGui
.QIcon('gfx/edit.png'), 'Edit lyrics')
57 edit
.setCheckable(True)
58 edit
.connect(edit
, QtCore
.SIGNAL('toggled(bool)'), self
.__toggle
_editable
)
60 self
.__toolbar
.addAction(QtGui
.QIcon('gfx/save.png'), 'Save lyrics', self
.__save
_lyrics
)
61 self
.__toolbar
.addAction(QtGui
.QIcon('gfx/delete.png'), 'Delete stored file', self
.plugin
.del_lyrics_file
)
63 self
.setLayout(QtGui
.QGridLayout())
64 self
.layout().setSpacing(0)
65 self
.layout().setMargin(0)
66 self
.layout().addWidget(self
.__toolbar
, 0, 0, -1, 1, QtCore
.Qt
.AlignTop
)
67 self
.layout().addWidget(self
.__label
, 0, 1)
68 self
.layout().addWidget(self
.__text
_view
, 1, 1)
70 self
.connect(self
.plugin
, QtCore
.SIGNAL('new_lyrics_fetched'), self
.set_lyrics
)
72 def set_lyrics(self
, song
, lyrics
, flags
= 0):
73 """Set currently displayed lyrics for song. flags parameter is
77 return self
.__text
_view
.clear()
79 # a late thread might call this for a previous song
80 if song
!= self
.plugin
.mpclient
.current_song():
83 self
.__text
_view
.clear()
84 self
.__label
.setText('<b>%s</b> by <u>%s</u> on <u>%s</u>'\
85 %(song
.title(), song
.artist(), song
.album()))
87 self
.logger
.info('Setting new lyrics.')
88 self
.__text
_view
.insertPlainText(lyrics
.decode('utf-8'))
90 self
.logger
.info('Lyrics not found.')
91 self
.__text
_view
.insertPlainText('Lyrics not found.')
93 def __save_lyrics(self
):
94 self
.plugin
.save_lyrics_file(unicode(self
.__text
_view
.toPlainText()).encode('utf-8'))
96 def __toggle_editable(self
, val
):
97 self
.__text
_view
.setReadOnly(not val
)
102 """A dict of { site name : function }. Function takes a song and returns lyrics
103 as a python string or None if not found."""
107 DEFAULTS
= {'sites' : QtCore
.QStringList(['lyricwiki', 'animelyrics']), 'lyricdir' : '$musicdir/$songdir',
108 'lyricname' : '.lyrics_nephilim_$artist_$album_$title', 'store' : True}
109 __available_sites
= {}
113 def __init__(self
, parent
, mpclient
, name
):
114 Plugin
.__init
__(self
, parent
, mpclient
, name
)
116 self
.__available
_sites
['lyricwiki'] = self
.__fetch
_lyricwiki
117 self
.__available
_sites
['animelyrics'] = self
.__fetch
_animelyrics
120 self
.o
= LyricsWidget(self
)
121 for site
in self
.__available
_sites
:
122 if site
in self
.settings().value('%s/sites'%self
.name
).toStringList():
123 self
.sites
[site
] = self
.__available
_sites
[site
]
124 self
.connect(self
.mpclient
, QtCore
.SIGNAL('song_changed'), self
.refresh
)
128 self
.disconnect(self
.mpclient
, QtCore
.SIGNAL('song_changed'), self
.refresh
)
130 return "Show (and fetch) the lyrics of the currently playing song."
132 def _get_dock_widget(self
):
133 return self
._create
_dock
(self
.o
)
135 class FetchThread(QtCore
.QThread
):
136 def __init__(self
, parent
, fetch_func
, song
):
137 QtCore
.QThread
.__init
__(self
)
138 self
.setParent(parent
)
139 self
.fetch_func
= fetch_func
142 self
.fetch_func(self
.song
)
145 """Attempt to automatically get lyrics first from a file, then from the internet."""
146 self
.logger
.info('Autorefreshing lyrics.')
147 song
= self
.mpclient
.current_song()
149 return self
.o
.set_lyrics(None, None)
151 (self
.__lyrics
_dir
, self
.__lyrics
_path
) = misc
.generate_metadata_path(song
,
152 self
.settings().value(self
.name
+ '/lyricdir').toString(),
153 self
.settings().value(self
.name
+ '/lyricname').toString())
155 self
.logger
.info('Trying to read lyrics from file %s.'%self
.__lyrics
_path
)
156 file = open(self
.__lyrics
_path
, 'r')
160 return self
.emit(QtCore
.SIGNAL('new_lyrics_fetched'), song
, lyrics
)
162 self
.logger
.info('Error reading lyrics file: %s.'%e)
165 thread
= self
.FetchThread(self
, self
.__fetch
_lyrics
, song
)
168 def save_lyrics_file(self
, lyrics
, path
= None):
169 """Save lyrics to a file specified in path.
170 If path is None, then a default value is used."""
171 self
.logger
.info('Saving lyrics...')
174 file = open(path
, 'w')
176 file = open(self
.__lyrics
_path
, 'w')
179 self
.logger
.info('Lyrics successfully saved.')
181 self
.logger
.error('Error writing lyrics: %s', e
)
183 def del_lyrics_file(self
, song
= None):
184 """Delete a lyrics file for song. If song is not specified
185 current song is used."""
187 path
= self
.__lyrics
_path
189 path
= misc
.generate_metadata_path(song
, self
.settings().value(self
.name
+ '/lyricdir').toString(),
190 self
.settings().value(self
.name
+ '/lyricname').toString())
195 self
.logger
.error('Error removing lyrics file %s: %s'%(path
, e
))
197 def __fetch_lyrics(self
, song
):
198 self
.logger
.info('Trying to download lyrics from internet.')
200 for site
in self
.sites
:
201 self
.logger
.info('Trying %s.'%site
)
202 lyrics
= self
.sites
[site
](song
)
204 if self
.settings().value(self
.name
+ '/store').toBool():
205 self
.save_lyrics_file(lyrics
)
206 return self
.emit(QtCore
.SIGNAL('new_lyrics_fetched'), song
, lyrics
)
208 self
.emit(QtCore
.SIGNAL('new_lyrics_fetched'), song
, None)
210 def __fetch_lyricwiki(self
, song
):
211 url
= 'http://lyricwiki.org/api.php?%s' %urllib
.urlencode({'func':'getSong',
212 'artist':song
.artist().encode('utf-8'), 'song':song
.title().encode('utf-8'),
216 tree
= etree
.HTML(urllib
.urlopen(url
).read())
217 if tree
.find('.//lyrics').text
== 'Not found':
219 #get page with lyrics and change <br> tags to newlines
220 url
= tree
.find('.//url').text
221 page
= re
.sub('<br>|<br/>|<br />', '\n', urllib
.urlopen(url
).read())
222 html
= etree
.HTML(page
)
224 for elem
in html
.iterfind('.//div'):
225 if elem
.get('class') == 'lyricbox':
226 lyrics
+= etree
.tostring(elem
, method
= 'text', encoding
= 'utf-8')
228 except (socket
.error
, IOError), e
:
229 self
.logger
.error('Error downloading lyrics from LyricWiki: %s.'%e)
232 def __fetch_animelyrics(self
, song
):
233 url
= 'http://www.animelyrics.com/search.php?%s'%urllib
.urlencode({'q':song
.artist().encode('utf-8'),
237 self
.logger
.info('Searching Animelyrics: %s.'%url
)
238 tree
= etree
.HTML(urllib
.urlopen(url
).read())
240 for elem
in tree
.iterfind('.//a'):
241 if ('href' in elem
.attrib
) and elem
.text
and (song
.title() in elem
.text
):
242 url
= 'http://www.animelyrics.com/%s'%elem
.get('href')
246 self
.logger
.info('Found song URL: %s.'%url
)
247 tree
= etree
.HTML(urllib
.urlopen(url
).read())
249 for elem
in tree
.iterfind('.//pre'):
250 if elem
.get('class') == 'lyrics':
251 ret
+= '%s\n\n'%etree
.tostring(elem
, method
= 'text', encoding
= 'utf-8')
253 except socket
.error
, e
:
254 self
.logger
.error('Error downloading lyrics from Animelyrics: %s.'%e)
256 except AttributeError:
260 class SettingsWidgetLyrics(Plugin
.SettingsWidget
):
265 def __init__(self
, plugin
):
266 Plugin
.SettingsWidget
.__init
__(self
, plugin
)
267 self
.settings().beginGroup(self
.plugin
.name
)
270 # store lyrics groupbox
271 self
.store
= QtGui
.QGroupBox('Store lyrics.')
272 self
.store
.setToolTip('Should %s store its own copy of lyrics?'%misc
.APPNAME
)
273 self
.store
.setCheckable(True)
274 self
.store
.setChecked(self
.settings().value('store').toBool())
275 self
.store
.setLayout(QtGui
.QGridLayout())
278 self
.lyricdir
= QtGui
.QLineEdit(self
.settings().value('lyricdir').toString())
279 self
.lyricdir
.setToolTip('Where should %s store lyrics.\n'
280 '$musicdir will be expanded to path to MPD music library (as set by user)\n'
281 '$songdir will be expanded to path to the song (relative to $musicdir\n'
282 'other tags same as in lyricname'
284 self
.lyricname
= QtGui
.QLineEdit(self
.settings().value('lyricname').toString())
285 self
.lyricname
.setToolTip('Filename for %s lyricsfiles.\n'
286 'All tags supported by MPD will be expanded to their\n'
287 'values for current song, e.g. $title, $track, $artist,\n'
288 '$album, $genre etc.'%misc
.APPNAME
)
289 self
.store
.layout().addWidget(QtGui
.QLabel('Lyrics directory'), 0, 0)
290 self
.store
.layout().addWidget(self
.lyricdir
, 0, 1)
291 self
.store
.layout().addWidget(QtGui
.QLabel('Lyrics filename'), 1, 0)
292 self
.store
.layout().addWidget(self
.lyricname
, 1, 1)
294 self
.setLayout(QtGui
.QVBoxLayout())
295 self
.layout().addWidget(self
.store
)
297 self
.settings().endGroup()
299 def save_settings(self
):
300 self
.settings().beginGroup(self
.plugin
.name
)
301 self
.settings().setValue('lyricdir', QVariant(self
.lyricdir
.text()))
302 self
.settings().setValue('lyricname', QVariant(self
.lyricname
.text()))
303 self
.settings().setValue('store', QVariant(self
.store
.isChecked()))
304 self
.settings().endGroup()
305 self
.plugin
.refresh()
307 def get_settings_widget(self
):
308 return self
.SettingsWidgetLyrics(self
)