Repository: beetbox/beets

Issue:
lyrics: Crash when BeautifulSoup is not installed
### Problem
The Lyrics plugin throws an error/crashes when ran without BeautifulSoup installed.

### Terminal output
_I currently can't run a verbose command because Beets is dragging itself through finding lyrics for many many albums._

The following error is displayed in Terminal when running:
```
$ beets lyrics artist song
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/beets/util/confit.py:21: UserWarning: beets.util.confit is deprecated; use confuse instead
  warnings.warn("beets.util.confit is deprecated; use confuse instead")
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.7/bin/beet", line 11, in <module>
    load_entry_point('beets==1.5.0', 'console_scripts', 'beet')()
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/beets/ui/__init__.py", line 1291, in main
    _raw_main(args)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/beets/ui/__init__.py", line 1274, in _raw_main
    subcommands, plugins, lib = _setup(options, lib)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/beets/ui/__init__.py", line 1153, in _setup
    plugins = _load_plugins(options, config)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/beets/ui/__init__.py", line 1139, in _load_plugins
    plugins.send("pluginload")
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/beets/plugins.py", line 497, in send
    for handler in event_handlers()[event]:
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/beets/plugins.py", line 480, in event_handlers
    for plugin in find_plugins():
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/beets/plugins.py", line 315, in find_plugins
    _instances[cls] = cls()
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/beetsplug/lyrics.py", line 736, in __init__
    sources = self.sanitize_bs_sources(sources)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/beetsplug/lyrics.py", line 763, in sanitize_bs_sources
    if source.REQUIRES_BS:
AttributeError: 'str' object has no attribute 'REQUIRES_BS'
``````

After installing BeautifulSoup the output is
```
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/beets/util/confit.py:21: UserWarning: beets.util.confit is deprecated; use confuse instead
  warnings.warn("beets.util.confit is deprecated; use confuse instead")
```

Andrew writes:
> We're supposed to silently fix up the configuration when BeautifulSoup is not installed; not crash with a traceback. Glancing at the code, I do see the mistake.

### Setup

* OS: macOS 10.13.6
* Python version: 3.7
* beets version: 1.5
* Turning off plugins made problem go away (yes/no):  

My configuration (output of `beet config`) is:

```yaml
library: ~/Music/Beets/musiclibrary.db
directory: /Volumes/Tunez

import:
    write: yes
    copy: yes
    move: no
    link: no
    hardlink: no
    delete: no
    resume: ask
    incremental: no
    incremental_skip_later: no
    from_scratch: no
    quiet_fallback: skip
    none_rec_action: ask
    timid: no
    log:
    autotag: yes
    quiet: no
    singletons: no
    default_action: apply
    languages: []
    detail: no
    flat: no
    group_albums: no
    pretend: false
    search_ids: []
    duplicate_action: remove
    bell: no
    set_fields: {}

clutter: ["Thumbs.DB", ".DS_Store"]
ignore: [".*", "*~", "System Volume Information", "lost+found"]
ignore_hidden: yes

replace:
    '[\\/]': _
    '^\.': _
    '[\x00-\x1f]': _
    '[<>:"\?\*\|]': ''
    '\.$': _
    '\s+$': ''
    '^\s+': ''
    '^-': _
path_sep_replace: _
asciify_paths: false
art_filename: cover
max_filename_length: 0

aunique:
    keys: albumartist album
    disambiguators: albumtype year samplerate label catalognum albumdisambig releasegroupdisambig
    bracket: '()'

plugins: 
    - inline
    - the
    - lastgenre
    - discogs
    - chroma
    - acousticbrainz
    - fromfilename
    - fetchart
    - embedart
    - ftintitle
    - edit
    - info
    - missing
    - types
    - convert
    - plexupdate
    - web
    - unimported
    - keyfinder
    - fuzzy
    - spotify
    - describe
    - bpmanalyser
    - lyrics
#    - smartplaylist
#    - goingrunning
pluginpath: 
    - /Users/xxxxxxxx/Documents/BeetsPluginDescribe/beetsplug
threaded: yes
timeout: 5.0
per_disc_numbering: no
verbose: 0
terminal_encoding:
original_date: no
artist_credit: no
id3v23: no
va_name: "Various Artists"

zero:
    fields: comments
    update_database: true

# Inline plugin template
item_fields:
  multidisc: 1 if disctotal > 1 else 0
  my_samplerate: round(samplerate / 1000)
  is_flac: 1 if format == "FLAC" else 0
album_fields:  
  format: |
       formatList = []
       for item in items:
           formatList.append(item.format)
       return formatList
  av_bitrate: |
       total = 0
       for item in items:
           total += item.bitrate
       return round(total / len(items) / 1000)
  album_bitdepth:  |
       total = 0
       for item in items:
           total += item.bitdepth
       return round(total / len(items))
  album_samplerate:  |
       total = 0
       for item in items:
           total += item.samplerate
       return round(total / len(items) / 1000)
  is_1644: |
       bd = 0
       sr = 0
       br = 0
       for item in items:
           bd += item.bitdepth
           sr += item.samplerate
           br += item.bitrate
       bd = round(bd / len(items))
       sr = round(sr / len(items) / 1000)
       br = round(br / len(items) / 1000)
       return 1 if bd == 16 and sr == 44 and br > 320 else 0

ui:
    terminal_width: 80
    length_diff_thresh: 10.0
    color: yes
    colors:
        text_success: green
        text_warning: yellow
        text_error: red
        text_highlight: red
        text_highlight_minor: lightgray
        action_default: turquoise
        action: blue

format_item: $artist - $album - $title
format_album: $albumartist - $album
time_format: '%Y-%m-%d %H:%M:%S'
format_raw_length: no

sort_album: albumartist+ album+
sort_item: artist+ album+ disc+ track+
sort_case_insensitive: yes

paths:
#  default: 'Albums/%the{$albumartist}/$album (%if{$original_year,$original_year}) %aunique{albumartist album year, albumdisambig}%if{$albumdisambig,($albumdisambig $year) }%if{$is_1644,,%if{$is_flac,($format $bitdepth-$album_samplerate),($format $av_bitrate)}}/%if{$multidisc,$disc-}$track - $title'
  default: 'Albums/%the{$albumartist}/%if{$original_year,$original_year} - $album %aunique{albumartist album year, albumdisambig}%if{$albumdisambig,($albumdisambig $year) }%if{$is_1644,,%if{$is_flac,($format $album_bitdepth-$album_samplerate),($format $av_bitrate)}}/%if{$multidisc,$disc-}$track - $title'
  dancetrack:1: 'Singles Dance/$artist - $title (%if{$original_year,$original_year}) %aunique{albumartist album year, albumtype label catalognum albumdisambig}%if{$albumdisambig,($albumdisambig$ $year)}($bpm bpm)'
  artistsingles:1: 'Albums/%the{$albumartist}/$album/$title'
  custcomp:1: 'Compilations/$album/$track - $artist - $title'
  custcompmin:1: 'Compilations/$album/$artist - $title'
  singleton: 'Singles/$artist - $title %aunique{albumartist album year, albumtype label catalognum albumdisambig}%if{$albumdisambig,($albumdisambig$ $year)}'
  comp: 'Albums/%the{$albumartist}/$album (%if{$original_year,$original_year}) %aunique{albumartist album year, albumtype label catalognum albumdisambig}%if{$albumdisambig,($albumdisambig $year) }%if{$is_1644,,%if{$is_flac,($format $bitdepth-$album_samplerate),($format $av_bitrate)}}/%if{$multidisc,$disc-}$track - $artist - $title'


statefile: state.pickle

musicbrainz:
    host: musicbrainz.org
    ratelimit: 1
    ratelimit_interval: 1.0
    searchlimit: 5

match:
    strong_rec_thresh: 0.04
    medium_rec_thresh: 0.25
    rec_gap_thresh: 0.25
    max_rec:
        missing_tracks: medium
        unmatched_tracks: medium
    distance_weights:
        source: 2.0
        artist: 3.0
        album: 3.0
        media: 1.0
        mediums: 1.0
        year: 1.0
        country: 0.5
        label: 0.5
        catalognum: 0.5
        albumdisambig: 0.5
        album_id: 5.0
        tracks: 2.0
        missing_tracks: 0.9
        unmatched_tracks: 0.6
        track_title: 3.0
        track_artist: 2.0
        track_index: 1.0
        track_length: 2.0
        track_id: 5.0
    preferred:
        countries: []
        media: ['Digital Media|File' , 'CD']
        original_year: no
    ignored: []
    required: []
    ignored_media: []
    ignore_data_tracks: yes
    ignore_video_tracks: yes
    track_length_grace: 10
    track_length_max: 30
    
chroma:
    auto: no
    
fetchart:
    auto: yes
    cautious: no
    cover_names: cover front art album folder
    minwidth: 290
    maxwidth: 1050
    sources: itunes amazon albumart lastfm fanart filesystem coverart 
    fanarttv_key: xxxxxxxxxxxxx
    lastfm_key: xxxxxxxxxxxx
    
acoustid: 
    apikey: xxxxxxxxx
   
embedart:
    auto: yes
    ifempty: no   
    
lastgenre:
    auto: yes
    canonical: no
    count: 1
    force: yes
    source: album
    whitelist: yes
    min_weight: 10
    fallback:
    separator: ', '
    prefer_specific: no
    
types:
    av_bitrate: int
    my_samplerate: int
    bitdepth: int
    album_bitdepth: int
    album_samplerate: int
    rating: int
    
convert:
    dest: ~/Music/BeetExports
    command: ffmpeg -i $source -y -vn -aq 0 $dest    
    extension: mp3
    format: mp3
    never_convert_lossy_files: yes

plex:
    host: localhost
    port: 32400
    token: xxxxxxxxxxxxxx
    
lastfm:
    user: xxxxxxxxxx
    
smartplaylist:
    playlist_dir: /Users/Shared/Music/Playlists
    auto: no
    playlists:
        - name: 'favorites.m3u' 
          query: rating:4..5
        - name: 'danceable.m3u'
          query: 
            - danceable:0.8..
        - name: 'acoustic.m3u'
          query: 
            - mood_acoustic:0.8..
        - name: 'aggresive.m3u'
          query: 
            - mood_aggresive:0.8..
        - name: 'electronic.m3u'
          query: 
            - mood_electronic:0.8..
        - name: 'danceable.m3u'
          query: 
            - danceable:0.8..
        - name: 'happy.m3u'
          query: 
            - mood_happy:0.8..           
        - name: 'party.m3u'
          query: 
            - mood_party:0.8.. 
        - name: 'relaxed.m3u'
          query: 
            - mood_relaxed:0.8..                   
        - name: 'sad.m3u'
          query: 
            - mood_sad:0.8..

spotify:
    mode: list
    show_failures: on
    tiebreak: first
    
bpmanalyser:
    auto: no
    dry-run: no
    write: yes
    force: no
    quiet: no
    
goingrunning:
  targets:
    subsonic_m3u:
      device_root: /Users/Shared/Music/Playlists
      device_path: 
      clean_target: yes
      generate_playlist: yes
      copy_files: no
  trainings:
    10K:
      use_flavours: [intensity_low, intensity_high, running]
      duration: 180
      target: subsonic_m3u
  flavours:
    intensity_low:
      bpm: 87..88
    intensity_high:
      bpm: 175..177
    running:
      genre: [electronic, hip hop, funk, trip hop, house]

web:
    host: 0.0.0.0
    cors: 'http://localhost:3000'
    
lyrics:
    auto: yes
    google_API_key: xxxxxxxxxxxxxxxxx
    google_engine_ID: 009217259823014548361:lndtuqkycfu
    
unimported:
    ignore_extensions: mid wav
```


Discussion:
Thanks! This looks like a recently-introduced bug. The root cause is a function that expects a `Source` object but is actually receiving a string.
The commit that introduced this bug is 316b79f72fd5c4f39e4d14b417c7bb5fd8864e4a by @wisp3rwind.

Resolving patch:
diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py
--- a/beetsplug/lyrics.py
+++ b/beetsplug/lyrics.py
@@ -757,7 +757,7 @@ def __init__(self):
     def sanitize_bs_sources(self, sources):
         enabled_sources = []
         for source in sources:
-            if source.REQUIRES_BS:
+            if self.SOURCE_BACKENDS[source].REQUIRES_BS:
                 self._log.debug('To use the %s lyrics source, you must '
                                 'install the beautifulsoup4 module. See '
                                 'the documentation for further details.'


Test patch: