Index: configure.ac =================================================================== --- configure.ac (revision 5177) +++ configure.ac (working copy) @@ -974,6 +974,7 @@ plugins/mtpdevice/Makefile plugins/iradio/Makefile plugins/lirc/Makefile plugins/lyrics/Makefile +plugins/lyrics/lyrics/Makefile plugins/sample-python/Makefile plugins/sample-vala/Makefile plugins/pythonconsole/Makefile Index: data/rhythmbox.schemas =================================================================== --- data/rhythmbox.schemas (revision 5177) +++ data/rhythmbox.schemas (working copy) @@ -1192,5 +1192,28 @@ True if the media player keys plugin is hidden. + + /schemas/apps/rhythmbox/plugins/lyrics/engines + /apps/rhythmbox/plugins/lyrics/engines + rhythmbox + list + string + [astraweb.com,lyrc.com.ar,leoslyrics.com] + + The list of sites to search for lyrics. + The list of sites to search for lyrics. + + + + /schemas/apps/rhythmbox/plugins/lyrics/folder + /apps/rhythmbox/plugins/lyrics/folder + rhythmbox + string + ~/.lyrics + + The directory to store lyrics in + The directory to store lyrics in + + Index: plugins/lyrics/lyrics.rb-plugin.desktop.in =================================================================== --- plugins/lyrics/lyrics.rb-plugin.desktop.in (revision 5177) +++ plugins/lyrics/lyrics.rb-plugin.desktop.in (working copy) @@ -4,6 +4,6 @@ Module=lyrics IAge=1 _Name=Song Lyrics _Description=Fetch song lyrics from the Internet -Authors=Jonathan Matthew, Eduardo Gonzalez -Copyright=Copyright © 2005-2006 Jonathan Matthew, Eduardo Gonzalez +Authors=Jonathan Matthew, Eduardo Gonzalez, Sirio Bolaños +Copyright=Copyright © 2005-2007 Jonathan Matthew, Eduardo Gonzalez, Sirio Bolaños Website=http://www.rhythmbox.org/ Index: plugins/lyrics/lyrics/__init__.py =================================================================== --- plugins/lyrics/lyrics/__init__.py (revision 0) +++ plugins/lyrics/lyrics/__init__.py (revision 0) @@ -0,0 +1,367 @@ +# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- +# +# Copyright (C) 2006 Jonathan Matthew +# Copyright (C) 2007 James Livingston +# Copyright (C) 2007 Sirio Bolaños Puchet +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# TODO: +# - check that the lyrics returned even remotely match the request? + +import os, re +import gtk, gtk.glade +import gconf +import rhythmdb, rb + +import LyricsParse +from LyricsConfigureDialog import LyricsConfigureDialog + +ui_str = """ + + + + + + + +""" + +LYRIC_TITLE_STRIP=["\(live[^\)]*\)", "\(acoustic[^\)]*\)", "\([^\)]*mix\)", "\([^\)]*version\)", "\([^\)]*edit\)", "\(feat[^\)]*\)", "\([^\)]*\)"] +LYRIC_TITLE_REPLACE=[("/", "-"), (" & ", " and ")] +LYRIC_ARTIST_REPLACE=[("/", "-"), (" & ", " and ")] + +gconf_keys = { 'engines' : '/apps/rhythmbox/plugins/lyrics/engines', + 'folder': '/apps/rhythmbox/plugins/lyrics/folder' + } + + +def create_lyrics_view(): + tview = gtk.TextView() + tview.set_wrap_mode(gtk.WRAP_WORD) + tview.set_editable(False) + + sw = gtk.ScrolledWindow() + sw.add(tview) + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + sw.set_shadow_type(gtk.SHADOW_IN) + + vbox = gtk.VBox(spacing=12) + vbox.pack_start(sw, expand=True) + + return (vbox, tview.get_buffer(), tview) + +def parse_song_data(artist, title): + + # replace ampersands and the like + for exp in LYRIC_ARTIST_REPLACE: + artist = re.sub(exp[0], exp[1], artist) + for exp in LYRIC_TITLE_REPLACE: + title = re.sub(exp[0], exp[1], title) + + # strip things like "(live at Somewhere)", "(accoustic)", etc + for exp in LYRIC_TITLE_STRIP: + title = re.sub (exp, '', title) + + # compress spaces + title = title.strip() + artist = artist.strip() + + return (artist, title) + +def build_cache_path(artist, title): + folder = gconf.client_get_default().get_string(gconf_keys['folder']) + if folder is None: + return None + + lyrics_folder = os.path.expanduser (folder) + if not os.path.exists (lyrics_folder): + os.mkdir (lyrics_folder) + + artist_folder = lyrics_folder + '/' + artist[:128] + if not os.path.exists (artist_folder): + os.mkdir (artist_folder) + + return artist_folder + '/' + title[:128] + '.lyric' + +class LyricGrabber(object): + def __init__(self, db, entry): + self.loader = rb.Loader () + self.db = db + self.entry = entry + + self.artist = self.db.entry_get(self.entry, rhythmdb.PROP_ARTIST).lower() + self.title = self.db.entry_get(self.entry, rhythmdb.PROP_TITLE).lower() + + (self.artist, self.title) = parse_song_data(self.artist, self.title) + + self.cache_path = build_cache_path(self.artist, self.title) + + def verify_lyric(self): + return os.path.exists(self.cache_path) + + def search_lyrics(self, callback, mode=None): + self.callback = callback + + status = self.verify_lyric() + + if status: + self.loader.get_url(self.cache_path, callback) + else: + if mode is not None: + self.callback(_("No lyrics found")) + else: + def lyric_callback (text): + if text is not None: + f = file (self.cache_path, 'w') + f.write (text) + f.close () + self.callback(text) + else: + self.callback(_("No lyrics found")) + + parser = LyricsParse.Parser(gconf_keys, self.artist, self.title) + parser.get_lyrics(lyric_callback) + +class LyricPane(object): + def __init__(self, db, song_info): + self.db = db + self.song_info = song_info + self.entry = self.song_info.get_property("current-entry") + + self.build_path() + + def save_lyrics(cache_path, text): + f = file (cache_path, 'w') + f.write (text) + f.close () + + def erase_lyrics(cache_path): + f = file (cache_path, 'w') + f.write ("") + f.close () + + def save_callback(): + buf = self.buffer + startiter = buf.get_start_iter() + enditer = buf.get_end_iter() + text = buf.get_text(startiter, enditer) + save_lyrics(self.cache_path, text) + self.get_lyrics() + + def edit_callback(widget): + if self.edit.get_active() == 1: + self.tview.set_editable(True) + self.edit.set_label("_Save") + else: + if self.cache_path is not None: + save_callback() + self.tview.set_editable(False) + self.edit.set_label("_Edit") + + def discard_callback(widget): + if self.cache_path is not None and os.path.exists(self.cache_path): + os.remove(self.cache_path) + self.get_lyrics() + + def clear_callback(widget): + if self.cache_path is not None and os.path.exists (self.cache_path): + erase_lyrics(self.cache_path) + self.get_lyrics() + + + self.edit = gtk.ToggleButton("_Edit") + self.edit.connect('toggled', edit_callback) + self.discard = gtk.Button("_Search again") + self.discard.connect('clicked', discard_callback) + self.clear = gtk.Button(stock=gtk.STOCK_CLEAR) + self.clear.connect('clicked', clear_callback) + self.hbox = gtk.HBox() + self.hbox.pack_end(self.edit, expand=False) + self.hbox.pack_end(self.clear, expand=False) + self.hbox.pack_start(self.discard, expand=False) + + (self.view, self.buffer, self.tview) = create_lyrics_view() + + self.view.pack_start(self.hbox, expand=False, fill=False) + self.view.set_spacing(2) + + self.view.show_all() + self.page_num = song_info.append_page(_("Lyrics"), self.view) + self.have_lyrics = 0 + self.visible = 0 + + self.entry_change_id = song_info.connect('notify::current-entry', self.entry_changed) + nb = self.view.get_parent() + self.switch_page_id = nb.connect('switch-page', self.switch_page_cb) + + #self.get_lyrics() + + def build_path(self): + + artist = self.db.entry_get(self.entry, rhythmdb.PROP_ARTIST).lower() + title = self.db.entry_get(self.entry, rhythmdb.PROP_TITLE).lower() + (artist, title) = parse_song_data(artist, title) + cache_path = build_cache_path(artist, title) + self.cache_path = cache_path + + def entry_changed(self, pspec, duh): + self.entry = self.song_info.get_property("current-entry") + self.have_lyrics = 0 + if self.visible != 0: + self.build_path() + self.get_lyrics() + + def switch_page_cb(self, notebook, page, page_num): + if self.have_lyrics != 0: + return + + if page_num != self.page_num: + self.visible = 0 + return + + self.visible = 1 + self.get_lyrics() + + def get_lyrics(self): + if self.entry is None: + return + + self.buffer.set_text(_("Searching for lyrics...")); + lyrics_grabber = LyricGrabber(self.db, self.entry) + lyrics_grabber.search_lyrics(self.buffer.set_text) + + +class LyricWindow (gtk.Window): + + def __init__(self): + gtk.Window.__init__(self) + self.set_border_width(12) + + close = gtk.Button(stock=gtk.STOCK_CLOSE) + close.connect('clicked', lambda w: self.destroy()) + + (lyrics_view, buffer, tview) = create_lyrics_view() + self.buffer = buffer + bbox = gtk.HButtonBox() + bbox.set_layout(gtk.BUTTONBOX_END) + bbox.pack_start(close) + lyrics_view.pack_start(bbox, expand=False) + + self.add(lyrics_view) + self.set_default_size(400, 300) + self.show_all() + + def s_title(self, title, artist): + self.set_title(title + " - " + artist + " - Lyrics") + +class LyricsDisplayPlugin(rb.Plugin): + + def __init__ (self): + rb.Plugin.__init__ (self) + self.window = None + + def activate (self, shell): + self.shell = shell + self.action = gtk.Action ('ViewSongLyrics', _('Song L_yrics'), + _('Display lyrics for the playing song'), + 'rb-song-lyrics') + self.activate_id = self.action.connect ('activate', self.show_song_lyrics, shell) + + self.action_group = gtk.ActionGroup ('SongLyricsPluginActions') + self.action_group.add_action_with_accel (self.action, "L") + + uim = shell.get_ui_manager () + uim.insert_action_group (self.action_group, 0) + self.ui_id = uim.add_ui_from_string (ui_str) + uim.ensure_update () + + sp = shell.get_player () + self.pec_id = sp.connect('playing-song-changed', self.playing_entry_changed) + self.current_entry = None + self.playing_entry_changed (sp, sp.get_playing_entry ()) + + self.csi_id = shell.connect('create_song_info', self.create_song_info) + + def deactivate (self, shell): + + uim = shell.get_ui_manager() + uim.remove_ui (self.ui_id) + uim.remove_action_group (self.action_group) + + self.action_group = None + self.action = None + + sp = shell.get_player () + sp.disconnect (self.pec_id) + + if self.window is not None: + self.window.destroy () + self.window = None + + def create_configure_dialog(self, dialog=None): + if not dialog: + glade_file = self.find_file("lyrics-prefs.glade") + dialog = LyricsConfigureDialog (glade_file, gconf_keys).get_dialog() + dialog.present() + return dialog + + def playing_entry_changed (self, sp, entry): + if entry is not None: + self.action.set_sensitive (True) + self.update_song_lyrics(entry) + else: + self.action.set_sensitive (False) + + def update_song_lyrics(self, entry): + if entry == self.current_entry: + return + + db = self.shell.get_property ("db") + + if self.window is None: + return + + title = db.entry_get(entry, rhythmdb.PROP_TITLE) + artist = db.entry_get(entry, rhythmdb.PROP_ARTIST) + + self.window.s_title(title, artist) + lyrics_grabber = LyricGrabber(db, entry) + lyrics_grabber.search_lyrics(self.window.buffer.set_text, True) + + def show_song_lyrics (self, action, shell): + + if self.window is not None: + self.window.destroy () + + db = shell.get_property ("db") + sp = shell.get_player () + entry = sp.get_playing_entry () + + if entry is None: + return + + title = db.entry_get(entry, rhythmdb.PROP_TITLE) + artist = db.entry_get(entry, rhythmdb.PROP_ARTIST) + + self.window = LyricWindow() + self.window.s_title(title, artist) + lyrics_grabber = LyricGrabber(db, entry) + lyrics_grabber.search_lyrics(self.window.buffer.set_text) + + def create_song_info (self, shell, song_info, is_multiple): + + if is_multiple is False: + x = LyricPane(shell.get_property ("db"), song_info) Index: plugins/lyrics/lyrics/LyricsParse.py =================================================================== --- plugins/lyrics/lyrics/LyricsParse.py (revision 0) +++ plugins/lyrics/lyrics/LyricsParse.py (revision 0) @@ -0,0 +1,63 @@ +# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- +# +# Copyright (C) 2007 James Livingston +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + + +import urllib +import re +import gconf +import rb + +from LyrcParser import LyrcParser +from AstrawebParser import AstrawebParser +from LeoslyricsParser import LeoslyricsParser + + +engines_map = { + 'lyrc.com.ar': LyrcParser, + 'astraweb.com': AstrawebParser, + 'leoslyrics.com': LeoslyricsParser +} + + +class Parser (object): + def __init__(self, gconf_keys, artist, title): + self.title = title + self.artist = artist + self.engines = gconf.client_get_default().get_list(gconf_keys['engines'], gconf.VALUE_STRING) + + def searcher(self, plexer, callback, *data): + for e in self.engines: + plexer.clear() + if e in engines_map: + parser = engines_map[e] (self.artist, self.title) + print "searching " + e + " for lyrics" + + parser.search(plexer.send()) + yield None + + _, (lyrics,) = plexer.receive() + if lyrics is not None: + callback (lyrics, *data) + return + + callback (None, *data) + + def get_lyrics(self, callback, *data): + rb.Coroutine (self.searcher, callback, *data).begin () + + Index: plugins/lyrics/lyrics/Makefile.am =================================================================== --- plugins/lyrics/lyrics/Makefile.am (revision 0) +++ plugins/lyrics/lyrics/Makefile.am (revision 0) @@ -0,0 +1,10 @@ +# Search Lyrics Python Plugin + +plugindir = $(PLUGINDIR)/lyrics +plugin_PYTHON = \ + LyricsParse.py \ + LyricsConfigureDialog.py \ + __init__.py \ + LyrcParser.py \ + AstrawebParse.py \ + LyeoslyricsParser.py Index: plugins/lyrics/lyrics/AstrawebParser.py =================================================================== --- plugins/lyrics/lyrics/AstrawebParser.py (revision 0) +++ plugins/lyrics/lyrics/AstrawebParser.py (revision 0) @@ -0,0 +1,82 @@ +# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- +# +# Copyright (C) 2007 James Livingston +# Copyright (C) 2007 Sirio Bolaños Puchet +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + +import urllib +import re +import rb + +class AstrawebParser (object): + def __init__(self, artist, title): + self.artist = artist + self.title = title + + def search(self, callback, *data): + wartist = re.sub('%20', '+', urllib.quote(self.artist)) + wtitle = re.sub('%20', '+', urllib.quote(self.title)) + + wurl = 'http://search.lyrics.astraweb.com/?word=%s+%s' % (wartist, wtitle) + + loader = rb.Loader() + loader.get_url (wurl, self.got_results, callback, *data) + + def got_results (self, result, callback, *data): + if result is None: + callback (None, *data) + return + + results = re.sub('\n', '', re.sub('\r', '', result)) + + if re.search('()([^<]*)', entry)[2] + title = re.split('(\/display[^>]*)([^<]*)', entry)[2][1:] + + if not ((re.search(self.title.lower().strip(), title.lower().strip()) is None)): + if not (re.search(self.artist.lower().strip(), artist.lower().strip()) is None): + loader = rb.Loader() + loader.get_url ('http://display.lyrics.astraweb.com' + url, self.parse_lyrics, callback, *data) + return + continue + + callback (None, *data) + return + + def parse_lyrics(self, result, callback, *data): + if result is None: + callback (None, *data) + return + + result = re.sub('\n', '', re.sub('\r', '', result)) + + artist_title = re.split('(Lyrics: )([^<]*)', result)[2] + artist = artist_title.split( " - " )[0] + title = artist_title.split( " - " )[1] + + title = "%s - %s\n\n" % (artist, title) + lyrics = re.split('(<font face=arial size=2>)(.*)(<\/font><br></td><td*)', result)[2] + lyrics = title + lyrics + lyrics = re.sub('<[Bb][Rr][^>]*>', '\n', lyrics) + lyrics += "\n\nLyrics provided by lyrics.astraweb.com" + + callback (lyrics, *data) + Index: plugins/lyrics/lyrics/LeoslyricsParser.py =================================================================== --- plugins/lyrics/lyrics/LeoslyricsParser.py (revision 0) +++ plugins/lyrics/lyrics/LeoslyricsParser.py (revision 0) @@ -0,0 +1,107 @@ +# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- +# +# Copyright (C) 2005 Eduardo Gonzalez +# Copyright (C) 2006 Jonathan Matthew +# Copyright (C) 2007 James Livingston +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + + +import urllib +import re +import rb +from xml.dom import minidom + + +class LeoslyricsParser(object): + def __init__(self, artist, title): + self.artist = artist + self.title = title + + def search(self, callback, *data): + artist = urllib.quote(self.artist) + title = urllib.quote(self.title) + + htstring = 'http://api.leoslyrics.com/api_search.php?auth=Rhythmbox&artist=%s&songtitle=%s' % (artist, title) + + loader = rb.Loader() + loader.get_url (htstring, self.got_lyrics, callback, *data) + + def got_lyrics (self, lyrics, callback, *data): + if lyrics is None: + callback (None, *data) + return + + try: + xmldoc = minidom.parseString(lyrics).documentElement + except e: + print e + callback (None, *data) + return + + result_code = xmldoc.getElementsByTagName('response')[0].getAttribute('code') + if result_code != '0': + xmldoc.unlink() + callback (None, *data) + return + + matches = xmldoc.getElementsByTagName('result')[:10] + + i = 0 + for match in matches: + title = match.getElementsByTagName('title')[0].firstChild.data + artist = match.getElementsByTagName('name')[0].firstChild.data + + if (re.search(self.title.lower().strip(), title.lower().strip()) and + re.search(self.artist.lower().strip(), artist.lower().strip())): + continue + + matches = matches[i:] + i += 1 + + hids = map(lambda x: x.getAttribute('hid'), matches) + + if len(hids) == 0: + xmldoc.unlink() + callback (None, *data) + return + + xmldoc.unlink() + + lurl = "http://api.leoslyrics.com/api_lyrics.php?auth=Rhythmbox&hid=%s" % (urllib.quote(hids[0].encode('utf-8'))) + loader = rb.Loader() + loader.get_url (lurl, self.parse_lyrics, callback, *data) + + + def parse_lyrics(self, result, callback, *data): + if result is None: + callback (None, *data) + return + + try: + xmldoc = minidom.parseString(result).documentElement + except e: + print e + callback (None, *data) + return + + lyrics = xmldoc.getElementsByTagName('title')[0].firstChild.nodeValue + lyrics += ' - ' + xmldoc.getElementsByTagName('artist')[0].getElementsByTagName('name')[0].firstChild.nodeValue + '\n\n' + lyrics += xmldoc.getElementsByTagName('text')[0].firstChild.nodeValue + xmldoc.unlink() + + lyrics += "\n\nLyrics provided by leoslyrics.com" + + callback (lyrics, *data) Index: plugins/lyrics/lyrics/LyrcParser.py =================================================================== --- plugins/lyrics/lyrics/LyrcParser.py (revision 0) +++ plugins/lyrics/lyrics/LyrcParser.py (revision 0) @@ -0,0 +1,78 @@ +# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- +# +# Copyright (C) 2007 James Livingston +# Copyright (C) 2007 Sirio Bolaños Puchet +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + +import urllib +import re +import rb + +EXPS = ['\n', '\r', '<[iI][mM][gG][^>]*>', '<[aA][^>]*>[^<]*<\/[aA]>', + '<[sS][cC][rR][iI][pP][tT][^>]*>[^<]*(<!--[^>]*>)*[^<]*<\/[sS][cC][rR][iI][pP][tT]>', + '<[sS][tT][yY][lL][eE][^>]*>[^<]*(<!--[^>]*>)*[^<]*<\/[sS][tT][yY][lL][eE]>'] +CEXPS = [re.compile (exp) for exp in EXPS] + +SEPARATOR_RE = re.compile("<[fF][oO][nN][tT][ ]*[sS][iI][zZ][eE][ ]*='2'[ ]*>") + + +class LyrcParser (object): + def __init__(self, artist, title): + self.artist = artist + self.title = title + + def search(self, callback, *data): + path = 'http://www.lyrc.com.ar/en/' + + wartist = urllib.quote(self.artist) + wtitle = urllib.quote(self.title) + wurl = 'tema1en.php?artist=%s&songname=%s' % (wartist, wtitle) + + loader = rb.Loader() + loader.get_url (path + wurl, self.got_lyrics, callback, *data) + + def got_lyrics(self, lyrics, callback, *data): + if lyrics is None: + callback (None, *data) + return + + for exp in CEXPS: + lyrics = exp.sub('', lyrics) + + lyricIndex = SEPARATOR_RE.search(lyrics) + + if lyricIndex is not None: + callback(self.parse_lyrics(SEPARATOR_RE.split(lyrics, 1)[1]), *data) + else: + callback (None, *data) + + def parse_lyrics(self, lyrics): + if re.search('<p><hr', lyrics): + lyrics = re.split('<p><hr', lyrics, 1)[0] + else: + lyrics = re.split('<br><br>', lyrics, 1)[0] + + lyrics = re.sub('<[fF][oO][nN][tT][^>]*>', '', lyrics) + title = re.split('(<[bB]>)([^<]*)', lyrics)[2] + artist = re.split('(<[uU]>)([^<]*)', lyrics)[2] + lyrics = re.sub('<[bB]>[^<].*<\/[tT][aA][bB][lL][eE]>', '', lyrics) + lyrics = re.sub('<[Bb][Rr][^>]*>', '\n', lyrics) + titl = "%s - %s\n\n" % (artist, title) + lyrics = titl + lyrics + lyrics += "\n\nLyrics provided by lyrc.com.ar" + + return lyrics + Index: plugins/lyrics/lyrics/LyricsConfigureDialog.py =================================================================== --- plugins/lyrics/lyrics/LyricsConfigureDialog.py (revision 0) +++ plugins/lyrics/lyrics/LyricsConfigureDialog.py (revision 0) @@ -0,0 +1,101 @@ +# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- +# +# Copyright (C) 2007 James Livingston +# Copyright (C) 2007 Sirio Bolaños Puchet +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + + + +import gtk, gtk.glade +import gconf +from os import system, path + +class LyricsConfigureDialog (object): + def __init__(self, glade_file, gconf_keys): + self.gconf = gconf.client_get_default() + self.gconf_keys = gconf_keys + self.gladexml = gtk.glade.XML(glade_file) + + self.dialog = self.gladexml.get_widget("preferences_dialog") + + self.toggle1 = self.gladexml.get_widget("engine1") + self.toggle2 = self.gladexml.get_widget("engine2") + self.toggle3 = self.gladexml.get_widget("engine3") + self.choose_button = self.gladexml.get_widget("choose_button") + self.path_display = self.gladexml.get_widget("path_display") + + self.choose_button.connect("clicked", self.choose_callback) + self.dialog.connect("response", self.dialog_response) + + # set fields from gconf + engines, self.folder = self.get_prefs() + self.path_display.set_text(self.folder) + self.toggle1.set_active('leoslyrics.com' in engines) + self.toggle2.set_active('lyrc.com.ar' in engines) + self.toggle3.set_active('astraweb.com' in engines) + + def dialog_response(self, dialog, response): + if response == gtk.RESPONSE_OK: + self.set_values() + self.dialog.hide() + elif response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: + self.dialog.hide() + else: + print "unexpected response type" + + + def set_values(self): + engines = [] + if self.toggle1.get_active(): + engines.append('astraweb.com') + if self.toggle2.get_active(): + engines.append('lyrc.com.ar') + if self.toggle3.get_active(): + engines.append('leoslyrics.com') + + if len(self.path_display.get_text()) is not 0: + self.folder = self.path_display.get_text() + + self.gconf.set_list(self.gconf_keys['engines'], gconf.VALUE_STRING, engines) + self.gconf.set_string(self.gconf_keys['folder'], self.folder) + + def choose_callback(self, widget): + def response_handler(widget, response): + if response == gtk.RESPONSE_OK: + path = self.chooser.get_filename() + self.chooser.destroy() + self.path_display.set_text(path) + else: + self.chooser.destroy() + + buttons = (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE, + gtk.STOCK_OK, gtk.RESPONSE_OK) + self.chooser = gtk.FileChooserDialog(title="Choose lyrics folder...", + parent=None, + action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, + buttons=buttons) + self.chooser.connect("response", response_handler) + self.chooser.set_modal(True) + self.chooser.set_transient_for(self.dialog) + self.chooser.present() + + def get_dialog (self): + return self.dialog + + def get_prefs (self): + engines = gconf.client_get_default().get_list(self.gconf_keys['engines'], gconf.VALUE_STRING) + folder = gconf.client_get_default().get_string(self.gconf_keys['folder']) + return (engines, folder) Index: plugins/lyrics/Makefile.am =================================================================== --- plugins/lyrics/Makefile.am (revision 5177) +++ plugins/lyrics/Makefile.am (working copy) @@ -1,13 +1,20 @@ -# Lyric display plugin +# Search Lyrics Python Plugin + +SUBDIRS = lyrics + plugindir = $(PLUGINDIR)/lyrics -plugin_PYTHON = lyrics.py -plugin_in_files = lyrics.rb-plugin.desktop.in %.rb-plugin: %.rb-plugin.desktop.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po) ; $(INTLTOOL_MERGE) $(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache - plugin_DATA = $(plugin_in_files:.rb-plugin.desktop.in=.rb-plugin) -EXTRA_DIST = $(plugin_in_files) +plugin_in_files = lyrics.rb-plugin.desktop.in + +glade_DATA = \ + lyrics-prefs.glade + +gladedir = $(plugindir) +EXTRA_DIST = $(plugin_in_files) $(glade_DATA) CLEANFILES = $(plugin_DATA) DISTCLEANFILES = $(plugin_DATA) + Index: plugins/lyrics/lyrics.py =================================================================== --- plugins/lyrics/lyrics.py (revision 5177) +++ plugins/lyrics/lyrics.py (working copy) @@ -1,319 +0,0 @@ -# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- -# -# Copyright 2005 Eduardo Gonzalez -# Copyright (C) 2006 Jonathan Matthew -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# TODO: -# - multiple lyrics sites (lyrc.com.ar, etc.) -# - handle multiple results usefully -# - save lyrics to disk? -# - check that the lyrics returned even remotely match the request? -# - share URL grabbing code with other plugins - -import os -import gtk, gobject -import urllib -import re -from xml.dom import minidom -import rb -import rhythmdb - -ui_str = """ -<ui> - <menubar name="MenuBar"> - <menu name="ViewMenu" action="View"> - <menuitem name="ViewSongLyrics" action="ViewSongLyrics"/> - </menu> - </menubar> -</ui> -""" - -LYRICS_FOLDER="~/.lyrics" -LYRIC_TITLE_STRIP=["\(live[^\)]*\)", "\(acoustic[^\)]*\)", "\([^\)]*mix\)", "\([^\)]*version\)", "\([^\)]*edit\)", "\(feat[^\)]*\)"] -LYRIC_TITLE_REPLACE=[("/", "-"), (" & ", " and ")] -LYRIC_ARTIST_REPLACE=[("/", "-"), (" & ", " and ")] - - -def create_lyrics_view(): - view = gtk.TextView() - view.set_wrap_mode(gtk.WRAP_WORD) - view.set_editable(False) - - sw = gtk.ScrolledWindow() - sw.add(view) - sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) - sw.set_shadow_type(gtk.SHADOW_IN) - - vbox = gtk.VBox(spacing=12) - vbox.pack_start(sw, expand=True) - return (vbox, view.get_buffer()) - -class LyricWindow(gtk.Window): - def __init__(self, db, parent, entry): - gtk.Window.__init__(self) - self.set_border_width(12) - self.set_transient_for(parent) - - title = db.entry_get(entry, rhythmdb.PROP_TITLE) - artist = db.entry_get(entry, rhythmdb.PROP_ARTIST) - self.set_title(title + " - " + artist + " - Lyrics") - - close = gtk.Button(stock=gtk.STOCK_CLOSE) - close.connect('clicked', lambda w: self.destroy()) - - (lyrics_view, buffer) = create_lyrics_view() - self.buffer = buffer - bbox = gtk.HButtonBox() - bbox.set_layout(gtk.BUTTONBOX_END) - bbox.pack_start(close) - lyrics_view.pack_start(bbox, expand=False) - - self.buffer.set_text(_("Searching for lyrics...")) - self.add(lyrics_view) - self.set_default_size(400, 300) - self.show_all() - -class LyricPane(object): - def __init__(self, db, song_info): - (self.view, self.buffer) = create_lyrics_view() - self.view.show_all() - self.page_num = song_info.append_page(_("Lyrics"), self.view) - self.db = db - self.song_info = song_info - self.have_lyrics = 0 - self.visible = 0 - self.entry = self.song_info.get_property("current-entry") - - self.entry_change_id = song_info.connect('notify::current-entry', self.entry_changed) - nb = self.view.get_parent() - self.switch_page_id = nb.connect('switch-page', self.switch_page_cb) - - def entry_changed(self, pspec, duh): - self.entry = self.song_info.get_property("current-entry") - self.have_lyrics = 0 - if self.visible != 0: - self.get_lyrics() - - def switch_page_cb(self, notebook, page, page_num): - if self.have_lyrics != 0: - return - - if page_num != self.page_num: - self.visible = 0 - return - - self.visible = 1 - self.get_lyrics() - - def get_lyrics(self): - if self.entry is None: - return - - self.buffer.set_text(_("Searching for lyrics...")); - lyrics_grabber = LyricGrabber() - lyrics_grabber.get_lyrics(self.db, self.entry, self.buffer.set_text) - - - -class LyricGrabber(object): - def __init__(self): - self.loader = rb.Loader () - - def _build_cache_path(self, artist, title): - lyrics_folder = os.path.expanduser (LYRICS_FOLDER) - if not os.path.exists (lyrics_folder): - os.mkdir (lyrics_folder) - - artist_folder = lyrics_folder + '/' + artist[:128] - if not os.path.exists (artist_folder): - os.mkdir (artist_folder) - - return artist_folder + '/' + title[:128] + '.lyric' - - def get_lyrics(self, db, entry, callback): - self.callback = callback - artist = db.entry_get(entry, rhythmdb.PROP_ARTIST).lower() - title = db.entry_get(entry, rhythmdb.PROP_TITLE).lower() - - # replace ampersands and the like - for exp in LYRIC_ARTIST_REPLACE: - p = re.compile (exp[0]) - artist = p.sub(exp[1], artist) - for exp in LYRIC_TITLE_REPLACE: - p = re.compile (exp[0]) - title = p.sub(exp[1], title) - - # strip things like "(live at Somewhere)", "(accoustic)", etc - for exp in LYRIC_TITLE_STRIP: - p = re.compile (exp) - title = p.sub ('', title) - - # compress spaces - title = title.strip() - artist = artist.strip() - - self.cache_path = self._build_cache_path(artist, title) - - if os.path.exists (self.cache_path): - self.loader.get_url(self.cache_path, callback) - return; - - url = "http://api.leoslyrics.com/api_search.php?auth=Rhythmbox&artist=%s&songtitle=%s" % ( - urllib.quote(artist.encode('utf-8')), - urllib.quote(title.encode('utf-8'))) - self.loader.get_url(url, self.search_results) - - def search_results(self, data): - if data is None: - self.callback("Server did not respond.") - return - - try: - xmldoc = minidom.parseString(data).documentElement - except: - self.callback("Couldn't parse search results.") - return - - result_code = xmldoc.getElementsByTagName('response')[0].getAttribute('code') - if result_code != '0': - self.callback("Server is busy, try again later.") - xmldoc.unlink() - return - - # We don't really need the top 100 matches, so I'm limiting it to ten - matches = xmldoc.getElementsByTagName('result')[:10] - #songs = map(lambda x: - # x.getElementsByTagName('name')[0].firstChild.nodeValue - # + " - " + - # x.getElementsByTagName('title')[0].firstChild.nodeValue, - # matches) - hids = map(lambda x: x.getAttribute('hid'), matches) - #exacts = map(lambda x: x.getAttribute('exactMatch'), matches) - - if len(hids) == 0: - # FIXME show other matches - self.callback("Unable to find lyrics for this track.") - xmldoc.unlink() - return - - #songlist = [] - #for i in range(len(hids)): - # songlist.append((songs[i], hids[i], exacts[i])) - - xmldoc.unlink() - url = "http://api.leoslyrics.com/api_lyrics.php?auth=Rhythmbox&hid=%s" % (urllib.quote(hids[0].encode('utf-8'))) - self.loader.get_url(url, self.lyrics) - - - def lyrics(self, data): - if data is None: - self.callback("Unable to find lyrics for this track.") - return - - try: - xmldoc = minidom.parseString(data).documentElement - except: - self.callback("Unable to parse the lyrics returned.") - return - - text = xmldoc.getElementsByTagName('title')[0].firstChild.nodeValue - text += ' - ' + xmldoc.getElementsByTagName('artist')[0].getElementsByTagName('name')[0].firstChild.nodeValue + '\n\n' - text += xmldoc.getElementsByTagName('text')[0].firstChild.nodeValue - xmldoc.unlink() - - text += "\n\n"+_("Lyrics provided by leoslyrics.com") - - - f = file (self.cache_path, 'w') - f.write (text) - f.close () - - self.callback(text) - - - -class LyricsDisplayPlugin(rb.Plugin): - - def __init__ (self): - rb.Plugin.__init__ (self) - self.window = None - - def activate (self, shell): - self.action = gtk.Action ('ViewSongLyrics', _('Song L_yrics'), - _('Display lyrics for the playing song'), - 'rb-song-lyrics') - self.activate_id = self.action.connect ('activate', self.show_song_lyrics, shell) - - self.action_group = gtk.ActionGroup ('SongLyricsPluginActions') - self.action_group.add_action (self.action) - - uim = shell.get_ui_manager () - uim.insert_action_group (self.action_group, 0) - self.ui_id = uim.add_ui_from_string (ui_str) - uim.ensure_update () - - sp = shell.get_player () - self.pec_id = sp.connect('playing-song-changed', self.playing_entry_changed) - self.playing_entry_changed (sp, sp.get_playing_entry ()) - - self.csi_id = shell.connect('create_song_info', self.create_song_info) - - def deactivate (self, shell): - - uim = shell.get_ui_manager() - uim.remove_ui (self.ui_id) - uim.remove_action_group (self.action_group) - - self.action_group = None - self.action = None - - shell.disconnect (self.csi_id) - - sp = shell.get_player () - sp.disconnect (self.pec_id) - - if self.window is not None: - self.window.destroy () - - - def playing_entry_changed (self, sp, entry): - if entry is not None: - self.action.set_sensitive (True) - else: - self.action.set_sensitive (False) - - def show_song_lyrics (self, action, shell): - - if self.window is not None: - self.window.destroy () - - db = shell.get_property ("db") - sp = shell.get_player () - entry = sp.get_playing_entry () - - if entry is None: - return - - self.window = LyricWindow(db, shell.get_property ("window"), entry) - lyrics_grabber = LyricGrabber() - lyrics_grabber.get_lyrics(db, entry, self.window.buffer.set_text) - - def create_song_info (self, shell, song_info, is_multiple): - - if is_multiple is False: - x = LyricPane(shell.get_property ("db"), song_info) - Index: plugins/lyrics/lyrics-prefs.glade =================================================================== --- plugins/lyrics/lyrics-prefs.glade (revision 0) +++ plugins/lyrics/lyrics-prefs.glade (revision 0) @@ -0,0 +1,669 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd"> +<!--*- mode: xml -*--> +<glade-interface> + <widget class="GtkDialog" id="preferences_dialog"> + <property name="visible">True</property> + <property name="title" translatable="yes">Lyrics Plugin Preferences</property> + <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkVBox" id="lyrics_vbox"> + <property name="visible">True</property> + <property name="border_width">5</property> + <property name="spacing">10</property> + <child> + <widget class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">GTK_SHADOW_NONE</property> + <child> + <widget class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkCheckButton" id="engine1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">Astraweb (astraweb.com)</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="engine2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">Lyrc (lyrc.com.ar)</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="engine3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">Leo's Lyrics (leoslyrics.com)</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label58"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><b>Search engines</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkFrame" id="frame2"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">GTK_SHADOW_NONE</property> + <child> + <widget class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkHBox" id="hbox23"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkEntry" id="path_display"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </widget> + </child> + <child> + <widget class="GtkButton" id="choose_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <widget class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="xscale">0</property> + <property name="yscale">0</property> + <child> + <widget class="GtkHBox" id="hbox24"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="stock">gtk-open</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label60"> + <property name="visible">True</property> + <property name="label" translatable="yes">Browse...</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label59"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><b>Lyrics Folder</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">2</property> + </packing> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="layout_style">GTK_BUTTONBOX_EDGE</property> + <child> + <widget class="GtkButton" id="cancel_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">gtk-cancel</property> + <property name="use_stock">True</property> + <property name="response_id">-6</property> + </widget> + </child> + <child> + <widget class="GtkButton" id="ok_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">gtk-ok</property> + <property name="use_stock">True</property> + <property name="response_id">-5</property> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">GTK_PACK_END</property> + </packing> + </child> + </widget> + </child> + </widget> +</glade-interface> +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd"> +<!--*- mode: xml -*--> +<glade-interface> + <widget class="GtkDialog" id="preferences_dialog"> + <property name="visible">True</property> + <property name="title" translatable="yes">Lyrics Plugin Preferences</property> + <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkVBox" id="lyrics_vbox"> + <property name="visible">True</property> + <property name="border_width">5</property> + <property name="spacing">10</property> + <child> + <widget class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">GTK_SHADOW_NONE</property> + <child> + <widget class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkCheckButton" id="engine1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">Astraweb (astraweb.com)</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="engine2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">Lyrc (lyrc.com.ar)</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="engine3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">Leo's Lyrics (leoslyrics.com)</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label58"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><b>Search engines</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkFrame" id="frame2"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">GTK_SHADOW_NONE</property> + <child> + <widget class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkHBox" id="hbox23"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkEntry" id="path_display"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </widget> + </child> + <child> + <widget class="GtkButton" id="choose_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <widget class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="xscale">0</property> + <property name="yscale">0</property> + <child> + <widget class="GtkHBox" id="hbox24"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="stock">gtk-open</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label60"> + <property name="visible">True</property> + <property name="label" translatable="yes">Browse...</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label59"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><b>Lyrics Folder</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">2</property> + </packing> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="layout_style">GTK_BUTTONBOX_EDGE</property> + <child> + <widget class="GtkButton" id="cancel_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">gtk-cancel</property> + <property name="use_stock">True</property> + <property name="response_id">-6</property> + </widget> + </child> + <child> + <widget class="GtkButton" id="ok_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">gtk-ok</property> + <property name="use_stock">True</property> + <property name="response_id">-5</property> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">GTK_PACK_END</property> + </packing> + </child> + </widget> + </child> + </widget> +</glade-interface> +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd"> +<!--*- mode: xml -*--> +<glade-interface> + <widget class="GtkDialog" id="preferences_dialog"> + <property name="visible">True</property> + <property name="title" translatable="yes">Lyrics Plugin Preferences</property> + <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkVBox" id="lyrics_vbox"> + <property name="visible">True</property> + <property name="border_width">5</property> + <property name="spacing">10</property> + <child> + <widget class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">GTK_SHADOW_NONE</property> + <child> + <widget class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkCheckButton" id="engine1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">Astraweb (astraweb.com)</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="engine2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">Lyrc (lyrc.com.ar)</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkCheckButton" id="engine3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label">Leo's Lyrics (leoslyrics.com)</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label58"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><b>Search engines</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkFrame" id="frame2"> + <property name="visible">True</property> + <property name="label_xalign">0</property> + <property name="shadow_type">GTK_SHADOW_NONE</property> + <child> + <widget class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="left_padding">12</property> + <child> + <widget class="GtkHBox" id="hbox23"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <widget class="GtkEntry" id="path_display"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </widget> + </child> + <child> + <widget class="GtkButton" id="choose_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <widget class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="xscale">0</property> + <property name="yscale">0</property> + <child> + <widget class="GtkHBox" id="hbox24"> + <property name="visible">True</property> + <property name="spacing">2</property> + <child> + <widget class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="stock">gtk-open</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <widget class="GtkLabel" id="label60"> + <property name="visible">True</property> + <property name="label" translatable="yes">Browse...</property> + <property name="use_underline">True</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + </child> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label59"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><b>Lyrics Folder</b></property> + <property name="use_markup">True</property> + </widget> + <packing> + <property name="type">label_item</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="position">2</property> + </packing> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="layout_style">GTK_BUTTONBOX_EDGE</property> + <child> + <widget class="GtkButton" id="cancel_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">gtk-cancel</property> + <property name="use_stock">True</property> + <property name="response_id">-6</property> + </widget> + </child> + <child> + <widget class="GtkButton" id="ok_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">gtk-ok</property> + <property name="use_stock">True</property> + <property name="response_id">-5</property> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">GTK_PACK_END</property> + </packing> + </child> + </widget> + </child> + </widget> +</glade-interface>