Repository: beetbox/beets

Issue:
beatport plugin failing 
Hi,

 Trying to test the fix made by @temrix on the beatport plugin but i get errors on any file

Running this command in verbose (`-vv`) mode:

```sh
$ beet -vv import /mnt/d/test/
```

Led to this problem:

```
user configuration: /root/.config/beets/config.yaml
data directory: /root/.config/beets
plugin paths:
Sending event: pluginload
library database: /mnt/d/Music/musiclibrary.db
library directory: /mnt/d/Music
Sending event: library_opened
Sending event: import_begin
Sending event: import_task_created
Sending event: import_task_start
Looking up: /mnt/d/test
Tagging Bawrut - 4x4 (Remixes)
No album ID found.
Search terms: Bawrut - 4x4 (Remixes)
Album might be VA: False
Searching for MusicBrainz releases with: {'release': u'4x4 (remixes)', 'tracks': u'1', 'artist': u'bawrut'}
Requesting MusicBrainz release f68de3a5-30e1-4849-bde5-7237833ee9f9
primary MB release type: single
Sending event: albuminfo_received
Candidate: Bawrut - Ciquita (f68de3a5-30e1-4849-bde5-7237833ee9f9)
Computing track assignment...
...done.
Success. Distance: 0.56
Requesting MusicBrainz release dda48d1f-cf31-47ba-9713-c4414ddef4c2
primary MB release type: ep
Sending event: albuminfo_received
Candidate: Wilmer X - 4x4 (dda48d1f-cf31-47ba-9713-c4414ddef4c2)
Computing track assignment...
...done.
Success. Distance: 0.75
Requesting MusicBrainz release b4c65504-6bd0-4ff8-97a7-9df3ee838b2c
primary MB release type: album
Sending event: albuminfo_received
Candidate: La Unión - 4x4 (b4c65504-6bd0-4ff8-97a7-9df3ee838b2c)
Computing track assignment...
...done.
Success. Distance: 0.80
Requesting MusicBrainz release 207c7744-b6a0-4f10-ab7c-a7b216106121
primary MB release type: album
Sending event: albuminfo_received
Candidate: McCoy Tyner Quartets - 4x4 (207c7744-b6a0-4f10-ab7c-a7b216106121)
Computing track assignment...
...done.
Success. Distance: 0.81
Requesting MusicBrainz release 8a204bbf-7a58-4421-aa19-512590dbfc1c
primary MB release type: ep
Sending event: albuminfo_received
Candidate: Works Progress Administration - 4X4 (8a204bbf-7a58-4421-aa19-512590dbfc1c)
Computing track assignment...
...done.
Success. Distance: 0.71
Traceback (most recent call last):
  File "/usr/local/bin/beet", line 11, in <module>
    load_entry_point('beets==1.5.0', 'console_scripts', 'beet')()
  File "/usr/local/lib/python2.7/dist-packages/beets/ui/__init__.py", line 1267, in main
    _raw_main(args)
  File "/usr/local/lib/python2.7/dist-packages/beets/ui/__init__.py", line 1254, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "/usr/local/lib/python2.7/dist-packages/beets/ui/commands.py", line 956, in import_func
    import_files(lib, paths, query)
  File "/usr/local/lib/python2.7/dist-packages/beets/ui/commands.py", line 926, in import_files
    session.run()
  File "/usr/local/lib/python2.7/dist-packages/beets/importer.py", line 329, in run
    pl.run_parallel(QUEUE_SIZE)
  File "/usr/local/lib/python2.7/dist-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/python2.7/dist-packages/beets/util/pipeline.py", line 312, in run
    out = self.coro.send(msg)
  File "/usr/local/lib/python2.7/dist-packages/beets/util/pipeline.py", line 194, in coro
    func(*(args + (task,)))
  File "/usr/local/lib/python2.7/dist-packages/beets/importer.py", line 1353, in lookup_candidates
    task.lookup_candidates()
  File "/usr/local/lib/python2.7/dist-packages/beets/importer.py", line 641, in lookup_candidates
    autotag.tag_album(self.items, search_ids=self.search_ids)
  File "/usr/local/lib/python2.7/dist-packages/beets/autotag/match.py", line 460, in tag_album
    va_likely):
  File "/usr/local/lib/python2.7/dist-packages/beets/plugins.py", line 576, in decorated
    for v in generator(*args, **kwargs):
  File "/usr/local/lib/python2.7/dist-packages/beets/autotag/hooks.py", line 637, in album_candidates
    for candidate in plugins.candidates(items, artist, album, va_likely):
  File "/usr/local/lib/python2.7/dist-packages/beets/plugins.py", line 386, in candidates
    for candidate in plugin.candidates(items, artist, album, va_likely):
  File "/usr/local/lib/python2.7/dist-packages/beetsplug/beatport.py", line 360, in candidates
    return self._get_releases(query)
  File "/usr/local/lib/python2.7/dist-packages/beetsplug/beatport.py", line 412, in _get_releases
    for x in self.client.search(query)]
  File "/usr/local/lib/python2.7/dist-packages/beetsplug/beatport.py", line 138, in search
    release = self.get_release(item['id'])
  File "/usr/local/lib/python2.7/dist-packages/beetsplug/beatport.py", line 154, in get_release
    release.tracks = self.get_release_tracks(beatport_id)
  File "/usr/local/lib/python2.7/dist-packages/beetsplug/beatport.py", line 166, in get_release_tracks
    return [BeatportTrack(t) for t in response]
  File "/usr/local/lib/python2.7/dist-packages/beetsplug/beatport.py", line 265, in __init__
    self.genre = six.text_type(data['subGenres'][0].get('name'))
IndexError: list index out of range
```

Here's a link to the music files that trigger the bug (if relevant):
https://www.beatport.com/track/more-cowbell-jimpster-remix/10656680
https://www.beatport.com/track/shakti-pan-feat-sven-original-mix/9130725
etc.
Basically any track i tried

### Setup

* OS: Ubuntu 18.04
* Python version: 2.7.15
* beets version: 1.5.0 (latest from git)

My configuration (output of `beet config`) is:

```yaml
paths:
    default: $genre/$year_$month/$artist_$title
    singleton: $genre/$year_$month/$artist_$title
library: /mnt/d/Music/musiclibrary.db

replace:
    '[\\/]': _
    ^\.: _
    '[\x00-\x1f]': _
    '[<>:"\?\*\|]': _
    \.$: _
    \s+$: ''
original_date: yes
path_sep_replace: _

plugins: inline beatport
directory: /mnt/d/Music

import:
    move: no
    copy: yes
    write: yes
beatport:
    source_weight: 1
    tokenfile: beatport_token.json
    apikey: REDACTED
    apisecret: REDACTED
pathfields: {}
album_fields: {}
item_fields: {}
```

Thank you.

Discussion:

Patch:
diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py
index 8ef356fe8..c3933ed73 100644
--- a/beetsplug/beatport.py
+++ b/beetsplug/beatport.py
@@ -150,9 +150,11 @@ class BeatportClient(object):
         :rtype:                 :py:class:`BeatportRelease`
         """
         response = self._get('/catalog/3/releases', id=beatport_id)
-        release = BeatportRelease(response[0])
-        release.tracks = self.get_release_tracks(beatport_id)
-        return release
+        if response:
+            release = BeatportRelease(response[0])
+            release.tracks = self.get_release_tracks(beatport_id)
+            return release
+        return None
 
     def get_release_tracks(self, beatport_id):
         """ Get all tracks for a given release.
@@ -261,10 +263,10 @@ class BeatportTrack(BeatportObject):
             self.musical_key = six.text_type(data['key'].get('shortName'))
 
         # Use 'subgenre' and if not present, 'genre' as a fallback.
-        if 'subGenres' in data:
+        if data.get('subGenres'):
             self.genre = six.text_type(data['subGenres'][0].get('name'))
-            if not self.genre and 'genres' in data:
-                self.genre = six.text_type(data['genres'][0].get('name'))
+        elif data.get('genres'):
+            self.genre = six.text_type(data['genres'][0].get('name'))
 
 
 class BeatportPlugin(BeetsPlugin):
@@ -375,27 +377,31 @@ class BeatportPlugin(BeetsPlugin):
 
     def album_for_id(self, release_id):
         """Fetches a release by its Beatport ID and returns an AlbumInfo object
-        or None if the release is not found.
+        or None if the query is not a valid ID or release is not found.
         """
         self._log.debug(u'Searching for release {0}', release_id)
         match = re.search(r'(^|beatport\.com/release/.+/)(\d+)$', release_id)
         if not match:
+            self._log.debug(u'Not a valid Beatport release ID.')
             return None
         release = self.client.get_release(match.group(2))
-        album = self._get_album_info(release)
-        return album
+        if release:
+            return self._get_album_info(release)
+        return None
 
     def track_for_id(self, track_id):
         """Fetches a track by its Beatport ID and returns a TrackInfo object
-        or None if the track is not found.
+        or None if the track is not a valid Beatport ID or track is not found.
         """
         self._log.debug(u'Searching for track {0}', track_id)
         match = re.search(r'(^|beatport\.com/track/.+/)(\d+)$', track_id)
         if not match:
+            self._log.debug(u'Not a valid Beatport track ID.')
             return None
         bp_track = self.client.get_track(match.group(2))
-        track = self._get_track_info(bp_track)
-        return track
+        if bp_track is not None:
+            return self._get_track_info(bp_track)
+        return None
 
     def _get_releases(self, query):
         """Returns a list of AlbumInfo objects for a beatport search query.
diff --git a/docs/changelog.rst b/docs/changelog.rst
index aa87929c6..261b74505 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -107,6 +107,8 @@ Fixes:
 * ``none_rec_action`` does not import automatically when ``timid`` is enabled.
   Thanks to :user:`RollingStar`.
   :bug:`3242`
+* Fix a bug that caused a crash when tagging items with the beatport plugin.
+  :bug:`3374`
 
 For plugin developers:
 


Test patch:
diff --git a/test/test_beatport.py b/test/test_beatport.py
index a1591d58c..8e830df76 100644
--- a/test/test_beatport.py
+++ b/test/test_beatport.py
@@ -31,7 +31,7 @@ class BeatportTest(_common.TestCase, TestHelper):
     def _make_release_response(self):
         """Returns a dict that mimics a response from the beatport API.
 
-        The results were retrived from:
+        The results were retrieved from:
         https://oauth-api.beatport.com/catalog/3/releases?id=1742984
         The list of elements on the returned dict is incomplete, including just
         those required for the tests on this class.
@@ -72,7 +72,7 @@ class BeatportTest(_common.TestCase, TestHelper):
     def _make_tracks_response(self):
         """Return a list that mimics a response from the beatport API.
 
-        The results were retrived from:
+        The results were retrieved from:
         https://oauth-api.beatport.com/catalog/3/tracks?releaseId=1742984
         The list of elements on the returned list is incomplete, including just
         those required for the tests on this class.
@@ -425,7 +425,6 @@ class BeatportTest(_common.TestCase, TestHelper):
 
         # Set up 'test_album'.
         self.test_album = self.mk_test_album()
-        # print(self.test_album.keys())
 
         # Set up 'test_tracks'
         self.test_tracks = self.test_album.items()
@@ -559,6 +558,70 @@ class BeatportTest(_common.TestCase, TestHelper):
             self.assertEqual(track.genre, test_track.genre)
 
 
+class BeatportResponseEmptyTest(_common.TestCase, TestHelper):
+    def _make_tracks_response(self):
+        results = [{
+            "id": 7817567,
+            "name": "Mirage a Trois",
+            "genres": [{
+              "id": 9,
+              "name": "Breaks",
+              "slug": "breaks",
+              "type": "genre"
+            }],
+            "subGenres": [{
+              "id": 209,
+              "name": "Glitch Hop",
+              "slug": "glitch-hop",
+              "type": "subgenre"
+            }],
+        }]
+        return results
+
+    def setUp(self):
+        self.setup_beets()
+        self.load_plugins('beatport')
+        self.lib = library.Library(':memory:')
+
+        # Set up 'tracks'.
+        self.response_tracks = self._make_tracks_response()
+        self.tracks = [beatport.BeatportTrack(t) for t in self.response_tracks]
+
+        # Make alias to be congruent with class `BeatportTest`.
+        self.test_tracks = self.response_tracks
+
+    def tearDown(self):
+        self.unload_plugins()
+        self.teardown_beets()
+
+    def test_response_tracks_empty(self):
+        response_tracks = []
+        tracks = [beatport.BeatportTrack(t) for t in response_tracks]
+        self.assertEqual(tracks, [])
+
+    def test_sub_genre_empty_fallback(self):
+        """No 'sub_genre' is provided. Test if fallback to 'genre' works.
+        """
+        self.response_tracks[0]['subGenres'] = []
+        tracks = [beatport.BeatportTrack(t) for t in self.response_tracks]
+
+        self.test_tracks[0]['subGenres'] = []
+
+        self.assertEqual(tracks[0].genre,
+                         self.test_tracks[0]['genres'][0]['name'])
+
+    def test_genre_empty(self):
+        """No 'genre' is provided. Test if 'sub_genre' is applied.
+        """
+        self.response_tracks[0]['genres'] = []
+        tracks = [beatport.BeatportTrack(t) for t in self.response_tracks]
+
+        self.test_tracks[0]['genres'] = []
+
+        self.assertEqual(tracks[0].genre,
+                         self.test_tracks[0]['subGenres'][0]['name'])
+
+
 def suite():
     return unittest.TestLoader().loadTestsFromName(__name__)