Exemple #1
0
    def do_activate(self):
        # Get references for the Shell and the Shell-player
        self.shell = self.object
        self.player = self.shell.props.shell_player
        self.appshell = ApplicationShell(self.shell)

        # Create dictionary which assigns sources to their corresponding modules
        self.dict = dict({"Kuwo.cn": KuwoParser, "Baidu.com": BaiduParser, "Musixmatch": MusixmatchParser,
                          "Lyricwiki.org": LyricwikiParser, "Letras.terra.com.br": LetrasTerraParser,
                          "Metrolyrics.com": MetrolyricsParser, "AZLyrics.com": AZLyricsParser,
                          "Lyricsmania.com": LyricsmaniaParser, "Chartlyrics.com": ChartlyricsParser,
                          "Darklyrics.com": DarklyricsParser, "Genius.com": GeniusParser,
                          "Vagalume.com.br": VagalumeParser, "lyrics.alsong.co.kr": ALSongLyricsParser,
                          "External": External})
        self.add_builtin_lyrics_sources()

        # Get the user preferences
        config = Config()
        self.settings = config.get_settings()
        self.get_user_preferences(self.settings, None, config)
        # Watch for setting changes
        self.skc_id = self.settings.connect('changed', self.get_user_preferences, config)

        self.position = RB.ShellUILocation.RIGHT_SIDEBAR
        if self.left_sidebar:
            self.position = RB.ShellUILocation.SIDEBAR

        # Initialize the UI
        self.lmui_id = None
        self.tbui_id = None
        self.init_sidebar()
        self.init_menu()

        # Create flag, used to pop out sidebar on initial start of playback
        self.first = True
        self.visible = False

        # Event flag indicates whether the user is currently editing lyrics
        self.edit_event = threading.Event()
        self.edit_event.set()

        self.current_source = None
        self.tags = None
        self.current_tag = None
        self.showing_on_demand = False
        self.was_corrected = False

        # Search lyrics if already playing (this will be the case if user reactivates plugin during playback)
        if self.player.props.playing:
            self.search_lyrics(self.player, self.player.get_playing_entry())
        # Search lyrics everytime the song changes 
        self.psc_id = self.player.connect('playing-song-changed', self.search_lyrics)
        # Connect to elapsed-changed signal to handle synchronized lyrics
        self.pec_id = self.player.connect('elapsed-changed', self.elapsed_changed)

        print("activated plugin lLyrics")
Exemple #2
0
    def do_activate(self):
        # Get references for the Shell and the Shell-player
        self.shell = self.object
        self.player = self.shell.props.shell_player
        self.appshell = ApplicationShell(self.shell)
        self.add_builtin_lyrics_sources()

        # Get the user preferences
        config = Config()
        self.settings = config.get_settings()
        self.get_user_preferences(self.settings, None, config)
        # Watch for setting changes
        self.skc_id = self.settings.connect('changed',
                                            self.get_user_preferences, config)

        self.position = RB.ShellUILocation.RIGHT_SIDEBAR
        if self.left_sidebar:
            self.position = RB.ShellUILocation.SIDEBAR

        # Initialize the UI
        self.lmui_id = None
        self.tbui_id = None
        self.init_sidebar()
        self.init_menu()

        # Create flag, used to pop out sidebar on initial start of playback
        self.first = True
        self.visible = False

        # Event flag indicates whether the user is currently editing lyrics
        self.edit_event = threading.Event()
        self.edit_event.set()

        self.tags = None
        self.current_tag = None
        self.showing_on_demand = False
        self.was_corrected = False

        self.location = None
        #for tests otherwise will be read from preferences
        self.overwrite = True
        self.cache = True

        # Search lyrics if already playing
        # (this will be the case if user reactivates plugin during playback)
        if self.player.props.playing:
            self.search_lyrics(self.player, self.player.get_playing_entry())
        # Search lyrics everytime the song changes
        self.psc_id = self.player.connect('playing-song-changed',
                                          self.search_lyrics)
        # Connect to elapsed-changed signal to handle synchronized lyrics
        self.pec_id = self.player.connect('elapsed-changed',
                                          self.elapsed_changed)

        print("activated plugin lLyrics")
Exemple #3
0
 def do_activate(self):
     # Get references for the Shell and the Shell-player
     self.shell = self.object
     self.player = self.shell.props.shell_player
     self.appshell = ApplicationShell(self.shell)
             
     # Create dictionary which assigns sources to their corresponding modules
     self.dict = dict({"Lyricwiki.org": LyricwikiParser, "Letras.terra.com.br": LetrasTerraParser,
                      "Metrolyrics.com": MetrolyricsParser, "AZLyrics.com": AZLyricsParser,
                      "Lyricsmania.com": LyricsmaniaParser, "Chartlyrics.com": ChartlyricsParser,
                      "Lyrdb.com": LyrdbParser, "Leoslyrics.com": LeoslyricsParser, 
                      "Darklyrics.com": DarklyricsParser, "Rapgenius.com": RapgeniusParser, 
                      "Lyricsnmusic.com": LyricsNMusicParser, "Vagalume.com.br": VagalumeParser,
                      "GeCi.me": GeCiMeParser, "MP3.Baidu.com": BaiduMusicParser,
                      "External": External})
     self.add_builtin_lyrics_sources()
     
     # Get the user preferences
     config = Config()
     self.settings = config.get_settings()
     self.get_user_preferences(self.settings, None, config)
     # Watch for setting changes
     self.skc_id = self.settings.connect('changed', self.get_user_preferences, config)
     
     self.position = RB.ShellUILocation.RIGHT_SIDEBAR
     if self.left_sidebar:
         self.position = RB.ShellUILocation.SIDEBAR
     
     # Initialize the UI
     self.lmui_id = None
     self.tbui_id = None 
     self.init_sidebar()
     self.init_menu()
     
     # Create flag, used to pop out sidebar on initial start of playback
     self.first = True
     self.visible = False
     
     # Event flag indicates whether the user is currently editing lyrics
     self.edit_event = threading.Event()
     self.edit_event.set()
     
     self.current_source = None
     self.tags = None
     self.current_tag = None
     self.showing_on_demand = False
     self.was_corrected = False
     
     # Search lyrics if already playing (this will be the case if user reactivates plugin during playback)
     if self.player.props.playing:
             self.search_lyrics(self.player, self.player.get_playing_entry())
     # Search lyrics everytime the song changes 
     self.psc_id = self.player.connect('playing-song-changed', self.search_lyrics)
     # Connect to elapsed-changed signal to handle synchronized lyrics
     self.pec_id = self.player.connect('elapsed-changed', self.elapsed_changed)
            
     print("activated plugin lLyrics")
Exemple #4
0
    def do_activate(self):
        # Get references for the Shell and the Shell-player
        self.shell = self.object
        self.player = self.shell.props.shell_player
        self.appshell = ApplicationShell(self.shell)
        self.add_builtin_lyrics_sources()

        # Get the user preferences
        config = Config()
        self.settings = config.get_settings()
        self.get_user_preferences(self.settings, None, config)
        # Watch for setting changes
        self.skc_id = self.settings.connect('changed',
                                            self.get_user_preferences, config)

        self.position = RB.ShellUILocation.RIGHT_SIDEBAR
        if self.left_sidebar:
            self.position = RB.ShellUILocation.SIDEBAR

        # Initialize the UI
        self.lmui_id = None
        self.tbui_id = None
        self.init_sidebar()
        self.init_menu()

        # Create flag, used to pop out sidebar on initial start of playback
        self.first = True
        self.visible = False

        # Event flag indicates whether the user is currently editing lyrics
        self.edit_event = threading.Event()
        self.edit_event.set()

        self.tags = None
        self.current_tag = None
        self.showing_on_demand = False
        self.was_corrected = False

        self.location = None
        #for tests otherwise will be read from preferences
        self.overwrite = True
        self.cache = True

        # Search lyrics if already playing
        # (this will be the case if user reactivates plugin during playback)
        if self.player.props.playing:
                self.search_lyrics(self.player,
                                   self.player.get_playing_entry())
        # Search lyrics everytime the song changes
        self.psc_id = self.player.connect('playing-song-changed',
                                          self.search_lyrics)
        # Connect to elapsed-changed signal to handle synchronized lyrics
        self.pec_id = self.player.connect('elapsed-changed',
                                          self.elapsed_changed)

        print("activated plugin lLyrics")
Exemple #5
0
class lLyrics(GObject.Object, Peas.Activatable):
    __gtype_name__ = 'lLyrics'
    object = GObject.property(type=GObject.Object)
    
    
    
    def __init__(self):
        GObject.Object.__init__(self)
        GObject.threads_init()
        Gdk.threads_init()
        
        

    def do_activate(self):
        # Get references for the Shell and the Shell-player
        self.shell = self.object
        self.player = self.shell.props.shell_player
        self.appshell = ApplicationShell(self.shell)
                
        # Create dictionary which assigns sources to their corresponding modules
        self.dict = dict({"Lyricwiki.org": LyricwikiParser, "Letras.terra.com.br": LetrasTerraParser,
                         "Metrolyrics.com": MetrolyricsParser, "AZLyrics.com": AZLyricsParser,
                         "Lyricsmania.com": LyricsmaniaParser, "Chartlyrics.com": ChartlyricsParser,
                         "Lyrdb.com": LyrdbParser, "Leoslyrics.com": LeoslyricsParser, 
                         "Darklyrics.com": DarklyricsParser, "Rapgenius.com": RapgeniusParser, 
                         "Lyricsnmusic.com": LyricsNMusicParser, "Vagalume.com.br": VagalumeParser,
                         "External": External})
        self.add_builtin_lyrics_sources()
        
        # Get the user preferences
        config = Config()
        self.settings = config.get_settings()
        self.get_user_preferences(self.settings, None, config)
        # Watch for setting changes
        self.skc_id = self.settings.connect('changed', self.get_user_preferences, config)
        
        self.position = RB.ShellUILocation.RIGHT_SIDEBAR
        if self.left_sidebar:
            self.position = RB.ShellUILocation.SIDEBAR
        
        # Initialize the UI
        self.lmui_id = None
        self.tbui_id = None 
        self.init_sidebar()
        self.init_menu()
        
        # Create flag, used to pop out sidebar on initial start of playback
        self.first = True
        self.visible = False
        
        # Event flag indicates whether the user is currently editing lyrics
        self.edit_event = threading.Event()
        self.edit_event.set()
        
        self.current_source = None
        self.tags = None
        self.current_tag = None
        self.showing_on_demand = False
        self.was_corrected = False
        
        # Search lyrics if already playing (this will be the case if user reactivates plugin during playback)
        if self.player.props.playing:
                self.search_lyrics(self.player, self.player.get_playing_entry())
        # Search lyrics everytime the song changes 
        self.psc_id = self.player.connect('playing-song-changed', self.search_lyrics)
        # Connect to elapsed-changed signal to handle synchronized lyrics
        self.pec_id = self.player.connect('elapsed-changed', self.elapsed_changed)
               
        print("activated plugin lLyrics")
        
        

    def do_deactivate(self):
        if self.visible:
            self.shell.remove_widget (self.vbox, self.position)
        
        self.settings.disconnect(self.skc_id)
        if self.psc_id is not None:
            self.player.disconnect(self.psc_id)
            self.player.disconnect(self.pec_id)
        
        self.appshell.cleanup()

        self.vbox = None
        self.textbuffer = None
        self.textview = None
        self.psc_id = None
        self.visible = None
        self.player = None
        self.toggle_action_group = None
        self.context_action_group = None
        self.appshell = None
        self.menu = None
        self.button_menu = None
        self.cache = None
        self.dict = None
        self.sources = None
        self.ui_id = None
        self.tag = None
        self.first = None
        self.current_source = None
        self.artist = None
        self.title = None
        self.clean_artist = None
        self.clean_title = None
        self.path = None
        self.lyrics_before_edit = None
        self.edit_event = None
        self.path_before_edit = None
        self.sources = None
        self.cache = None
        self.lyrics_folder = None
        self.ignore_brackets = None
        self.left_sidebar = None
        self.hide_label = None
        self.show_first = None
        self.position = None
        self.hbox = None
        self.back_button = None
        
        self.shell = None

        print("deactivated plugin lLyrics")
    
    
    
    def add_builtin_lyrics_sources(self):
        # find and append path for built-in lyrics plugin
        for p in sys.path:
            if p.endswith("/rhythmbox/plugins/rb"):
                path = p.replace("/rb", "/lyrics")
                sys.path.append(path)
                break
        else:
            print("Path to built-in lyrics plugin could not be detected")
            return
    
        
    
    def get_user_preferences(self, settings, key, config):
        self.sources = config.get_lyrics_sources()
        self.show_first = config.get_show_first()
        self.cache = config.get_cache_lyrics()
        self.lyrics_folder = config.get_lyrics_folder()
        self.ignore_brackets = config.get_ignore_brackets()
        self.left_sidebar = config.get_left_sidebar()
        self.hide_label = config.get_hide_label()
        
        # if this is called in do_activate or we need a reload to apply, return here
        if key is None or key in ["left-sidebar"]:
            return
        
        if key == "hide-label":
            if self.hide_label:
                self.label.hide()
            else:
                self.label.show()
            return   

        
           
    def insert_ui(self):
        self.appshell.add_app_menuitems(view_menu_ui, 'lLyricsPluginToggleActions', 'view')
        self.appshell.add_browser_menuitems(context_ui, 'lLyricsPluginPopupActions')    
    
    
    
    def init_menu(self):
        # add actions
        self.toggle_action_group = ActionGroup(self.shell, 'lLyricsPluginToggleActions')
        self.toggle_action_group.add_action(func=self.toggle_visibility,
            action_name='ToggleLyricSideBar', label=_("Lyrics"), action_state=ActionGroup.TOGGLE,
            action_type='app', accel="<Ctrl>l", tooltip=_("Display lyrics for the current playing song"))
        self.appshell.insert_action_group(self.toggle_action_group)
        
        self.context_action_group = ActionGroup(self.shell, 'lLyricsPluginPopupActions')
        self.context_action_group.add_action(action_name="lLyricsPopupAction", label=_("Show lyrics"),
                            tooltip=_("Search and display lyrics for this song"), func=self.context_action_callback)
        self.appshell.insert_action_group(self.context_action_group)
        
        self.insert_ui()
        
        
        
               
    def init_sidebar(self):
        self.vbox = Gtk.VBox()
        
        hbox_header = Gtk.HBox();
                
        self.label = Gtk.Label(_("Lyrics"))
        self.label.set_use_markup(True)
        self.label.set_padding(3, 10)
        self.label.set_alignment(0, 0)
        
        self.menu = self.get_button_menu()
        self.set_menu_sensitive(False)
        
        # menu without toolbar
        icon_factory = Gtk.IconFactory()
        pxbf = GdkPixbuf.Pixbuf.new_from_file(os.path.dirname(__file__) + "/menu-arrow.png")
        icon_factory.add("llyrics_menu", Gtk.IconSet.new_from_pixbuf(pxbf))
        icon_factory.add_default()
        
        menu_button = Gtk.Image.new_from_stock("llyrics_menu", Gtk.IconSize.SMALL_TOOLBAR);
        eventBox = Gtk.EventBox();
        eventBox.add(menu_button);
        eventBox.connect("button-press-event", self.popup_menu, self.menu)
        
        hbox_header.pack_start(self.label, True, True, 0)
        hbox_header.pack_end(eventBox, False, False, 5)
        
        # create a TextView for displaying lyrics
        self.textview = Gtk.TextView()
        self.textview.set_editable(False)
        self.textview.set_cursor_visible(False)
        self.textview.set_left_margin(10)
        self.textview.set_right_margin(10)
        self.textview.set_pixels_above_lines(5)
        self.textview.set_pixels_below_lines(5)
        self.textview.set_wrap_mode(Gtk.WrapMode.WORD)
                
        # create a ScrollView
        sw = Gtk.ScrolledWindow()
        sw.add(self.textview)
        sw.set_shadow_type(Gtk.ShadowType.IN)
        
        # initialize a TextBuffer to store lyrics in
        self.textbuffer = Gtk.TextBuffer()
        self.textview.set_buffer(self.textbuffer)
        
        # tag to style headers bold and underlined
        self.tag = self.textbuffer.create_tag(None, underline=Pango.Underline.SINGLE, weight=600, 
                                              pixels_above_lines=10, pixels_below_lines=20)
        # tag to highlight synchronized lyrics
        self.sync_tag = self.textbuffer.create_tag(None, weight=600)
        
        # create save and cancel buttons for edited lyrics
        save_button = Gtk.Button.new_with_label(_("Save"))
        save_button.connect("clicked", self.save_button_callback)
        cancel_button = Gtk.Button.new_with_label(_("Cancel"))
        cancel_button.connect("clicked", self.cancel_button_callback)
        self.hbox = Gtk.HBox()
        self.hbox.pack_start(save_button, True, True, 3)
        self.hbox.pack_start(cancel_button, True, True, 3)
        
        # button for closing on demand lyrics
        self.back_button = Gtk.Button.new_with_label(_("Back to playing song"))
        self.back_button.connect("clicked", self.back_button_callback)
                
        # pack everything into side pane
        self.vbox.pack_start(hbox_header, False, False, 0);
        self.vbox.pack_start(sw, True, True, 0)
        self.vbox.pack_end(self.hbox, False, False, 3)
        self.vbox.pack_end(self.back_button, False, False, 3)        

        self.vbox.show_all()
        self.hbox.hide()
        self.back_button.hide()
        
        if self.hide_label:
            self.label.hide()
            
        self.vbox.set_size_request(200, -1)
        self.visible = False
        
        
        
    def get_button_menu(self):
        menu = Gtk.Menu();
        
        self.radio_sources = Gtk.Menu()
        
        item_unselect = Gtk.RadioMenuItem.new_with_label([], "SelectNothing")
        item_unselect.connect("activate", self.scan_selected_source_callback, "SelectNothing")
        self.radio_sources.append(item_unselect)
        
        last_item = item_unselect
        
        for entry in LYRICS_SOURCES[:-1]:
            last_item = self.add_radio_menu_item(self.radio_sources, entry, self.scan_selected_source_callback, last_item)
        
        self.radio_sources.append(Gtk.SeparatorMenuItem())
        last_item = self.add_radio_menu_item(self.radio_sources, _("External"), self.scan_selected_source_callback, last_item)
        self.radio_sources.append(Gtk.SeparatorMenuItem())
        self.add_radio_menu_item(self.radio_sources, _("From cache file"), self.scan_selected_source_callback, last_item)
        
        self.radio_sources.show_all();
        
        item_sources = Gtk.MenuItem(_("Sources"));
        item_sources.set_submenu(self.radio_sources);
        menu.append(item_sources)
        
        self.add_menu_item(menu, _("Scan next source"), self.scan_next_action_callback)
        self.add_menu_item(menu, _("Scan all sources"), self.scan_all_action_callback)
        menu.append(Gtk.SeparatorMenuItem())
        self.add_menu_item(menu, _("Search online"), self.search_online_action_callback)
        menu.append(Gtk.SeparatorMenuItem())
        self.add_menu_item(menu, _("Mark as instrumental"), self.instrumental_action_callback)
        menu.append(Gtk.SeparatorMenuItem())
        self.add_menu_item(menu, _("Clear lyrics"), self.clear_action_callback)
        self.add_menu_item(menu, _("Edit lyrics"), self.edit_action_callback)
        
        # add preferences item
        menu.append(Gtk.SeparatorMenuItem())
        self.add_menu_item(menu, _("Preferences"), self.preferences_dialog_action_callback)
        
        menu.show_all()
        
        # hide the SelectNothing choice
        item_unselect.hide()
        
        return menu
    
    
    
    def add_menu_item(self, menu, label, callback):
        item = Gtk.MenuItem(label)
        item.connect("activate", callback)            
        menu.append(item)
    
    
    
    def add_radio_menu_item(self, menu, label, callback, last):
        group = last.get_group()
        item = Gtk.RadioMenuItem.new_with_label(group, label)
        if label == _("External"):
            label = "External"
        if label == _("From cache file"):
            label = "From cache file"
        item.connect("toggled", callback, label)
        menu.append(item)
        
        return item
            
    
    
    def set_menu_sensitive(self, sensitive):
        index = 0
        for item in self.menu:
            # 'Preferences' option should always be sensitive
            if index == len(self.menu)-1:
                continue
            item.set_sensitive(sensitive)
            index += 1
    
    
    
    def set_radio_menu_item_active(self, itemlabel):
        for item in self.radio_sources:
            if item.get_label() == itemlabel:
                item.set_active(True)
                break
            
    
    
    def popup_menu(self, widget, event, menu):
        menu.popup(None, None, lambda x,y: (event.x_root+event.x, event.y_root+event.y, True), None, event.button, event.time)
        
        
    
    def toggle_visibility(self, action, param=None, data=None):
        action = self.toggle_action_group.get_action('ToggleLyricSideBar')
        
        if action.get_active():
            self.shell.add_widget(self.vbox, self.position, True, True)
            self.visible = True
            if not self.first and not self.showing_on_demand:
                self.search_lyrics(self.player, self.player.get_playing_entry())
        else:
            self.shell.remove_widget(self.vbox, self.position)
            self.visible = False
                    
            
        
    def search_lyrics(self, player, entry):
        # clear sync stuff
        if self.tags is not None:
            self.tags = None
            self.current_tag = None
            start, end = self.textbuffer.get_bounds()
            self.textbuffer.remove_tag(self.sync_tag, start, end)
                
        if entry is None:
            return
        
        # don't show lyrics for podcasts and radio
        if entry.get_entry_type().get_name() in ('iradio', 'podcast-post'):
            print("entry type: " + entry.get_entry_type().get_name())
            if not self.first:
                self.first = True
                print('removing the sidebar')
                self.toggle_action_group.get_action("ToggleLyricSideBar").set_active(False)
            return
        
        # pop out sidebar at first playback
        if self.first and not self.showing_on_demand:
            self.first = False
            if not self.visible and self.show_first:
                self.toggle_action_group.get_action("ToggleLyricSideBar").set_active(True)
                # toggling the sidebar will start lyrics search again, so we can return here
                return
        
        # only do something if visible
        if not self.visible:
            return
            
        # get the song data
        self.artist = entry.get_string(RB.RhythmDBPropType.ARTIST)
        self.title = entry.get_string(RB.RhythmDBPropType.TITLE)

        print("search lyrics for " + self.artist + " - " + self.title)
        
        self.was_corrected = False
   
        (self.clean_artist, self.clean_title) = self.clean_song_data(self.artist, self.title)
        self.path = self.build_cache_path(self.clean_artist, self.clean_title)
        
        self.scan_all_sources(self.clean_artist, self.clean_title, True)
        
        

    def clean_song_data(self, artist, title):
        # convert to lowercase
        artist = artist.lower()
        title = title.lower()
        
        # remove accents
        artist = unicodedata.normalize('NFKD', artist)
        artist = "".join([c for c in artist if not unicodedata.combining(c)])
        title = unicodedata.normalize('NFKD', title)
        title = "".join([c for c in title if not unicodedata.combining(c)])
        
        if self.ignore_brackets:
            LYRICS_TITLE_STRIP.append("\(.*\)")
    
        # replace ampersands and the like
        for exp in LYRICS_ARTIST_REPLACE:
            artist = re.sub(exp[0], exp[1], artist)
        for exp in LYRICS_TITLE_REPLACE:
            title = re.sub(exp[0], exp[1], title)
    
        # strip things like "(live at Somewhere)", "(acoustic)", etc
        for exp in LYRICS_TITLE_STRIP:
            title = re.sub (exp, '', title)
    
        # compress spaces
        title = title.strip()
        artist = artist.strip()
                
        return (artist, title)
    
    
    
    def build_cache_path(self, artist, title):
        artist_folder = os.path.join(self.lyrics_folder, artist[:128])
        if not os.path.exists (artist_folder):
            os.mkdir (artist_folder)
    
        return os.path.join(artist_folder, title[:128] + '.lyric')
    
    
    
    def scan_selected_source_callback(self, action, activated_action):
        if not action.get_active():
            return 
        
        source = activated_action
        if source == "SelectNothing" or source == self.current_source:
            return
        
        self.scan_source(source, self.clean_artist, self.clean_title)
        
        
        
    def scan_next_action_callback(self, action):
        if self.current_source is None or self.current_source == "From cache file":
            index = 0
        else:
            index = self.sources.index(self.current_source) + 1
            index = index % (len(self.sources)+1)
            
        if index >= len(self.sources):
            source = "From cache file"
        else:
            source = self.sources[index]
        
        self.scan_source(source, self.clean_artist, self.clean_title)
        
        
    
    def scan_all_action_callback(self, action):
        self.scan_all_sources(self.clean_artist, self.clean_title, False)
        
    
        
    def search_online_action_callback(self, action):
        webbrowser.open("http://www.google.com/search?q=%s+%s+lyrics" % (self.clean_artist, self.clean_title))
    
    
            
    def instrumental_action_callback(self, action):
        lyrics = "-- Instrumental --"
        self.write_lyrics_to_cache(self.path, lyrics)
        self.current_source = None
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.show_lyrics, lyrics)
        
        
        
    def save_to_cache_action_callback(self, action):
        start, end = self.textbuffer.get_bounds()
        start.forward_lines(1)
        lyrics = self.textbuffer.get_text(start, end, False)
        
        self.write_lyrics_to_cache(self.path, lyrics)
        
        
        
    def clear_action_callback(self, action):
        self.textbuffer.set_text("")
        try:
            os.remove(self.path)
        except:
            print("No cache file found to clear")
        print("cleared lyrics")
        
        
        
    def edit_action_callback(self, action):
        # Unset event flag to indicate editing and so block all other threads which 
        # want to display new lyrics until editing is finished.
        self.edit_event.clear()
        
        # Conserve lyrics in order to restore original lyrics when editing is canceled 
        start, end = self.textbuffer.get_bounds()
        self.lyrics_before_edit = self.textbuffer.get_text(start, end, False)
        # remove sync tag
        self.textbuffer.remove_tag(self.sync_tag, start, end)
        # Conserve cache path in order to be able to correctly save edited lyrics although
        # the playing song might have changed during editing.
        self.path_before_edit = self.path
        
        self.set_menu_sensitive(False)
        
        # Enable editing and set cursor
        self.textview.set_cursor_visible(True)
        self.textview.set_editable(True)
        cursor = self.textbuffer.get_iter_at_line(1)
        self.textbuffer.place_cursor(cursor)
        self.textview.grab_focus()
        
        self.hbox.show()
    
    
    
    def preferences_dialog_action_callback(self, action):
        content = ConfigDialog().do_create_configure_widget()
        
        dialog = Gtk.Dialog(_('lLyrics Preferences'), self.shell.get_property('window'),
                Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, (Gtk.STOCK_OK, Gtk.ResponseType.OK))
                
        content_area = dialog.get_content_area()
        content_area.pack_start(content, True, True, 0)
            
        dialog.show_all()
        
        dialog.run()
        
        dialog.hide()
        
        
    
    def context_action_callback(self, action, param=None, data=None):
        page = self.shell.props.selected_page
        if not hasattr(page, "get_entry_view"):
            return
        
        selected = page.get_entry_view().get_selected_entries()
        if not selected:
            print("nothing selected")
            return
        
        # if multiple selections, take first
        entry = selected[0]
        
        self.showing_on_demand = True
        self.back_button.show()
        
        # Disconnect from song-changed and elapsed-change signals
        if self.psc_id:
            self.player.disconnect(self.psc_id)
            self.player.disconnect(self.pec_id)
            self.psc_id = None
            self.pec_id = None
        
        if not self.visible:
            self.toggle_action_group.get_action("ToggleLyricSideBar").set_active(True)
        
        self.search_lyrics(self.player, entry)
    
    
    
    def save_button_callback(self, button):
        self.textview.set_cursor_visible(False)
        self.textview.set_editable(False)
        self.hbox.hide()
        
        # get lyrics without artist-title header
        start, end = self.textbuffer.get_bounds()
        start.forward_lines(1)
        lyrics = self.textbuffer.get_text(start, end, False)
        
        # save edited lyrics to cache file and audio tag
        self.write_lyrics_to_cache(self.path_before_edit, lyrics)
        
        # If playing song changed, set "searching lyrics..." (might be overwritten
        # immediately, if thread for the new song already found lyrics)
        if self.path != self.path_before_edit:
            self.textbuffer.set_text(_("searching lyrics..."))
            
        self.set_menu_sensitive(True)
        
        # Set event flag to indicate end of editing and wake all threads 
        # waiting to display new lyrics.
        self.edit_event.set()
        
        
        
    def cancel_button_callback(self, button):
        self.textview.set_cursor_visible(False)
        self.textview.set_editable(False)
        self.hbox.hide()
        
        # Restore original lyrics if playing song didn't change,
        # otherwise set "searching lyrics..." (might be overwritten
        # immediately, if thread for the new song already found lyrics)
        if self.path == self.path_before_edit:
            self.textbuffer.set_text(self.lyrics_before_edit)
            start = self.textbuffer.get_start_iter()
            end = start.copy()
            end.forward_to_line_end()
            self.textbuffer.apply_tag(self.tag, start, end)
        else:
            self.textbuffer.set_text(_("searching lyrics..."))
        
        self.set_menu_sensitive(True)
        
        # Set event flag to indicate end of editing and wake all threads 
        # waiting to display new lyrics.
        self.edit_event.set()
    
    
    
    def back_button_callback(self, button):
        # reconnect to signals
        self.psc_id = self.player.connect('playing-song-changed', self.search_lyrics)
        self.pec_id = self.player.connect('elapsed-changed', self.elapsed_changed)
        
        self.back_button.hide()
        self.showing_on_demand = False
        
        playing_entry = self.player.get_playing_entry()
        
        # if nothing is playing, clear lyrics and return
        if not playing_entry:
            self.textbuffer.set_text("")
            return
        
        # otherwise search lyrics
        self.search_lyrics(self.player, playing_entry)
        
    
    
    def set_displayed_text(self, text):
        self.textbuffer.set_text(text)
        
        
    
    def scan_source(self, source, artist, title):
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.set_displayed_text, _("searching lyrics..."))
          
        newthread = Thread(target=self._scan_source_thread, args=(source, artist, title))
        newthread.start()
        
        
            
    def _scan_source_thread(self, source, artist, title):
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.set_menu_sensitive, False)
             
        if source == "From cache file":
            lyrics = self.get_lyrics_from_cache(self.path)
        else:   
            lyrics = self.get_lyrics_from_source(source, artist, title)
            
        # check if playing song changed
        if artist != self.clean_artist or title != self.clean_title:
            print("song changed")
            return          
        
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.set_menu_sensitive, True)
        
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.show_lyrics, lyrics)
        
        
        
    def scan_all_sources(self, artist, title, cache):
        if self.edit_event.is_set():
            Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.set_displayed_text, _("searching lyrics..."))
            
        newthread = Thread(target=self._scan_all_sources_thread, args=(artist, title, cache))
        newthread.start()
        
        
    
    def _scan_all_sources_thread(self, artist, title, cache):
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.set_menu_sensitive, False)
        
        lyrics = ""
        if cache:
            lyrics = self.get_lyrics_from_cache(self.path)
        
        if lyrics == "":
            i = 0
            while lyrics == "" and i in range(len(self.sources)):
                lyrics = self.get_lyrics_from_source(self.sources[i], artist, title)
                # check if playing song changed
                if artist != self.clean_artist or title != self.clean_title:
                    print("song changed")
                    return
                i += 1
        
        # We can't display new lyrics while user is editing! 
        self.edit_event.wait()
        
        # check if playing song changed
        if artist != self.clean_artist or title != self.clean_title:
            print("song changed")
            return
            
        if lyrics == "": 
            # check for lastfm corrections
            if not self.was_corrected:
                self.was_corrected = True  
                (artist, title, corrected) = Util.get_lastfm_correction(artist, title) 
                if corrected:
                    (self.clean_artist, self.clean_title) = self.clean_song_data(artist, title)
                    self._scan_all_sources_thread(self.clean_artist, self.clean_title, False)
                    return    
                                       
            self.current_source = None  
                
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.set_menu_sensitive, True)
        
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.show_lyrics, lyrics)

        
                
    def get_lyrics_from_cache(self, path): 
        self.current_source = "From cache file"
               
        # try to load lyrics from cache
        if os.path.exists (path):
            try:
                cachefile = open(path, "r")
                lyrics = cachefile.read()
                cachefile.close()
            except:
                print("error reading cache file")
                return ""
                
            print("got lyrics from cache")
            return lyrics
            
        return ""
    
    
    
    def write_lyrics_to_cache(self, path, lyrics):
        try:
            cachefile = open(path, "w+")
            cachefile.write(lyrics)
            cachefile.close()
            print("wrote lyrics to cache file")
        except:
            print("error writing lyrics to cache file")
                
            
        
    def get_lyrics_from_source(self, source, artist, title):
        # Playing song might change during search, so we want to 
        # conserve the correct cache path.
        path = self.path
        
        print("source: " + source)        
        self.current_source = source
        
        parser = self.dict[source].Parser(artist, title)
        try:
            lyrics = parser.parse()
        except Exception as e:
            print("Error in parser " + source)
            print(str(e))
            return ""
        
        if lyrics != "":
            print("got lyrics from source")
            if source != "External":
                lyrics = "%s\n\n(lyrics from %s)" % (lyrics, source)
            
            if self.cache:
                self.write_lyrics_to_cache(path, lyrics)
            
        return lyrics
    
    
    
    def show_lyrics(self, lyrics):
        if self.current_source is None:
            self.set_radio_menu_item_active("SelectNothing")
        elif self.current_source == "From cache file":
            self.set_radio_menu_item_active(_("From cache file"))
        else:
            self.set_radio_menu_item_active(self.current_source)
        
        if lyrics == "":
            print("no lyrics found")
            lyrics = _("No lyrics found")
        else:        
            lyrics, self.tags = Util.parse_lrc(lyrics)
        
        self.textbuffer.set_text("%s - %s\n%s" % (self.artist, self.title, lyrics))
        
        # make 'artist - title' header bold and underlined 
        start = self.textbuffer.get_start_iter()
        end = start.copy()
        end.forward_to_line_end()
        self.textbuffer.apply_tag(self.tag, start, end)
        
    
    
    def elapsed_changed(self, player, seconds):
        if not self.tags or not self.edit_event.is_set():
            return
        
        matching_tag = None
        for tag in self.tags:
            time, _ = tag
            if time > seconds:
                break
            matching_tag = tag
        
        if matching_tag is None or self.current_tag == matching_tag:
            return
        
        self.current_tag = matching_tag
        
        # remove old tag
        start, end = self.textbuffer.get_bounds()
        self.textbuffer.remove_tag(self.sync_tag, start, end)
        
        # highlight next line
        line = self.tags.index(self.current_tag) + 1
        start = self.textbuffer.get_iter_at_line(line)
        end = start.copy()
        end.forward_to_line_end()
        self.textbuffer.apply_tag(self.sync_tag, start, end)
        self.textview.scroll_to_iter(start, 0.1, False, 0, 0)
Exemple #6
0
class lLyrics(GObject.Object, Peas.Activatable):
    __gtype_name__ = 'lLyrics'
    object = GObject.property(type=GObject.Object)

    def __init__(self):
        GObject.Object.__init__(self)
        GObject.threads_init()
        Gdk.threads_init()

    def do_activate(self):
        # Get references for the Shell and the Shell-player
        self.shell = self.object
        self.player = self.shell.props.shell_player
        self.appshell = ApplicationShell(self.shell)

        # Create dictionary which assigns sources to their corresponding modules
        self.dict = dict({"Lyricwiki.org": LyricwikiParser, "Letras.terra.com.br": LetrasTerraParser,
                          "Metrolyrics.com": MetrolyricsParser, "AZLyrics.com": AZLyricsParser,
                          "Lyricsmania.com": LyricsmaniaParser, "Chartlyrics.com": ChartlyricsParser,
                          "Darklyrics.com": DarklyricsParser, "Genius.com": GeniusParser,
                          "Vagalume.com.br": VagalumeParser})
        self.add_builtin_lyrics_sources()

        # Get the user preferences
        config = Config()
        self.settings = config.get_settings()
        self.get_user_preferences(self.settings, None, config)
        # Watch for setting changes
        self.skc_id = self.settings.connect('changed', self.get_user_preferences, config)

        self.position = RB.ShellUILocation.RIGHT_SIDEBAR
        if self.left_sidebar:
            self.position = RB.ShellUILocation.SIDEBAR

        # Initialize the UI
        self.lmui_id = None
        self.tbui_id = None
        self.init_sidebar()
        self.init_menu()

        # Create flag, used to pop out sidebar on initial start of playback
        self.first = True
        self.visible = False

        # Event flag indicates whether the user is currently editing lyrics
        self.edit_event = threading.Event()
        self.edit_event.set()

        self.current_source = None
        self.tags = None
        self.current_tag = None
        self.showing_on_demand = False
        self.was_corrected = False

        # Search lyrics if already playing (this will be the case if user reactivates plugin during playback)
        if self.player.props.playing:
            self.search_lyrics(self.player, self.player.get_playing_entry())
        # Search lyrics everytime the song changes 
        self.psc_id = self.player.connect('playing-song-changed', self.search_lyrics)
        # Connect to elapsed-changed signal to handle synchronized lyrics
        self.pec_id = self.player.connect('elapsed-changed', self.elapsed_changed)

        print("activated plugin lLyrics")

    def do_deactivate(self):
        if self.visible:
            self.shell.remove_widget(self.vbox, self.position)

        self.settings.disconnect(self.skc_id)
        if self.psc_id is not None:
            self.player.disconnect(self.psc_id)
            self.player.disconnect(self.pec_id)

        self.appshell.cleanup()

        self.vbox = None
        self.textbuffer = None
        self.textview = None
        self.psc_id = None
        self.visible = None
        self.player = None
        self.toggle_action_group = None
        self.context_action_group = None
        self.appshell = None
        self.menu = None
        self.button_menu = None
        self.cache = None
        self.dict = None
        self.sources = None
        self.ui_id = None
        self.tag = None
        self.first = None
        self.current_source = None
        self.artist = None
        self.title = None
        self.clean_artist = None
        self.clean_title = None
        self.path = None
        self.lyrics_before_edit = None
        self.edit_event = None
        self.path_before_edit = None
        self.sources = None
        self.cache = None
        self.lyrics_folder = None
        self.ignore_brackets = None
        self.left_sidebar = None
        self.hide_label = None
        self.show_first = None
        self.position = None
        self.hbox = None
        self.back_button = None

        self.shell = None

        print("deactivated plugin lLyrics")

    def add_builtin_lyrics_sources(self):
        # find and append path for built-in lyrics plugin
        for p in sys.path:
            if p.endswith("/rhythmbox/plugins/rb"):
                path = p.replace("/rb", "/lyrics")
                sys.path.append(path)
                break
        else:
            print("Path to built-in lyrics plugin could not be detected")
            return

    def get_user_preferences(self, settings, key, config):
        self.sources = config.get_lyrics_sources()
        self.show_first = config.get_show_first()
        self.cache = config.get_cache_lyrics()
        self.lyrics_folder = config.get_lyrics_folder()
        self.ignore_brackets = config.get_ignore_brackets()
        self.left_sidebar = config.get_left_sidebar()
        self.hide_label = config.get_hide_label()

        # if this is called in do_activate or we need a reload to apply, return here
        if key is None or key in ["left-sidebar"]:
            return

        if key == "hide-label":
            if self.hide_label:
                self.label.hide()
            else:
                self.label.show()
            return

    def insert_ui(self):
        self.appshell.add_app_menuitems(view_menu_ui, 'lLyricsPluginToggleActions', 'view')
        self.appshell.add_browser_menuitems(context_ui, 'lLyricsPluginPopupActions')

    def init_menu(self):
        # add actions
        self.toggle_action_group = ActionGroup(self.shell, 'lLyricsPluginToggleActions')
        self.toggle_action_group.add_action(func=self.toggle_visibility,
                                            action_name='ToggleLyricSideBar', label=_("Lyrics"),
                                            action_state=ActionGroup.TOGGLE,
                                            action_type='app', accel="<Ctrl>l",
                                            tooltip=_("Display lyrics for the current playing song"))
        self.appshell.insert_action_group(self.toggle_action_group)

        self.context_action_group = ActionGroup(self.shell, 'lLyricsPluginPopupActions')
        self.context_action_group.add_action(action_name="lLyricsPopupAction", label=_("Show lyrics"),
                                             tooltip=_("Search and display lyrics for this song"),
                                             func=self.context_action_callback)
        self.appshell.insert_action_group(self.context_action_group)

        self.insert_ui()

    def init_sidebar(self):
        self.vbox = Gtk.VBox()

        hbox_header = Gtk.HBox()

        self.label = Gtk.Label(_("Lyrics"))
        self.label.set_use_markup(True)
        self.label.set_padding(3, 10)
        self.label.set_alignment(0, 0)

        self.menu = self.get_button_menu()
        self.set_menu_sensitive(False)

        # menu without toolbar
        icon_factory = Gtk.IconFactory()
        pxbf = GdkPixbuf.Pixbuf.new_from_file(os.path.dirname(__file__) + "/menu-icon.png")
        icon_factory.add("llyrics_menu", Gtk.IconSet.new_from_pixbuf(pxbf))
        icon_factory.add_default()

        menu_button = Gtk.Image.new_from_stock("llyrics_menu", Gtk.IconSize.SMALL_TOOLBAR)
        event_box = Gtk.EventBox()
        event_box.add(menu_button)
        event_box.connect("button-press-event", self.popup_menu, self.menu)

        hbox_header.pack_start(self.label, True, True, 0)
        hbox_header.pack_end(event_box, False, False, 5)

        # create a TextView for displaying lyrics
        self.textview = Gtk.TextView()
        self.textview.set_editable(False)
        self.textview.set_cursor_visible(False)
        self.textview.set_left_margin(10)
        self.textview.set_right_margin(10)
        self.textview.set_pixels_above_lines(5)
        self.textview.set_pixels_below_lines(5)
        self.textview.set_wrap_mode(Gtk.WrapMode.WORD)

        # create a ScrollView
        sw = Gtk.ScrolledWindow()
        sw.add(self.textview)
        sw.set_shadow_type(Gtk.ShadowType.IN)

        # initialize a TextBuffer to store lyrics in
        self.textbuffer = Gtk.TextBuffer()
        self.textview.set_buffer(self.textbuffer)

        # tag to style headers bold and underlined
        self.tag = self.textbuffer.create_tag(None, underline=Pango.Underline.SINGLE, weight=600,
                                              pixels_above_lines=10, pixels_below_lines=20)
        # tag to highlight synchronized lyrics
        self.sync_tag = self.textbuffer.create_tag(None, weight=600)

        # create save and cancel buttons for edited lyrics
        save_button = Gtk.Button.new_with_label(_("Save"))
        save_button.connect("clicked", self.save_button_callback)
        cancel_button = Gtk.Button.new_with_label(_("Cancel"))
        cancel_button.connect("clicked", self.cancel_button_callback)
        self.hbox = Gtk.HBox()
        self.hbox.pack_start(save_button, True, True, 3)
        self.hbox.pack_start(cancel_button, True, True, 3)

        # button for closing on demand lyrics
        self.back_button = Gtk.Button.new_with_label(_("Back to playing song"))
        self.back_button.connect("clicked", self.back_button_callback)

        # pack everything into side pane
        self.vbox.pack_start(hbox_header, False, False, 0)
        self.vbox.pack_start(sw, True, True, 0)
        self.vbox.pack_end(self.hbox, False, False, 3)
        self.vbox.pack_end(self.back_button, False, False, 3)

        self.vbox.show_all()
        self.hbox.hide()
        self.back_button.hide()

        if self.hide_label:
            self.label.hide()

        self.visible = False

    def get_button_menu(self):
        menu = Gtk.Menu()

        self.radio_sources = Gtk.Menu()

        item_unselect = Gtk.RadioMenuItem.new_with_label([], "SelectNothing")
        item_unselect.connect("activate", self.scan_selected_source_callback, "SelectNothing")
        self.radio_sources.append(item_unselect)

        last_item = item_unselect

        for entry in LYRICS_SOURCES[:-1]:
            last_item = self.add_radio_menu_item(self.radio_sources, entry, self.scan_selected_source_callback,
                                                 last_item)

        self.radio_sources.append(Gtk.SeparatorMenuItem())
        self.add_radio_menu_item(self.radio_sources, _("From cache file"), self.scan_selected_source_callback,
                                 last_item)

        self.radio_sources.show_all()

        item_sources = Gtk.MenuItem(_("Sources"))
        item_sources.set_submenu(self.radio_sources)
        menu.append(item_sources)

        self.add_menu_item(menu, _("Scan next source"), self.scan_next_action_callback)
        self.add_menu_item(menu, _("Scan all sources"), self.scan_all_action_callback)
        menu.append(Gtk.SeparatorMenuItem())
        self.add_menu_item(menu, _("Search online"), self.search_online_action_callback)
        menu.append(Gtk.SeparatorMenuItem())
        self.add_menu_item(menu, _("Mark as instrumental"), self.instrumental_action_callback)
        menu.append(Gtk.SeparatorMenuItem())
        self.add_menu_item(menu, _("Clear lyrics"), self.clear_action_callback)
        self.add_menu_item(menu, _("Edit lyrics"), self.edit_action_callback)

        # add preferences item
        menu.append(Gtk.SeparatorMenuItem())
        self.add_menu_item(menu, _("Preferences"), self.preferences_dialog_action_callback)

        menu.show_all()

        # hide the SelectNothing choice
        item_unselect.hide()

        return menu

    def add_menu_item(self, menu, label, callback):
        item = Gtk.MenuItem(label)
        item.connect("activate", callback)
        menu.append(item)

    def add_radio_menu_item(self, menu, label, callback, last):
        group = last.get_group()
        item = Gtk.RadioMenuItem.new_with_label(group, label)
        if label == _("From cache file"):
            label = "From cache file"
        item.connect("toggled", callback, label)
        menu.append(item)

        return item

    def set_menu_sensitive(self, sensitive):
        index = 0
        for item in self.menu:
            # 'Preferences' option should always be sensitive
            if index == len(self.menu) - 1:
                continue
            item.set_sensitive(sensitive)
            index += 1

    def set_radio_menu_item_active(self, itemlabel):
        for item in self.radio_sources:
            if item.get_label() == itemlabel:
                item.set_active(True)
                break

    def popup_menu(self, widget, event, menu):
        menu.popup(None, widget, None, None, event.button, event.time)

    def toggle_visibility(self, action, param=None, data=None):
        action = self.toggle_action_group.get_action('ToggleLyricSideBar')

        if action.get_active():
            self.shell.add_widget(self.vbox, self.position, True, True)
            self.visible = True
            if not self.first and not self.showing_on_demand:
                self.search_lyrics(self.player, self.player.get_playing_entry())
        else:
            self.shell.remove_widget(self.vbox, self.position)
            self.visible = False

    def search_lyrics(self, player, entry):
        # clear sync stuff
        if self.tags is not None:
            self.tags = None
            self.current_tag = None
            start, end = self.textbuffer.get_bounds()
            self.textbuffer.remove_tag(self.sync_tag, start, end)

        if entry is None:
            return

        # don't show lyrics for podcasts and radio
        if entry.get_entry_type().get_name() in ('iradio', 'podcast-post'):
            print("entry type: " + entry.get_entry_type().get_name())
            if not self.first:
                self.first = True
                print('removing the sidebar')
                self.toggle_action_group.get_action("ToggleLyricSideBar").set_active(False)
            return

        # pop out sidebar at first playback
        if self.first and not self.showing_on_demand:
            self.first = False
            if not self.visible and self.show_first:
                self.toggle_action_group.get_action("ToggleLyricSideBar").set_active(True)
                # toggling the sidebar will start lyrics search again, so we can return here
                return

        # only do something if visible
        if not self.visible:
            return

        # get the song data
        self.artist = entry.get_string(RB.RhythmDBPropType.ARTIST)
        self.title = entry.get_string(RB.RhythmDBPropType.TITLE)

        print("search lyrics for " + self.artist + " - " + self.title)

        self.was_corrected = False

        (self.clean_artist, self.clean_title) = self.clean_song_data(self.artist, self.title)
        self.path = self.build_cache_path(self.clean_artist, self.clean_title)

        self.scan_all_sources(self.clean_artist, self.clean_title, True)

    def clean_song_data(self, artist, title):
        # convert to lowercase
        artist = artist.lower()
        title = title.lower()

        # remove accents
        artist = unicodedata.normalize('NFKD', artist)
        artist = "".join([c for c in artist if not unicodedata.combining(c)])
        title = unicodedata.normalize('NFKD', title)
        title = "".join([c for c in title if not unicodedata.combining(c)])

        if self.ignore_brackets:
            LYRICS_TITLE_STRIP.append("\(.*\)")

        # replace ampersands and the like
        for exp in LYRICS_ARTIST_REPLACE:
            artist = re.sub(exp[0], exp[1], artist)
        for exp in LYRICS_TITLE_REPLACE:
            title = re.sub(exp[0], exp[1], title)

        # strip things like "(live at Somewhere)", "(acoustic)", etc
        for exp in LYRICS_TITLE_STRIP:
            title = re.sub(exp, '', title)

        # compress spaces
        title = title.strip()
        artist = artist.strip()

        return (artist, title)

    def build_cache_path(self, artist, title):
        artist_folder = os.path.join(self.lyrics_folder, artist[:128])
        if not os.path.exists(artist_folder):
            os.mkdir(artist_folder)

        return os.path.join(artist_folder, title[:128] + '.lyric')

    def scan_selected_source_callback(self, action, activated_action):
        if not action.get_active():
            return

        source = activated_action
        if source == "SelectNothing" or source == self.current_source:
            return

        self.scan_source(source, self.clean_artist, self.clean_title)

    def scan_next_action_callback(self, action):
        if self.current_source is None or self.current_source == "From cache file":
            index = 0
        else:
            index = self.sources.index(self.current_source) + 1
            index = index % (len(self.sources) + 1)

        if index >= len(self.sources):
            source = "From cache file"
        else:
            source = self.sources[index]

        self.scan_source(source, self.clean_artist, self.clean_title)

    def scan_all_action_callback(self, action):
        self.scan_all_sources(self.clean_artist, self.clean_title, False)

    def search_online_action_callback(self, action):
        webbrowser.open("http://www.google.com/search?q=%s+%s+lyrics" % (self.clean_artist, self.clean_title))

    def instrumental_action_callback(self, action):
        lyrics = "-- Instrumental --"
        self.write_lyrics_to_cache(self.path, lyrics)
        self.current_source = None
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.show_lyrics, lyrics)

    def save_to_cache_action_callback(self, action):
        start, end = self.textbuffer.get_bounds()
        start.forward_lines(1)
        lyrics = self.textbuffer.get_text(start, end, False)

        self.write_lyrics_to_cache(self.path, lyrics)

    def clear_action_callback(self, action):
        self.textbuffer.set_text("")
        try:
            os.remove(self.path)
        except:
            print("No cache file found to clear")
        print("cleared lyrics")

    def edit_action_callback(self, action):
        # Unset event flag to indicate editing and so block all other threads which 
        # want to display new lyrics until editing is finished.
        self.edit_event.clear()

        # Conserve lyrics in order to restore original lyrics when editing is canceled 
        start, end = self.textbuffer.get_bounds()
        self.lyrics_before_edit = self.textbuffer.get_text(start, end, False)
        # remove sync tag
        self.textbuffer.remove_tag(self.sync_tag, start, end)
        # Conserve cache path in order to be able to correctly save edited lyrics although
        # the playing song might have changed during editing.
        self.path_before_edit = self.path

        self.set_menu_sensitive(False)

        # Enable editing and set cursor
        self.textview.set_cursor_visible(True)
        self.textview.set_editable(True)
        cursor = self.textbuffer.get_iter_at_line(1)
        self.textbuffer.place_cursor(cursor)
        self.textview.grab_focus()

        self.hbox.show()

    def preferences_dialog_action_callback(self, action):
        content = ConfigDialog().do_create_configure_widget()

        dialog = Gtk.Dialog(_('lLyrics Preferences'), self.shell.get_property('window'),
                            Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
                            (Gtk.STOCK_OK, Gtk.ResponseType.OK))

        content_area = dialog.get_content_area()
        content_area.pack_start(content, True, True, 0)

        dialog.show_all()

        dialog.run()

        dialog.hide()

    def context_action_callback(self, action, param=None, data=None):
        page = self.shell.props.selected_page
        if not hasattr(page, "get_entry_view"):
            return

        selected = page.get_entry_view().get_selected_entries()
        if not selected:
            print("nothing selected")
            return

        # if multiple selections, take first
        entry = selected[0]

        self.showing_on_demand = True
        self.back_button.show()

        # Disconnect from song-changed and elapsed-change signals
        if self.psc_id:
            self.player.disconnect(self.psc_id)
            self.player.disconnect(self.pec_id)
            self.psc_id = None
            self.pec_id = None

        if not self.visible:
            self.toggle_action_group.get_action("ToggleLyricSideBar").set_active(True)

        self.search_lyrics(self.player, entry)

    def save_button_callback(self, button):
        self.textview.set_cursor_visible(False)
        self.textview.set_editable(False)
        self.hbox.hide()

        # get lyrics without artist-title header
        start, end = self.textbuffer.get_bounds()
        start.forward_lines(1)
        lyrics = self.textbuffer.get_text(start, end, False)

        # save edited lyrics to cache file and audio tag
        self.write_lyrics_to_cache(self.path_before_edit, lyrics)

        # If playing song changed, set "searching lyrics..." (might be overwritten
        # immediately, if thread for the new song already found lyrics)
        if self.path != self.path_before_edit:
            self.textbuffer.set_text(_("searching lyrics..."))

        self.set_menu_sensitive(True)

        # Set event flag to indicate end of editing and wake all threads 
        # waiting to display new lyrics.
        self.edit_event.set()

    def cancel_button_callback(self, button):
        self.textview.set_cursor_visible(False)
        self.textview.set_editable(False)
        self.hbox.hide()

        # Restore original lyrics if playing song didn't change,
        # otherwise set "searching lyrics..." (might be overwritten
        # immediately, if thread for the new song already found lyrics)
        if self.path == self.path_before_edit:
            self.textbuffer.set_text(self.lyrics_before_edit)
            start = self.textbuffer.get_start_iter()
            end = start.copy()
            end.forward_to_line_end()
            self.textbuffer.apply_tag(self.tag, start, end)
        else:
            self.textbuffer.set_text(_("searching lyrics..."))

        self.set_menu_sensitive(True)

        # Set event flag to indicate end of editing and wake all threads 
        # waiting to display new lyrics.
        self.edit_event.set()

    def back_button_callback(self, button):
        # reconnect to signals
        self.psc_id = self.player.connect('playing-song-changed', self.search_lyrics)
        self.pec_id = self.player.connect('elapsed-changed', self.elapsed_changed)

        self.back_button.hide()
        self.showing_on_demand = False

        playing_entry = self.player.get_playing_entry()

        # if nothing is playing, clear lyrics and return
        if not playing_entry:
            self.textbuffer.set_text("")
            return

        # otherwise search lyrics
        self.search_lyrics(self.player, playing_entry)

    def set_displayed_text(self, text):
        self.textbuffer.set_text(text)

    def scan_source(self, source, artist, title):
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.set_displayed_text, _("searching lyrics..."))

        newthread = Thread(target=self._scan_source_thread, args=(source, artist, title))
        newthread.start()

    def _scan_source_thread(self, source, artist, title):
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.set_menu_sensitive, False)

        if source == "From cache file":
            lyrics = self.get_lyrics_from_cache(self.path)
        else:
            lyrics = self.get_lyrics_from_source(source, artist, title)

        # check if playing song changed
        if artist != self.clean_artist or title != self.clean_title:
            print("song changed")
            return

        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.set_menu_sensitive, True)

        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.show_lyrics, lyrics)

    def scan_all_sources(self, artist, title, cache):
        if self.edit_event.is_set():
            Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.set_displayed_text, _("searching lyrics..."))

        newthread = Thread(target=self._scan_all_sources_thread, args=(artist, title, cache))
        newthread.start()

    def _scan_all_sources_thread(self, artist, title, cache):
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.set_menu_sensitive, False)

        lyrics = ""
        if cache:
            lyrics = self.get_lyrics_from_cache(self.path)

        if lyrics == "":
            i = 0
            while lyrics == "" and i in range(len(self.sources)):
                lyrics = self.get_lyrics_from_source(self.sources[i], artist, title)
                # check if playing song changed
                if artist != self.clean_artist or title != self.clean_title:
                    print("song changed")
                    return
                i += 1

        # We can't display new lyrics while user is editing! 
        self.edit_event.wait()

        # check if playing song changed
        if artist != self.clean_artist or title != self.clean_title:
            print("song changed")
            return

        if lyrics == "":
            # check for lastfm corrections
            if not self.was_corrected:
                self.was_corrected = True
                (artist, title, corrected) = Util.get_lastfm_correction(artist, title)
                if corrected:
                    (self.clean_artist, self.clean_title) = self.clean_song_data(artist, title)
                    self._scan_all_sources_thread(self.clean_artist, self.clean_title, False)
                    return

            self.current_source = None

        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.set_menu_sensitive, True)

        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.show_lyrics, lyrics)

    def get_lyrics_from_cache(self, path):
        self.current_source = "From cache file"

        # try to load lyrics from cache
        if os.path.exists(path):
            try:
                cachefile = open(path, "r")
                lyrics = cachefile.read()
                cachefile.close()
            except:
                print("error reading cache file")
                return ""

            print("got lyrics from cache")
            return lyrics

        return ""

    def write_lyrics_to_cache(self, path, lyrics):
        try:
            cachefile = open(path, "w+")
            cachefile.write(lyrics)
            cachefile.close()
            print("wrote lyrics to cache file")
        except:
            print("error writing lyrics to cache file")

    def get_lyrics_from_source(self, source, artist, title):
        # Playing song might change during search, so we want to 
        # conserve the correct cache path.
        path = self.path

        print("source: " + source)
        self.current_source = source

        parser = self.dict[source].Parser(artist, title)
        try:
            lyrics = parser.parse()
        except Exception as e:
            print("Error in parser " + source)
            print(str(e))
            return ""

        if lyrics != "":
            print("got lyrics from source")
            lyrics = "%s\n\n(lyrics from %s)" % (lyrics, source)

            if self.cache:
                self.write_lyrics_to_cache(path, lyrics)

        return lyrics

    def show_lyrics(self, lyrics):
        if self.current_source is None:
            self.set_radio_menu_item_active("SelectNothing")
        elif self.current_source == "From cache file":
            self.set_radio_menu_item_active(_("From cache file"))
        else:
            self.set_radio_menu_item_active(self.current_source)

        if lyrics == "":
            print("no lyrics found")
            lyrics = _("No lyrics found")
        else:
            lyrics, self.tags = Util.parse_lrc(lyrics)

        self.textbuffer.set_text("%s - %s\n%s" % (self.artist, self.title, lyrics))

        # make 'artist - title' header bold and underlined 
        start = self.textbuffer.get_start_iter()
        end = start.copy()
        end.forward_to_line_end()
        self.textbuffer.apply_tag(self.tag, start, end)

    def elapsed_changed(self, player, seconds):
        if not self.tags or not self.edit_event.is_set():
            return

        matching_tag = None
        for tag in self.tags:
            time, _ = tag
            if time > seconds:
                break
            matching_tag = tag

        if matching_tag is None or self.current_tag == matching_tag:
            return

        self.current_tag = matching_tag

        # remove old tag
        start, end = self.textbuffer.get_bounds()
        self.textbuffer.remove_tag(self.sync_tag, start, end)

        # highlight next line
        line = self.tags.index(self.current_tag) + 1
        start = self.textbuffer.get_iter_at_line(line)
        end = start.copy()
        end.forward_to_line_end()
        self.textbuffer.apply_tag(self.sync_tag, start, end)
        self.textview.scroll_to_iter(start, 0.1, False, 0, 0)