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
, parent
, entry
):
67 gtk
.Window
.__init
__(self
)
68 self
.set_border_width(12)
69 self
.set_transient_for(parent
)
71 title
= db
.entry_get(entry
, rhythmdb
.PROP_TITLE
)
72 artist
= db
.entry_get(entry
, rhythmdb
.PROP_ARTIST
)
73 self
.set_title(title
+ " - " + artist
+ " - Lyrics")
75 close
= gtk
.Button(stock
=gtk
.STOCK_CLOSE
)
76 close
.connect('clicked', lambda w
: self
.destroy())
78 (lyrics_view
, buffer) = create_lyrics_view()
80 bbox
= gtk
.HButtonBox()
81 bbox
.set_layout(gtk
.BUTTONBOX_END
)
82 bbox
.pack_start(close
)
83 lyrics_view
.pack_start(bbox
, expand
=False)
85 self
.buffer.set_text(_("Searching for lyrics..."))
87 self
.set_default_size(400, 300)
90 class LyricPane(object):
91 def __init__(self
, db
, song_info
):
92 (self
.view
, self
.buffer) = create_lyrics_view()
94 self
.page_num
= song_info
.append_page(_("Lyrics"), self
.view
)
96 self
.song_info
= song_info
99 self
.entry
= self
.song_info
.get_property("current-entry")
101 self
.entry_change_id
= song_info
.connect('notify::current-entry', self
.entry_changed
)
102 nb
= self
.view
.get_parent()
103 self
.switch_page_id
= nb
.connect('switch-page', self
.switch_page_cb
)
105 def entry_changed(self
, pspec
, duh
):
106 self
.entry
= self
.song_info
.get_property("current-entry")
108 if self
.visible
!= 0:
111 def switch_page_cb(self
, notebook
, page
, page_num
):
112 if self
.have_lyrics
!= 0:
115 if page_num
!= self
.page_num
:
122 def get_lyrics(self
):
123 if self
.entry
is None:
126 self
.buffer.set_text(_("Searching for lyrics..."));
127 lyrics_grabber
= LyricGrabber()
128 lyrics_grabber
.get_lyrics(self
.db
, self
.entry
, self
.buffer.set_text
)
132 class LyricGrabber(object):
134 self
.loader
= rb
.Loader ()
136 def _build_cache_path(self
, artist
, title
):
137 lyrics_folder
= os
.path
.expanduser (LYRICS_FOLDER
)
138 if not os
.path
.exists (lyrics_folder
):
139 os
.mkdir (lyrics_folder
)
141 artist_folder
= lyrics_folder
+ '/' + artist
[:128]
142 if not os
.path
.exists (artist_folder
):
143 os
.mkdir (artist_folder
)
145 return artist_folder
+ '/' + title
[:128] + '.lyric'
147 def get_lyrics(self
, db
, entry
, callback
):
148 self
.callback
= callback
149 artist
= db
.entry_get(entry
, rhythmdb
.PROP_ARTIST
).lower()
150 title
= db
.entry_get(entry
, rhythmdb
.PROP_TITLE
).lower()
152 # replace ampersands and the like
153 for exp
in LYRIC_ARTIST_REPLACE
:
154 p
= re
.compile (exp
[0])
155 artist
= p
.sub(exp
[1], artist
)
156 for exp
in LYRIC_TITLE_REPLACE
:
157 p
= re
.compile (exp
[0])
158 title
= p
.sub(exp
[1], title
)
160 # strip things like "(live at Somewhere)", "(accoustic)", etc
161 for exp
in LYRIC_TITLE_STRIP
:
163 title
= p
.sub ('', title
)
166 title
= title
.strip()
167 artist
= artist
.strip()
169 self
.cache_path
= self
._build
_cache
_path
(artist
, title
)
171 if os
.path
.exists (self
.cache_path
):
172 self
.loader
.get_url(self
.cache_path
, callback
)
175 url
= "http://api.leoslyrics.com/api_search.php?auth=Rhythmbox&artist=%s&songtitle=%s" % (
176 urllib
.quote(artist
.encode('utf-8')),
177 urllib
.quote(title
.encode('utf-8')))
178 self
.loader
.get_url(url
, self
.search_results
)
180 def search_results(self
, data
):
182 self
.callback("Server did not respond.")
186 xmldoc
= minidom
.parseString(data
).documentElement
188 self
.callback("Couldn't parse search results.")
191 result_code
= xmldoc
.getElementsByTagName('response')[0].getAttribute('code')
192 if result_code
!= '0':
193 self
.callback("Server is busy, try again later.")
197 # We don't really need the top 100 matches, so I'm limiting it to ten
198 matches
= xmldoc
.getElementsByTagName('result')[:10]
199 #songs = map(lambda x:
200 # x.getElementsByTagName('name')[0].firstChild.nodeValue
202 # x.getElementsByTagName('title')[0].firstChild.nodeValue,
204 hids
= map(lambda x
: x
.getAttribute('hid'), matches
)
205 #exacts = map(lambda x: x.getAttribute('exactMatch'), matches)
208 # FIXME show other matches
209 self
.callback("Unable to find lyrics for this track.")
214 #for i in range(len(hids)):
215 # songlist.append((songs[i], hids[i], exacts[i]))
218 url
= "http://api.leoslyrics.com/api_lyrics.php?auth=Rhythmbox&hid=%s" % (urllib
.quote(hids
[0].encode('utf-8')))
219 self
.loader
.get_url(url
, self
.lyrics
)
222 def lyrics(self
, data
):
224 self
.callback("Unable to find lyrics for this track.")
228 xmldoc
= minidom
.parseString(data
).documentElement
230 self
.callback("Unable to parse the lyrics returned.")
233 text
= xmldoc
.getElementsByTagName('title')[0].firstChild
.nodeValue
234 text
+= ' - ' + xmldoc
.getElementsByTagName('artist')[0].getElementsByTagName('name')[0].firstChild
.nodeValue
+ '\n\n'
235 text
+= xmldoc
.getElementsByTagName('text')[0].firstChild
.nodeValue
238 text
+= "\n\n"+_("Lyrics provided by leoslyrics.com")
241 f
= file (self
.cache_path
, 'w')
249 class LyricsDisplayPlugin(rb
.Plugin
):
252 rb
.Plugin
.__init
__ (self
)
255 def activate (self
, shell
):
256 self
.action
= gtk
.Action ('ViewSongLyrics', _('Song L_yrics'),
257 _('Display lyrics for the playing song'),
259 self
.activate_id
= self
.action
.connect ('activate', self
.show_song_lyrics
, shell
)
261 self
.action_group
= gtk
.ActionGroup ('SongLyricsPluginActions')
262 self
.action_group
.add_action (self
.action
)
264 uim
= shell
.get_ui_manager ()
265 uim
.insert_action_group (self
.action_group
, 0)
266 self
.ui_id
= uim
.add_ui_from_string (ui_str
)
269 sp
= shell
.get_player ()
270 self
.pec_id
= sp
.connect('playing-song-changed', self
.playing_entry_changed
)
271 self
.playing_entry_changed (sp
, sp
.get_playing_entry ())
273 self
.csi_id
= shell
.connect('create_song_info', self
.create_song_info
)
275 def deactivate (self
, shell
):
277 uim
= shell
.get_ui_manager()
278 uim
.remove_ui (self
.ui_id
)
279 uim
.remove_action_group (self
.action_group
)
281 self
.action_group
= None
284 shell
.disconnect (self
.csi_id
)
286 sp
= shell
.get_player ()
287 sp
.disconnect (self
.pec_id
)
289 if self
.window
is not None:
290 self
.window
.destroy ()
293 def playing_entry_changed (self
, sp
, entry
):
294 if entry
is not None:
295 self
.action
.set_sensitive (True)
297 self
.action
.set_sensitive (False)
299 def show_song_lyrics (self
, action
, shell
):
301 if self
.window
is not None:
302 self
.window
.destroy ()
304 db
= shell
.get_property ("db")
305 sp
= shell
.get_player ()
306 entry
= sp
.get_playing_entry ()
311 self
.window
= LyricWindow(db
, shell
.get_property ("window"), entry
)
312 lyrics_grabber
= LyricGrabber()
313 lyrics_grabber
.get_lyrics(db
, entry
, self
.window
.buffer.set_text
)
315 def create_song_info (self
, shell
, song_info
, is_multiple
):
317 if is_multiple
is False:
318 x
= LyricPane(shell
.get_property ("db"), song_info
)