Repository: beetbox/beets

Issue:
Lastgenre plugin crashes with canonical_tree: yes and prefer_specific: yes
### Problem

When using the default canonical tree whenever a genre is unmatched [`_get_depth(genre)`](https://github.com/beetbox/beets/blob/4ae02e0d27996cec36e26c4df8c5d3fcfdf717de/beetsplug/lastgenre/__init__.py#L162) will return `None`, which when combined with `prefer_specific` will try to sort this None against a bunch of ints. For example Tycho will return this list (note `(None, chillout)`):

```
[(0, 'electronic'), (1, 'ambient'), (2, 'post-rock'), (1, 'alternative rock'), (0, 'rock'), (1, 'downtempo'), (None, 'chillout'), (2, 'shoegaze'), (2, 'idm'), (1, 'electronica')]
```

```
Traceback (most recent call last):
  File "/Users/wpaul/.asdf/installs/python/3.6.0/bin/beet", line 11, in <module>
    load_entry_point('beets==1.4.3', 'console_scripts', 'beet')()
  File "/Users/wpaul/.asdf/installs/python/3.6.0/lib/python3.6/site-packages/beets/ui/__init__.py", line 1209, in main
    _raw_main(args)
  File "/Users/wpaul/.asdf/installs/python/3.6.0/lib/python3.6/site-packages/beets/ui/__init__.py", line 1196, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "/Users/wpaul/.asdf/installs/python/3.6.0/lib/python3.6/site-packages/beetsplug/lastgenre/__init__.py", line 380, in lastgenre_func
    album.genre, src = self._get_genre(album)
  File "/Users/wpaul/.asdf/installs/python/3.6.0/lib/python3.6/site-packages/beetsplug/lastgenre/__init__.py", line 322, in _get_genre
    result = self.fetch_album_genre(obj)
  File "/Users/wpaul/.asdf/installs/python/3.6.0/lib/python3.6/site-packages/beetsplug/lastgenre/__init__.py", line 273, in fetch_album_genre
    u'album', LASTFM.get_album, obj.albumartist, obj.album
  File "/Users/wpaul/.asdf/installs/python/3.6.0/lib/python3.6/site-packages/beetsplug/lastgenre/__init__.py", line 265, in _last_lookup
    genre = self.fetch_genre(method(*args_replaced))
  File "/Users/wpaul/.asdf/installs/python/3.6.0/lib/python3.6/site-packages/beetsplug/lastgenre/__init__.py", line 228, in fetch_genre
    return self._resolve_genres(self._tags_for(lastfm_obj, min_weight))
  File "/Users/wpaul/.asdf/installs/python/3.6.0/lib/python3.6/site-packages/beetsplug/lastgenre/__init__.py", line 213, in _resolve_genres
    tags = self._sort_by_depth(tags)
  File "/Users/wpaul/.asdf/installs/python/3.6.0/lib/python3.6/site-packages/beetsplug/lastgenre/__init__.py", line 178, in _sort_by_depth
    depth_tag_pairs.sort(reverse=True)
TypeError: '<' not supported between instances of 'NoneType' and 'int'
```

My quick fix was to call [_get_depth in a guard](https://github.com/beetbox/beets/blob/4ae02e0d27996cec36e26c4df8c5d3fcfdf717de/beetsplug/lastgenre/__init__.py#L176):

```
depth_tag_pairs = [(self._get_depth(t), t) for t in tags if self._get_depth(t) != None]
```

Obviously we shouldn't be calling this twice without memoizing.

### Setup

* OS: macOS Sierra
* Python version: 3.6.0
* beets version: 1.4.3

My configuration (output of `beet config`) is:

```yaml
lastgenre:
  canonical: yes
  count: 3
  prefer_specific: yes
```



Discussion:
Thanks for the report and the diagnosis! I agree with your assessment of where the crash comes from.

Since you already have a good handle here on what's going wrong, is there any chance I can convince you to open a pull request? One simple fix, for example, would be to compute that list of pairs as-is and then add a second line that uses a second list comprehension to drop the Nones.
what would a 0 mean in this context?
I believe a 0 means top-level or in other words least specific genre in the the tree. None occurs whenever a genre doesn't exist in the tree.

Resolving patch:
diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py
--- a/beetsplug/lastgenre/__init__.py
+++ b/beetsplug/lastgenre/__init__.py
@@ -174,6 +174,7 @@ def _sort_by_depth(self, tags):
         genre tree.
         """
         depth_tag_pairs = [(self._get_depth(t), t) for t in tags]
+        depth_tag_pairs = [e for e in depth_tag_pairs if e[0] is not None]
         depth_tag_pairs.sort(reverse=True)
         return [p[1] for p in depth_tag_pairs]
 


Test patch:
diff --git a/test/test_lastgenre.py b/test/test_lastgenre.py
--- a/test/test_lastgenre.py
+++ b/test/test_lastgenre.py
@@ -213,6 +213,18 @@ def mock_fetch_artist_genre(self, obj):
         self.assertEqual(res, (config['lastgenre']['fallback'].get(),
                          u'fallback'))
 
+    def test_sort_by_depth(self):
+        self._setup_config(canonical=True)
+        # Normal case.
+        tags = ('electronic', 'ambient', 'post-rock', 'downtempo')
+        res = self.plugin._sort_by_depth(tags)
+        self.assertEqual(
+            res, ['post-rock', 'downtempo', 'ambient', 'electronic'])
+        # Non-canonical tag ('chillout') present.
+        tags = ('electronic', 'ambient', 'chillout')
+        res = self.plugin._sort_by_depth(tags)
+        self.assertEqual(res, ['ambient', 'electronic'])
+
 
 def suite():
     return unittest.TestLoader().loadTestsFromName(__name__)