diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 0e797d5a3..00b8820f4 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -373,7 +373,14 @@ class Genius(Backend): # At least Genius is nice and has a tag called 'lyrics'! # Updated css where the lyrics are based in HTML. - lyrics = html.find("div", class_="lyrics").get_text() + lyrics_div = html.find("div", class_="lyrics") + + # nullcheck + if lyrics_div is None: + self._log.debug(u'Genius lyrics for {0} not found', + page_url) + return None + lyrics = lyrics_div.get_text() return lyrics diff --git a/docs/changelog.rst b/docs/changelog.rst index 33ad386ad..fd0c225f9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -178,6 +178,8 @@ Fixes: * Removed ``@classmethod`` decorator from dbcore.query.NoneQuery.match method failing with AttributeError when called. It is now an instance method. :bug:`3516` :bug:`3517` +* :doc:`/plugins/lyrics`: Tolerate missing lyrics div in Genius scraper + :bug:`3535` For plugin developers: diff --git a/test/rsrc/lyrics/geniuscom/sample.txt b/test/rsrc/lyrics/geniuscom/sample.txt new file mode 100644 index 000000000..1648d070a --- /dev/null +++ b/test/rsrc/lyrics/geniuscom/sample.txt @@ -0,0 +1,270 @@ + + + + + + + + SAMPLE – SONG Lyrics | g-example Lyrics + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ g-example +
+ + + + + +
+
+ + + + +
+
+
+
+
+ # +
+
+ +
+
+

SONG

+

+ + SAMPLE + +

+

+ +

+

+ +

+
+
+ +
+
+
+ +
+
+
+

SONG Lyrics

+
+
+ !!!! MISSING LYRICS HERE !!! +
+
+
+
+
More on g-example
+
+
+
+
+
+ + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + diff --git a/test/test_lyrics.py b/test/test_lyrics.py index f7ea538e2..83a229f62 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -39,6 +39,7 @@ from mock import MagicMock log = logging.getLogger('beets.test_lyrics') raw_backend = lyrics.Backend({}, log) google = lyrics.Google(MagicMock(), log) +genius = lyrics.Genius(MagicMock(), log) class LyricsPluginTest(unittest.TestCase): @@ -214,6 +215,33 @@ class MockFetchUrl(object): return content +class GeniusMockGet(object): + + def __init__(self, pathval='fetched_path'): + self.pathval = pathval + self.fetched = None + + def __call__(self, url, headers=False): + from requests.models import Response + # for the first requests.get() return a path + if headers: + response = Response() + response.status_code = 200 + response._content = b'{"meta":{"status":200},\ + "response":{"song":{"path":"/lyrics/sample"}}}' + return response + # for the second requests.get() return the genius page + else: + from mock import PropertyMock + self.fetched = url + fn = url_to_filename(url) + with open(fn, 'r') as f: + content = f.read() + response = Response() + type(response).text = PropertyMock(return_value=content) + return response + + def is_lyrics_content_ok(title, text): """Compare lyrics text to expected lyrics for given title.""" if not text: @@ -395,6 +423,40 @@ class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest): google.is_page_candidate(url, url_title, s['title'], u'Sunn O)))') +class LyricsGeniusBaseTest(unittest.TestCase): + + def setUp(self): + """Set up configuration.""" + try: + __import__('bs4') + except ImportError: + self.skipTest('Beautiful Soup 4 not available') + if sys.version_info[:3] < (2, 7, 3): + self.skipTest("Python's built-in HTML parser is not good enough") + + +class LyricsGeniusScrapTest(LyricsGeniusBaseTest): + + """Checks that Genius backend works as intended. + """ + import requests + + def setUp(self): + """Set up configuration""" + LyricsGeniusBaseTest.setUp(self) + self.plugin = lyrics.LyricsPlugin() + + @patch.object(requests, 'get', GeniusMockGet()) + def test_no_lyrics_div(self): + """Ensure that `lyrics_from_song_api_path` doesn't crash when the html + for a Genius page contain
+ """ + # https://github.com/beetbox/beets/issues/3535 + # expected return value None + self.assertEqual(genius.lyrics_from_song_api_path('/nolyric'), + None) + + class SlugTests(unittest.TestCase): def test_slug(self):