udapted vi.po
[rhythmbox.git] / plugins / lyrics / lyrics.py
blobf60afc46580410e28b90e00224fdc9297bb59ff8
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)
9 # any later version.
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.
20 # TODO:
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
27 import os
28 import gtk, gobject
29 import urllib
30 import re
31 from xml.dom import minidom
32 import rb
33 import rhythmdb
35 ui_str = """
36 <ui>
37 <menubar name="MenuBar">
38 <menu name="ViewMenu" action="View">
39 <menuitem name="ViewSongLyrics" action="ViewSongLyrics"/>
40 </menu>
41 </menubar>
42 </ui>
43 """
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():
52 view = gtk.TextView()
53 view.set_wrap_mode(gtk.WRAP_WORD)
54 view.set_editable(False)
56 sw = gtk.ScrolledWindow()
57 sw.add(view)
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()
79 self.buffer = buffer
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..."))
86 self.add(lyrics_view)
87 self.set_default_size(400, 300)
88 self.show_all()
90 class LyricPane(object):
91 def __init__(self, db, song_info):
92 (self.view, self.buffer) = create_lyrics_view()
93 self.view.show_all()
94 self.page_num = song_info.append_page(_("Lyrics"), self.view)
95 self.db = db
96 self.song_info = song_info
97 self.have_lyrics = 0
98 self.visible = 0
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")
107 self.have_lyrics = 0
108 if self.visible != 0:
109 self.get_lyrics()
111 def switch_page_cb(self, notebook, page, page_num):
112 if self.have_lyrics != 0:
113 return
115 if page_num != self.page_num:
116 self.visible = 0
117 return
119 self.visible = 1
120 self.get_lyrics()
122 def get_lyrics(self):
123 if self.entry is None:
124 return
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):
133 def __init__(self):
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:
162 p = re.compile (exp)
163 title = p.sub ('', title)
165 # compress spaces
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)
173 return;
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):
181 if data is None:
182 self.callback("Server did not respond.")
183 return
185 try:
186 xmldoc = minidom.parseString(data).documentElement
187 except:
188 self.callback("Couldn't parse search results.")
189 return
191 result_code = xmldoc.getElementsByTagName('response')[0].getAttribute('code')
192 if result_code != '0':
193 self.callback("Server is busy, try again later.")
194 xmldoc.unlink()
195 return
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
201 # + " - " +
202 # x.getElementsByTagName('title')[0].firstChild.nodeValue,
203 # matches)
204 hids = map(lambda x: x.getAttribute('hid'), matches)
205 #exacts = map(lambda x: x.getAttribute('exactMatch'), matches)
207 if len(hids) == 0:
208 # FIXME show other matches
209 self.callback("Unable to find lyrics for this track.")
210 xmldoc.unlink()
211 return
213 #songlist = []
214 #for i in range(len(hids)):
215 # songlist.append((songs[i], hids[i], exacts[i]))
217 xmldoc.unlink()
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):
223 if data is None:
224 self.callback("Unable to find lyrics for this track.")
225 return
227 try:
228 xmldoc = minidom.parseString(data).documentElement
229 except:
230 self.callback("Unable to parse the lyrics returned.")
231 return
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
236 xmldoc.unlink()
238 text += "\n\n"+_("Lyrics provided by leoslyrics.com")
241 f = file (self.cache_path, 'w')
242 f.write (text)
243 f.close ()
245 self.callback(text)
249 class LyricsDisplayPlugin(rb.Plugin):
251 def __init__ (self):
252 rb.Plugin.__init__ (self)
253 self.window = None
255 def activate (self, shell):
256 self.action = gtk.Action ('ViewSongLyrics', _('Song L_yrics'),
257 _('Display lyrics for the playing song'),
258 'rb-song-lyrics')
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)
267 uim.ensure_update ()
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
282 self.action = 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)
296 else:
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 ()
308 if entry is None:
309 return
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)