home *** CD-ROM | disk | FTP | other *** search
/ PC Welt 2006 November (DVD) / PCWELT_11_2006.ISO / casper / filesystem.squashfs / usr / lib / rhythmbox / plugins / lyrics.py < prev    next >
Encoding:
Python Source  |  2006-08-30  |  9.1 KB  |  316 lines

  1. # -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- 
  2. #
  3. # Copyright 2005 Eduardo Gonzalez
  4. # Copyright (C) 2006 Jonathan Matthew
  5. #
  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.
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
  18. #
  19. # TODO: 
  20. # - multiple lyrics sites (lyrc.com.ar, etc.)
  21. # - handle multiple results usefully
  22. # - save lyrics to disk?
  23. # - check that the lyrics returned even remotely match the request?
  24. # - share URL grabbing code with other plugins
  25.  
  26. import os 
  27. import gtk, gobject
  28. import urllib
  29. import re
  30. from xml.dom import minidom
  31. from gettext import gettext as _
  32. import rb
  33. import rhythmdb
  34.  
  35. from Loader import Loader
  36.     
  37. ui_str = """
  38. <ui>
  39.   <menubar name="MenuBar">
  40.     <menu name="ViewMenu" action="View">
  41.       <menuitem name="ViewSongLyrics" action="ViewSongLyrics"/>
  42.     </menu>
  43.   </menubar>
  44. </ui>
  45. """
  46.  
  47. LYRICS_FOLDER="~/.gnome2/rhythmbox/lyrics"
  48. LYRIC_TITLE_STRIP=["\(live[^\)]*\)", "\(acoustic[^\)]*\)", "\([^\)]*mix\)", "\([^\)]*version\)", "\([^\)]*edit\)", "\(feat[^\)]*\)"]
  49. LYRIC_TITLE_REPLACE=[("/", "-"), ("&", "and")]
  50. LYRIC_ARTIST_REPLACE=[("/", "-"), ("&", "and")]
  51.  
  52.  
  53. def create_lyrics_view():
  54.     view = gtk.TextView()
  55.     view.set_wrap_mode(gtk.WRAP_WORD)
  56.     view.set_editable(False)
  57.  
  58.     sw = gtk.ScrolledWindow()
  59.     sw.add(view)
  60.     sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
  61.     sw.set_shadow_type(gtk.SHADOW_IN)
  62.  
  63.     vbox = gtk.VBox(spacing=12)
  64.     vbox.pack_start(sw, expand=True)
  65.     return (vbox, view.get_buffer())
  66.  
  67. class LyricWindow(gtk.Window):
  68.     def __init__(self, db, entry):
  69.         gtk.Window.__init__(self)
  70.         self.set_border_width(12)
  71.  
  72.     title = db.entry_get(entry, rhythmdb.PROP_TITLE)
  73.     artist = db.entry_get(entry, rhythmdb.PROP_ARTIST)
  74.         self.set_title(title + " - " + artist + " - Lyrics")
  75.  
  76.     close = gtk.Button(stock=gtk.STOCK_CLOSE)
  77.     close.connect('clicked', lambda w: self.destroy())
  78.  
  79.     (lyrics_view, buffer) = create_lyrics_view()
  80.     self.buffer = buffer
  81.         bbox = gtk.HButtonBox()
  82.         bbox.set_layout(gtk.BUTTONBOX_END)
  83.         bbox.pack_start(close)
  84.         lyrics_view.pack_start(bbox, expand=False)
  85.  
  86.         self.buffer.set_text(_("Searching for lyrics..."))
  87.         self.add(lyrics_view)
  88.         self.set_default_size(400, 300)
  89.         self.show_all()
  90.  
  91. class LyricPane(object):
  92.     def __init__(self, db, song_info):
  93.         (self.view, self.buffer) = create_lyrics_view()
  94.     self.view.show_all()
  95.     self.page_num = song_info.append_page(_("Lyrics"), self.view)
  96.     self.db = db
  97.     self.song_info = song_info
  98.     self.have_lyrics = 0
  99.     self.visible = 0
  100.         self.entry = self.song_info.get_property("current-entry")
  101.  
  102.     self.entry_change_id = song_info.connect('notify::current-entry', self.entry_changed)
  103.     nb = self.view.get_parent()
  104.     self.switch_page_id = nb.connect('switch-page', self.switch_page_cb)
  105.  
  106.     def entry_changed(self, pspec, duh):
  107.         self.entry = self.song_info.get_property("current-entry")
  108.     self.have_lyrics = 0
  109.     if self.visible != 0:
  110.         self.get_lyrics()
  111.  
  112.     def switch_page_cb(self, notebook, page, page_num):
  113.         if self.have_lyrics != 0:
  114.         return
  115.  
  116.         if page_num != self.page_num:
  117.         self.visible = 0
  118.         return
  119.  
  120.     self.visible = 1
  121.     self.get_lyrics()
  122.  
  123.     def get_lyrics(self):
  124.         if self.entry is None:
  125.         return
  126.  
  127.     self.buffer.set_text(_("Searching for lyrics..."));
  128.     lyrics_grabber = LyricGrabber()
  129.     lyrics_grabber.get_lyrics(self.db, self.entry, self.buffer.set_text)
  130.     
  131.     
  132.  
  133. class LyricGrabber(object):
  134.     def __init__(self):
  135.         self.loader = Loader ()
  136.  
  137.     def _build_cache_path(self, artist, title):
  138.         lyrics_folder = os.path.expanduser (LYRICS_FOLDER)
  139.         if not os.path.exists (lyrics_folder):
  140.             os.mkdir (lyrics_folder)
  141.  
  142.         return lyrics_folder + '/%s - %s.txt' % (artist, title)
  143.  
  144.     def get_lyrics(self, db, entry, callback):
  145.     self.callback = callback
  146.         artist = db.entry_get(entry, rhythmdb.PROP_ARTIST).lower()
  147.         title = db.entry_get(entry, rhythmdb.PROP_TITLE).lower()
  148.  
  149.     # replace ampersands and the like
  150.     for exp in LYRIC_ARTIST_REPLACE:
  151.         p = re.compile (exp[0])
  152.         artist = p.sub(exp[1], artist)
  153.     for exp in LYRIC_TITLE_REPLACE:
  154.         p = re.compile (exp[0])
  155.         title = p.sub(exp[1], title)
  156.  
  157.         # strip things like "(live at Somewhere)", "(accoustic)", etc
  158.         for exp in LYRIC_TITLE_STRIP:
  159.             p = re.compile (exp)
  160.             title = p.sub ('', title)
  161.  
  162.     # compress spaces
  163.     title = title.strip()
  164.     artist = artist.strip()
  165.  
  166.     self.cache_path = self._build_cache_path(artist, title)
  167.  
  168.     if os.path.exists (self.cache_path):
  169.             self.loader.get_url(self.cache_path, callback)
  170.             return;
  171.         
  172.     url = "http://api.leoslyrics.com/api_search.php?auth=Rhythmbox&artist=%s&songtitle=%s" % (
  173.         urllib.quote(artist.encode('utf-8')),
  174.         urllib.quote(title.encode('utf-8')))
  175.     self.loader.get_url(url, self.search_results)
  176.  
  177.     def search_results(self, data):
  178.         if data is None:
  179.         self.callback("Server did not respond.")
  180.         return
  181.  
  182.     try:
  183.         xmldoc = minidom.parseString(data).documentElement
  184.     except:
  185.         self.callback("Couldn't parse search results.")
  186.         return
  187.     
  188.     result_code = xmldoc.getElementsByTagName('response')[0].getAttribute('code')
  189.     if result_code != '0':
  190.         self.callback("Server is busy, try again later.")
  191.         xmldoc.unlink()
  192.         return
  193.     
  194.     # We don't really need the top 100 matches, so I'm limiting it to ten
  195.     matches = xmldoc.getElementsByTagName('result')[:10]
  196.     #songs = map(lambda x:
  197.     #        x.getElementsByTagName('name')[0].firstChild.nodeValue
  198.     #        + " - " +
  199.     #        x.getElementsByTagName('title')[0].firstChild.nodeValue,
  200.     #        matches)
  201.     hids = map(lambda x: x.getAttribute('hid'), matches)
  202.     #exacts = map(lambda x: x.getAttribute('exactMatch'), matches)
  203.  
  204.     if len(hids) == 0:
  205.         # FIXME show other matches
  206.         self.callback("Unable to find lyrics for this track.")
  207.         xmldoc.unlink()
  208.         return
  209.     
  210.     #songlist = []
  211.     #for i in range(len(hids)):
  212.     #    songlist.append((songs[i], hids[i], exacts[i]))
  213.  
  214.     xmldoc.unlink()
  215.     url = "http://api.leoslyrics.com/api_lyrics.php?auth=Rhythmbox&hid=%s" % (urllib.quote(hids[0].encode('utf-8')))
  216.     self.loader.get_url(url, self.lyrics)
  217.  
  218.  
  219.     def lyrics(self, data):
  220.         if data is None:
  221.         self.callback("Unable to find lyrics for this track.")
  222.         return
  223.  
  224.     try:
  225.         xmldoc = minidom.parseString(data).documentElement
  226.     except:
  227.         self.callback("Unable to parse the lyrics returned.")
  228.         return
  229.  
  230.     text = xmldoc.getElementsByTagName('title')[0].firstChild.nodeValue
  231.     text += ' - ' + xmldoc.getElementsByTagName('artist')[0].getElementsByTagName('name')[0].firstChild.nodeValue + '\n\n'
  232.     text += xmldoc.getElementsByTagName('text')[0].firstChild.nodeValue
  233.     xmldoc.unlink()
  234.  
  235.     text += "\n\n"+_("Lyrics provided by leoslyrics.com")
  236.  
  237.  
  238.         f = file (self.cache_path, 'w')
  239.         f.write (text)
  240.         f.close ()
  241.  
  242.     self.callback(text)
  243.  
  244.  
  245.  
  246. class LyricsDisplayPlugin(rb.Plugin):
  247.  
  248.     def __init__ (self):
  249.     rb.Plugin.__init__ (self)
  250.     self.window = None
  251.  
  252.     def activate (self, shell):
  253.     self.action = gtk.Action ('ViewSongLyrics', _('Song L_yrics'),
  254.                   _('Display lyrics for the playing song'),
  255.                   'rb-song-lyrics')
  256.     self.activate_id = self.action.connect ('activate', self.show_song_lyrics, shell)
  257.  
  258.     self.action_group = gtk.ActionGroup ('SongLyricsPluginActions')
  259.     self.action_group.add_action (self.action)
  260.         
  261.         uim = shell.get_ui_manager ()
  262.     uim.insert_action_group (self.action_group, 0)
  263.     self.ui_id = uim.add_ui_from_string (ui_str)
  264.     uim.ensure_update ()
  265.  
  266.     sp = shell.get_player ()
  267.     self.pec_id = sp.connect('playing-song-changed', self.playing_entry_changed)
  268.     self.playing_entry_changed (sp, sp.get_playing_entry ())
  269.  
  270.     self.csi_id = shell.connect('create_song_info', self.create_song_info)
  271.  
  272.     def deactivate (self, shell):
  273.         
  274.     uim = shell.get_ui_manager()
  275.     uim.remove_ui (self.ui_id)
  276.     uim.remove_action_group (self.action_group)
  277.  
  278.     self.action_group = None
  279.     self.action = None
  280.  
  281.     sp = shell.get_player ()
  282.     sp.disconnect (self.pec_id)
  283.  
  284.     if self.window is not None:
  285.         self.window.destroy ()
  286.  
  287.  
  288.     def playing_entry_changed (self, sp, entry):
  289.         if entry is not None:
  290.         self.action.set_sensitive (True)
  291.     else:
  292.         self.action.set_sensitive (False)
  293.  
  294.     def show_song_lyrics (self, action, shell):
  295.  
  296.     if self.window is not None:
  297.         self.window.destroy ()
  298.  
  299.     db = shell.get_property ("db")
  300.     sp = shell.get_player ()
  301.     entry = sp.get_playing_entry ()
  302.  
  303.     if entry is None:
  304.         return
  305.  
  306.     self.window = LyricWindow(db, entry)
  307.     lyrics_grabber = LyricGrabber()
  308.     lyrics_grabber.get_lyrics(db, entry, self.window.buffer.set_text)
  309.     
  310.     def create_song_info (self, shell, song_info, is_multiple):
  311.  
  312.     if is_multiple is False:
  313.         x = LyricPane(shell.get_property ("db"), song_info)
  314.  
  315.