diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 50d1529e1..51072e1ea 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -64,17 +64,20 @@ def fetch_url(url): log.debug(u'failed to fetch: {0} ({1})'.format(url, unicode(exc))) return None + def unescape(text): """Resolves xx; HTML entities (and some others).""" if isinstance(text, str): text = text.decode('utf8', 'ignore') out = text.replace(u' ', u' ') + def replchar(m): num = m.group(1) return unichr(int(num)) out = re.sub(u"(\d+);", replchar, out) return out + def extract_text(html, starttag): """Extract the text from a
sections # are now closed. Use str() rather than prettify() as it's more @@ -343,6 +386,7 @@ def scrape_lyrics_from_url(url): soup = BeautifulSoup(tagTokens[0]) return unescape(tagTokens[0].strip("\n\r: ")) + def fetch_google(artist, title): """Fetch lyrics from Google search results. """ @@ -378,6 +422,7 @@ def fetch_google(artist, title): # Plugin logic. + class LyricsPlugin(BeetsPlugin): def __init__(self): super(LyricsPlugin, self).__init__() @@ -394,6 +439,7 @@ class LyricsPlugin(BeetsPlugin): if self.config['google_API_key'].get(): self.backends.insert(0, fetch_google) + def commands(self): cmd = ui.Subcommand('lyrics', help='fetch song lyrics') cmd.parser.add_option('-p', '--print', dest='printlyr', @@ -414,11 +460,14 @@ class LyricsPlugin(BeetsPlugin): cmd.func = func return [cmd] - # Auto-fetch lyrics on import. + def imported(self, session, task): + """Auto-fetch lyrics on import""" if self.config['auto']: for item in task.imported_items(): - self.fetch_item_lyrics(session.lib, logging.DEBUG, item, False, False) + self.fetch_item_lyrics(session.lib, logging.DEBUG, item, \ + False, False) + def fetch_item_lyrics(self, lib, loglevel, item, write, force): """Fetch and store lyrics for a single item. If ``write``, then the @@ -434,18 +483,35 @@ class LyricsPlugin(BeetsPlugin): (item.artist, item.title)) return + artist = remove_ft_artist_suffix(item.artist) + title = remove_parenthesized_suffix(\ + remove_ft_artist_suffix(item.title)) + # Fetch lyrics. - lyrics = self.get_lyrics(item.artist, item.title) + lyrics = self.get_lyrics(artist, title) + + if not lyrics: + # Check for a songs combinations + # (e.g. Pink Floyd - Speak to Me / Breathe) + titles = split_multi_titles(title) + for t in titles: + lyrics_title = self.get_lyrics(artist, t) + if lyrics_title: + if lyrics : + lyrics += u"\n\n---\n\n%s" % lyrics_title + else: + lyrics = lyrics_title + if not lyrics: log.log(loglevel, u'lyrics not found: %s - %s' % - (item.artist, item.title)) + (artist, title)) if fallback: lyrics = fallback else: return else: - log.log(loglevel, u'fetched lyrics: %s - %s' % - (item.artist, item.title)) + log.log(loglevel, u'fetched lyrics : %s - %s' % + (artist, title)) item.lyrics = lyrics @@ -453,6 +519,7 @@ class LyricsPlugin(BeetsPlugin): item.try_write() item.store() + def get_lyrics(self, artist, title): """Fetch lyrics, trying each source in turn. Return a string or None if no lyrics were found. @@ -471,4 +538,4 @@ class LyricsPlugin(BeetsPlugin): log.debug(u'got lyrics from backend: {0}'.format( backend.__name__ )) - return lyrics + return lyrics.strip() diff --git a/docs/changelog.rst b/docs/changelog.rst index d0a74ca85..b3180846f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,9 @@ New stuff: * :doc:`/plugins/replaygain`: Added support for calculating ReplayGain values with GStreamer as well the mp3gain programs. This enables ReplayGain calculation for any audio format. +* :doc:`/plugins/lyrics`: Better handling of songs whose title contain a + featured artist. Songs combinations are resolved now (all lyrics are + appended). Thanks to KraYmer and paulp. * Add support for `initial_key` as field in the library and tag for media files. When the user sets this field with ``beet modify initial_key=Am`` the media files will reflect this in their tags. The diff --git a/test/test_lyrics.py b/test/test_lyrics.py new file mode 100644 index 000000000..70bced2e1 --- /dev/null +++ b/test/test_lyrics.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +"""Tests for the 'lyrics' plugin""" + +import _common +from _common import unittest +from beetsplug import lyrics +from beets import config +from beets.util import confit + + +class LyricsPluginTest(unittest.TestCase): + def setUp(self): + """Set up configuration""" + lyrics.LyricsPlugin() + + def test_split_multi_titles(self): + self.assertEqual(lyrics.split_multi_titles('song1 / song2 / song3'), + ['song1', 'song2', 'song3']) + self.assertEqual(lyrics.split_multi_titles('song1/song2 song3'), + ['song1', 'song2 song3']) + self.assertEqual(lyrics.split_multi_titles('song1 song2'), + None) + + def test_remove_ft_artist_suffix(self): + self.assertEqual(lyrics.remove_ft_artist_suffix('Bob featuring Marcia'), 'Bob') + self.assertEqual(lyrics.remove_ft_artist_suffix('Bob feat Marcia'), 'Bob') + self.assertEqual(lyrics.remove_ft_artist_suffix('Bob and Marcia'), 'Bob') + self.assertEqual(lyrics.remove_ft_artist_suffix('Bob feat. Marcia'), 'Bob') + self.assertEqual(lyrics.remove_ft_artist_suffix('Bob & Marcia'), 'Bob') + self.assertEqual(lyrics.remove_ft_artist_suffix('Bob feats Marcia'), 'Bob feats Marcia') + + def test_remove_parenthesized_suffix(self): + self.assertEqual(lyrics.remove_parenthesized_suffix('Song (live)'), 'Song') + self.assertEqual(lyrics.remove_parenthesized_suffix('Song (live) (new)'), 'Song') + self.assertEqual(lyrics.remove_parenthesized_suffix('Song (live (new))'), 'Song') + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + +if __name__ == '__main__': + unittest.main(defaultTest='suite') +