1 # -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
3 # Copyright 2005 Eduardo Gonzalez
4 # Copyright (C) 2006 Jonathan Matthew
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2, or (at your option)
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
21 # - multiple lyrics sites (lyrc.com.ar, etc.)
22 # - handle multiple results usefully
23 # - save lyrics to disk?
24 # - check that the lyrics returned even remotely match the request?
25 # - share URL grabbing code with other plugins
31 from xml
.dom
import minidom
37 <menubar name="MenuBar">
38 <menu name="ViewMenu" action="View">
39 <menuitem name="ViewSongLyrics" action="ViewSongLyrics"/>
45 LYRICS_FOLDER
="~/.lyrics"
46 LYRIC_TITLE_STRIP
=["\(live[^\)]*\)", "\(acoustic[^\)]*\)", "\([^\)]*mix\)", "\([^\)]*version\)", "\([^\)]*edit\)", "\(feat[^\)]*\)"]
47 LYRIC_TITLE_REPLACE
=[("/", "-"), (" & ", " and ")]
48 LYRIC_ARTIST_REPLACE
=[("/", "-"), (" & ", " and ")]
51 def create_lyrics_view():
53 view
.set_wrap_mode(gtk
.WRAP_WORD
)
54 view
.set_editable(False)
56 sw
= gtk
.ScrolledWindow()
58 sw
.set_policy(gtk
.POLICY_NEVER
, gtk
.POLICY_AUTOMATIC
)
59 sw
.set_shadow_type(gtk
.SHADOW_IN
)
61 vbox
= gtk
.VBox(spacing
=12)
62 vbox
.pack_start(sw
, expand
=True)
63 return (vbox
, view
.get_buffer())
65 class LyricWindow(gtk
.Window
):
66 def __init__(self
, db
, entry
):
67 gtk
.Window
.__init
__(self
)
68 self
.set_border_width(12)
70 title
= db
.entry_get(entry
, rhythmdb
.PROP_TITLE
)
71 artist
= db
.entry_get(entry
, rhythmdb
.PROP_ARTIST
)
72 self
.set_title(title
+ " - " + artist
+ " - Lyrics")
74 close
= gtk
.Button(stock
=gtk
.STOCK_CLOSE
)
75 close
.connect('clicked', lambda w
: self
.destroy())
77 (lyrics_view
, buffer) = create_lyrics_view()
79 bbox
= gtk
.HButtonBox()
80 bbox
.set_layout(gtk
.BUTTONBOX_END
)
81 bbox
.pack_start(close
)
82 lyrics_view
.pack_start(bbox
, expand
=False)
84 self
.buffer.set_text(_("Searching for lyrics..."))
86 self
.set_default_size(400, 300)
89 class LyricPane(object):
90 def __init__(self
, db
, song_info
):
91 (self
.view
, self
.buffer) = create_lyrics_view()
93 self
.page_num
= song_info
.append_page(_("Lyrics"), self
.view
)
95 self
.song_info
= song_info
98 self
.entry
= self
.song_info
.get_property("current-entry")
100 self
.entry_change_id
= song_info
.connect('notify::current-entry', self
.entry_changed
)
101 nb
= self
.view
.get_parent()
102 self
.switch_page_id
= nb
.connect('switch-page', self
.switch_page_cb
)
104 def entry_changed(self
, pspec
, duh
):
105 self
.entry
= self
.song_info
.get_property("current-entry")
107 if self
.visible
!= 0:
110 def switch_page_cb(self
, notebook
, page
, page_num
):
111 if self
.have_lyrics
!= 0:
114 if page_num
!= self
.page_num
:
121 def get_lyrics(self
):
122 if self
.entry
is None:
125 self
.buffer.set_text(_("Searching for lyrics..."));
126 lyrics_grabber
= LyricGrabber()
127 lyrics_grabber
.get_lyrics(self
.db
, self
.entry
, self
.buffer.set_text
)
131 class LyricGrabber(object):
133 self
.loader
= rb
.Loader ()
135 def _build_cache_path(self
, artist
, title
):
136 lyrics_folder
= os
.path
.expanduser (LYRICS_FOLDER
)
137 if not os
.path
.exists (lyrics_folder
):
138 os
.mkdir (lyrics_folder
)
140 artist_folder
= lyrics_folder
+ '/' + artist
[:128]
141 if not os
.path
.exists (artist_folder
):
142 os
.mkdir (artist_folder
)
144 return artist_folder
+ '/' + title
[:128] + '.lyric'
146 def get_lyrics(self
, db
, entry
, callback
):
147 self
.callback
= callback
148 artist
= db
.entry_get(entry
, rhythmdb
.PROP_ARTIST
).lower()
149 title
= db
.entry_get(entry
, rhythmdb
.PROP_TITLE
).lower()
151 # replace ampersands and the like
152 for exp
in LYRIC_ARTIST_REPLACE
:
153 p
= re
.compile (exp
[0])
154 artist
= p
.sub(exp
[1], artist
)
155 for exp
in LYRIC_TITLE_REPLACE
:
156 p
= re
.compile (exp
[0])
157 title
= p
.sub(exp
[1], title
)
159 # strip things like "(live at Somewhere)", "(accoustic)", etc
160 for exp
in LYRIC_TITLE_STRIP
:
162 title
= p
.sub ('', title
)
165 title
= title
.strip()
166 artist
= artist
.strip()
168 self
.cache_path
= self
._build
_cache
_path
(artist
, title
)
170 if os
.path
.exists (self
.cache_path
):
171 self
.loader
.get_url(self
.cache_path
, callback
)
174 url
= "http://api.leoslyrics.com/api_search.php?auth=Rhythmbox&artist=%s&songtitle=%s" % (
175 urllib
.quote(artist
.encode('utf-8')),
176 urllib
.quote(title
.encode('utf-8')))
177 self
.loader
.get_url(url
, self
.search_results
)
179 def search_results(self
, data
):
181 self
.callback("Server did not respond.")
185 xmldoc
= minidom
.parseString(data
).documentElement
187 self
.callback("Couldn't parse search results.")
190 result_code
= xmldoc
.getElementsByTagName('response')[0].getAttribute('code')
191 if result_code
!= '0':
192 self
.callback("Server is busy, try again later.")
196 # We don't really need the top 100 matches, so I'm limiting it to ten
197 matches
= xmldoc
.getElementsByTagName('result')[:10]
198 #songs = map(lambda x:
199 # x.getElementsByTagName('name')[0].firstChild.nodeValue
201 # x.getElementsByTagName('title')[0].firstChild.nodeValue,
203 hids
= map(lambda x
: x
.getAttribute('hid'), matches
)
204 #exacts = map(lambda x: x.getAttribute('exactMatch'), matches)
207 # FIXME show other matches
208 self
.callback("Unable to find lyrics for this track.")
213 #for i in range(len(hids)):
214 # songlist.append((songs[i], hids[i], exacts[i]))
217 url
= "http://api.leoslyrics.com/api_lyrics.php?auth=Rhythmbox&hid=%s" % (urllib
.quote(hids
[0].encode('utf-8')))
218 self
.loader
.get_url(url
, self
.lyrics
)
221 def lyrics(self
, data
):
223 self
.callback("Unable to find lyrics for this track.")
227 xmldoc
= minidom
.parseString(data
).documentElement
229 self
.callback("Unable to parse the lyrics returned.")
232 text
= xmldoc
.getElementsByTagName('title')[0].firstChild
.nodeValue
233 text
+= ' - ' + xmldoc
.getElementsByTagName('artist')[0].getElementsByTagName('name')[0].firstChild
.nodeValue
+ '\n\n'
234 text
+= xmldoc
.getElementsByTagName('text')[0].firstChild
.nodeValue
237 text
+= "\n\n"+_("Lyrics provided by leoslyrics.com")
240 f
= file (self
.cache_path
, 'w')
248 class LyricsDisplayPlugin(rb
.Plugin
):
251 rb
.Plugin
.__init
__ (self
)
254 def activate (self
, shell
):
255 self
.action
= gtk
.Action ('ViewSongLyrics', _('Song L_yrics'),
256 _('Display lyrics for the playing song'),
258 self
.activate_id
= self
.action
.connect ('activate', self
.show_song_lyrics
, shell
)
260 self
.action_group
= gtk
.ActionGroup ('SongLyricsPluginActions')
261 self
.action_group
.add_action (self
.action
)
263 uim
= shell
.get_ui_manager ()
264 uim
.insert_action_group (self
.action_group
, 0)
265 self
.ui_id
= uim
.add_ui_from_string (ui_str
)
268 sp
= shell
.get_player ()
269 self
.pec_id
= sp
.connect('playing-song-changed', self
.playing_entry_changed
)
270 self
.playing_entry_changed (sp
, sp
.get_playing_entry ())
272 self
.csi_id
= shell
.connect('create_song_info', self
.create_song_info
)
274 def deactivate (self
, shell
):
276 uim
= shell
.get_ui_manager()
277 uim
.remove_ui (self
.ui_id
)
278 uim
.remove_action_group (self
.action_group
)
280 self
.action_group
= None
283 shell
.disconnect (self
.csi_id
)
285 sp
= shell
.get_player ()
286 sp
.disconnect (self
.pec_id
)
288 if self
.window
is not None:
289 self
.window
.destroy ()
292 def playing_entry_changed (self
, sp
, entry
):
293 if entry
is not None:
294 self
.action
.set_sensitive (True)
296 self
.action
.set_sensitive (False)
298 def show_song_lyrics (self
, action
, shell
):
300 if self
.window
is not None:
301 self
.window
.destroy ()
303 db
= shell
.get_property ("db")
304 sp
= shell
.get_player ()
305 entry
= sp
.get_playing_entry ()
310 self
.window
= LyricWindow(db
, entry
)
311 lyrics_grabber
= LyricGrabber()
312 lyrics_grabber
.get_lyrics(db
, entry
, self
.window
.buffer.set_text
)
314 def create_song_info (self
, shell
, song_info
, is_multiple
):
316 if is_multiple
is False:
317 x
= LyricPane(shell
.get_property ("db"), song_info
)