hello,

when using the lyrics plugin, if lyrics are found and beets attempts
to write them to the file it will throw this error and stop:

Traceback (most recent call last):
  File "/usr/local/bin/beet", line 11, in <module>
    load_entry_point('beets==1.4.9', 'console_scripts', 'beet')()
  File "/usr/local/lib/python3.8/site-packages/beets/ui/__init__.py", line 
1266, in main
    _raw_main(args)
  File "/usr/local/lib/python3.8/site-packages/beets/ui/__init__.py", line 
1253, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "/usr/local/lib/python3.8/site-packages/beets/ui/commands.py", line 955, 
in import_func
    import_files(lib, paths, query)
  File "/usr/local/lib/python3.8/site-packages/beets/ui/commands.py", line 925, 
in import_files
    session.run()
  File "/usr/local/lib/python3.8/site-packages/beets/importer.py", line 329, in 
run
    pl.run_parallel(QUEUE_SIZE)
  File "/usr/local/lib/python3.8/site-packages/beets/util/pipeline.py", line 
445, in run_parallel
    six.reraise(exc_info[0], exc_info[1], exc_info[2])
  File "/usr/local/lib/python3.8/site-packages/six.py", line 703, in reraise
    raise value
  File "/usr/local/lib/python3.8/site-packages/beets/util/pipeline.py", line 
312, in run
    out = self.coro.send(msg)
  File "/usr/local/lib/python3.8/site-packages/beets/util/pipeline.py", line 
194, in coro
    func(*(args + (task,)))
  File "/usr/local/lib/python3.8/site-packages/beets/importer.py", line 1511, 
in plugin_stage
    func(session, task)
  File "/usr/local/lib/python3.8/site-packages/beets/plugins.py", line 143, in 
wrapper
    return func(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/beetsplug/lyrics.py", line 833, 
in imported
    self.fetch_item_lyrics(session.lib, item,
  File "/usr/local/lib/python3.8/site-packages/beetsplug/lyrics.py", line 847, 
in fetch_item_lyrics
    lyrics = [self.get_lyrics(artist, title) for title in titles]
  File "/usr/local/lib/python3.8/site-packages/beetsplug/lyrics.py", line 847, 
in <listcomp>
    lyrics = [self.get_lyrics(artist, title) for title in titles]
  File "/usr/local/lib/python3.8/site-packages/beetsplug/lyrics.py", line 880, 
in get_lyrics
    lyrics = backend.fetch(artist, title)
  File "/usr/local/lib/python3.8/site-packages/beetsplug/lyrics.py", line 403, 
in fetch
    return self.lyrics_from_song_api_path(song_api_path)
  File "/usr/local/lib/python3.8/site-packages/beetsplug/lyrics.py", line 375, 
in lyrics_from_song_api_path
    lyrics = html.find("div", class_="lyrics").get_text()
AttributeError: 'NoneType' object has no attribute 'get_text'

this is suspisciously like the other issue we have a patch for in the
port already. I went and took a look and found that there is a patch
for this[0] from back in September. Unfortunately just backporting
those exact changes does not fix the above error. There are
significant changes between the lyrics.py from version 1.4.9 and what
that patch is applied to. However if we just grab the lyrics.py with
that patch applied this error is resolved. That is what the attched
update does. I am not sure if this is the correct thing to do or not,
or if we at this point should simply pick a commit and wholesale
update the port (that would probably let us drop both current
patches).

thanks,

.jh

[0] 
https://github.com/beetbox/beets/commit/b239a0b3d2dcbde0495750903dc2e69067d48ebf.patch
Index: beets/Makefile
===================================================================
RCS file: /cvs/ports/audio/beets/Makefile,v
retrieving revision 1.51
diff -u -p -u -p -r1.51 Makefile
--- beets/Makefile      23 Feb 2021 19:39:09 -0000      1.51
+++ beets/Makefile      15 May 2021 18:00:24 -0000
@@ -5,7 +5,7 @@ COMMENT=        CLI tools to manage music colle
 MODPY_EGG_VERSION= 1.4.9
 DISTNAME=      beets-${MODPY_EGG_VERSION}
 CATEGORIES=    audio
-REVISION=      9
+REVISION=      10
 
 HOMEPAGE=      https://beets.io/
 
Index: beets/patches/patch-beetsplug_lyrics_py
===================================================================
RCS file: beets/patches/patch-beetsplug_lyrics_py
diff -N beets/patches/patch-beetsplug_lyrics_py
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ beets/patches/patch-beetsplug_lyrics_py     15 May 2021 18:00:24 -0000
@@ -0,0 +1,211 @@
+$OpenBSD$
+
+Index: beetsplug/lyrics.py
+--- beetsplug/lyrics.py.orig
++++ beetsplug/lyrics.py
+@@ -55,6 +55,7 @@ except ImportError:
+ 
+ from beets import plugins
+ from beets import ui
++from beets import util
+ import beets
+ 
+ DIV_RE = re.compile(r'<(/?)div>?', re.I)
+@@ -186,6 +187,9 @@ def search_pairs(item):
+     In addition to the artist and title obtained from the `item` the
+     method tries to strip extra information like paranthesized suffixes
+     and featured artists from the strings and add them as candidates.
++    The artist sort name is added as a fallback candidate to help in
++    cases where artist name includes special characters or is in a
++    non-latin script.
+     The method also tries to split multiple titles separated with `/`.
+     """
+     def generate_alternatives(string, patterns):
+@@ -199,12 +203,16 @@ def search_pairs(item):
+                 alternatives.append(match.group(1))
+         return alternatives
+ 
+-    title, artist = item.title, item.artist
++    title, artist, artist_sort = item.title, item.artist, item.artist_sort
+ 
+     patterns = [
+         # Remove any featuring artists from the artists name
+         r"(.*?) {0}".format(plugins.feat_tokens())]
+     artists = generate_alternatives(artist, patterns)
++    # Use the artist_sort as fallback only if it differs from artist to avoid
++    # repeated remote requests with the same search terms
++    if artist != artist_sort:
++        artists.append(artist_sort)
+ 
+     patterns = [
+         # Remove a parenthesized suffix from a title string. Common
+@@ -351,14 +359,9 @@ class Genius(Backend):
+             'User-Agent': USER_AGENT,
+         }
+ 
+-    def lyrics_from_song_api_path(self, song_api_path):
+-        song_url = self.base_url + song_api_path
+-        response = requests.get(song_url, headers=self.headers)
+-        json = response.json()
+-        path = json["response"]["song"]["path"]
+-
++    def lyrics_from_song_page(self, page_url):
+         # Gotta go regular html scraping... come on Genius.
+-        page_url = "https://genius.com"; + path
++        self._log.debug(u'fetching lyrics from: {0}', page_url)
+         try:
+             page = requests.get(page_url)
+         except requests.RequestException as exc:
+@@ -370,15 +373,38 @@ class Genius(Backend):
+         # Remove script tags that they put in the middle of the lyrics.
+         [h.extract() for h in html('script')]
+ 
+-        # 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()
++        # Most of the time, the page contains a div with class="lyrics" where
++        # all of the lyrics can be found already correctly formatted
++        # Sometimes, though, it packages the lyrics into separate divs, most
++        # likely for easier ad placement
++        lyrics_div = html.find("div", class_="lyrics")
++        if not lyrics_div:
++            self._log.debug(u'Received unusual song page html')
++            verse_div = html.find("div",
++                                  class_=re.compile("Lyrics__Container"))
++            if not verse_div:
++                if html.find("div",
++                             class_=re.compile("LyricsPlaceholder__Message"),
++                             string="This song is an instrumental"):
++                    self._log.debug('Detected instrumental')
++                    return "[Instrumental]"
++                else:
++                    self._log.debug("Couldn't scrape page using known 
layouts")
++                    return None
+ 
+-        return lyrics
++            lyrics_div = verse_div.parent
++            for br in lyrics_div.find_all("br"):
++                br.replace_with("\n")
++            ads = lyrics_div.find_all("div",
++                                      
class_=re.compile("InreadAd__Container"))
++            for ad in ads:
++                ad.replace_with("\n")
+ 
++        return lyrics_div.get_text()
++
+     def fetch(self, artist, title):
+         search_url = self.base_url + "/search"
+-        data = {'q': title}
++        data = {'q': title + " " + artist.lower()}
+         try:
+             response = requests.get(search_url, data=data,
+                                     headers=self.headers)
+@@ -392,21 +418,25 @@ class Genius(Backend):
+             self._log.debug(u'Genius API request returned invalid JSON')
+             return None
+ 
+-        song_info = None
+         for hit in json["response"]["hits"]:
+-            if hit["result"]["primary_artist"]["name"] == artist:
+-                song_info = hit
+-                break
++            # Genius uses zero-width characters to denote lowercase
++            # artist names.
++            hit_artist = hit["result"]["primary_artist"]["name"]. \
++                strip(u'\u200b').lower()
+ 
+-        if song_info:
+-            song_api_path = song_info["result"]["api_path"]
+-            return self.lyrics_from_song_api_path(song_api_path)
++            if hit_artist == artist.lower():
++                return self.lyrics_from_song_page(hit["result"]["url"])
+ 
++        self._log.debug(u'genius: no matching artist')
+ 
++
+ class LyricsWiki(SymbolsReplaced):
+     """Fetch lyrics from LyricsWiki."""
+ 
+-    URL_PATTERN = 'http://lyrics.wikia.com/%s:%s'
++    if util.SNI_SUPPORTED:
++        URL_PATTERN = 'https://lyrics.wikia.com/%s:%s'
++    else:
++        URL_PATTERN = 'http://lyrics.wikia.com/%s:%s'
+ 
+     def fetch(self, artist, title):
+         url = self.build_url(artist, title)
+@@ -740,7 +770,8 @@ class LyricsPlugin(plugins.BeetsPlugin):
+             write = ui.should_write()
+             if opts.writerest:
+                 self.writerest_indexes(opts.writerest)
+-            for item in lib.items(ui.decargs(args)):
++            items = lib.items(ui.decargs(args))
++            for item in items:
+                 if not opts.local_only and not self.config['local']:
+                     self.fetch_item_lyrics(
+                         lib, item, write,
+@@ -750,10 +781,10 @@ class LyricsPlugin(plugins.BeetsPlugin):
+                     if opts.printlyr:
+                         ui.print_(item.lyrics)
+                     if opts.writerest:
+-                        self.writerest(opts.writerest, item)
+-            if opts.writerest:
+-                # flush last artist
+-                self.writerest(opts.writerest, None)
++                        self.appendrest(opts.writerest, item)
++            if opts.writerest and len(items) > 0:
++                # flush last artist & write to ReST
++                self.writerest(opts.writerest)
+                 ui.print_(u'ReST files generated. to build, use one of:')
+                 ui.print_(u'  sphinx-build -b html %s _build/html'
+                           % opts.writerest)
+@@ -765,26 +796,21 @@ class LyricsPlugin(plugins.BeetsPlugin):
+         cmd.func = func
+         return [cmd]
+ 
+-    def writerest(self, directory, item):
+-        """Write the item to an ReST file
++    def appendrest(self, directory, item):
++        """Append the item to an ReST file
+ 
+         This will keep state (in the `rest` variable) in order to avoid
+         writing continuously to the same files.
+         """
+ 
+-        if item is None or slug(self.artist) != slug(item.albumartist):
+-            if self.rest is not None:
+-                path = os.path.join(directory, 'artists',
+-                                    slug(self.artist) + u'.rst')
+-                with open(path, 'wb') as output:
+-                    output.write(self.rest.encode('utf-8'))
+-                self.rest = None
+-                if item is None:
+-                    return
++        if slug(self.artist) != slug(item.albumartist):
++            # Write current file and start a new one ~ item.albumartist
++            self.writerest(directory)
+             self.artist = item.albumartist.strip()
+             self.rest = u"%s\n%s\n\n.. contents::\n   :local:\n\n" \
+                         % (self.artist,
+                            u'=' * len(self.artist))
++
+         if self.album != item.album:
+             tmpalbum = self.album = item.album.strip()
+             if self.album == '':
+@@ -795,6 +821,16 @@ class LyricsPlugin(plugins.BeetsPlugin):
+         self.rest += u"%s\n%s\n\n%s\n\n" % (title_str,
+                                             u'~' * len(title_str),
+                                             block)
++
++    def writerest(self, directory):
++        """Write self.rest to a ReST file
++        """
++        if self.rest is not None and self.artist is not None:
++            path = os.path.join(directory, 'artists',
++                                slug(self.artist) + u'.rst')
++            with open(path, 'wb') as output:
++                output.write(self.rest.encode('utf-8'))
++
+ 
+     def writerest_indexes(self, directory):
+         """Write conf.py and index.rst files necessary for Sphinx

Reply via email to