Updated Finnish translation
[rhythmbox.git] / plugins / lyrics / lyrics.py
bloba0c4974cd915bf9a9ab0061fe623a4f2d56fb1fc
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, 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()
78 self.buffer = buffer
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..."))
85 self.add(lyrics_view)
86 self.set_default_size(400, 300)
87 self.show_all()
89 class LyricPane(object):
90 def __init__(self, db, song_info):
91 (self.view, self.buffer) = create_lyrics_view()
92 self.view.show_all()
93 self.page_num = song_info.append_page(_("Lyrics"), self.view)
94 self.db = db
95 self.song_info = song_info
96 self.have_lyrics = 0
97 self.visible = 0
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")
106 self.have_lyrics = 0
107 if self.visible != 0:
108 self.get_lyrics()
110 def switch_page_cb(self, notebook, page, page_num):
111 if self.have_lyrics != 0:
112 return
114 if page_num != self.page_num:
115 self.visible = 0
116 return
118 self.visible = 1
119 self.get_lyrics()
121 def get_lyrics(self):
122 if self.entry is None:
123 return
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):
132 def __init__(self):
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:
161 p = re.compile (exp)
162 title = p.sub ('', title)
164 # compress spaces
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)
172 return;
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):
180 if data is None:
181 self.callback("Server did not respond.")
182 return
184 try:
185 xmldoc = minidom.parseString(data).documentElement
186 except:
187 self.callback("Couldn't parse search results.")
188 return
190 result_code = xmldoc.getElementsByTagName('response')[0].getAttribute('code')
191 if result_code != '0':
192 self.callback("Server is busy, try again later.")
193 xmldoc.unlink()
194 return
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
200 # + " - " +
201 # x.getElementsByTagName('title')[0].firstChild.nodeValue,
202 # matches)
203 hids = map(lambda x: x.getAttribute('hid'), matches)
204 #exacts = map(lambda x: x.getAttribute('exactMatch'), matches)
206 if len(hids) == 0:
207 # FIXME show other matches
208 self.callback("Unable to find lyrics for this track.")
209 xmldoc.unlink()
210 return
212 #songlist = []
213 #for i in range(len(hids)):
214 # songlist.append((songs[i], hids[i], exacts[i]))
216 xmldoc.unlink()
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):
222 if data is None:
223 self.callback("Unable to find lyrics for this track.")
224 return
226 try:
227 xmldoc = minidom.parseString(data).documentElement
228 except:
229 self.callback("Unable to parse the lyrics returned.")
230 return
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
235 xmldoc.unlink()
237 text += "\n\n"+_("Lyrics provided by leoslyrics.com")
240 f = file (self.cache_path, 'w')
241 f.write (text)
242 f.close ()
244 self.callback(text)
248 class LyricsDisplayPlugin(rb.Plugin):
250 def __init__ (self):
251 rb.Plugin.__init__ (self)
252 self.window = None
254 def activate (self, shell):
255 self.action = gtk.Action ('ViewSongLyrics', _('Song L_yrics'),
256 _('Display lyrics for the playing song'),
257 'rb-song-lyrics')
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)
266 uim.ensure_update ()
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
281 self.action = 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)
295 else:
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 ()
307 if entry is None:
308 return
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)