Merge pull request #3372 from temrix/master

Fix #2080
This commit is contained in:
Adrian Sampson 2019-09-19 17:22:11 -04:00 committed by GitHub
commit db7ef0e60e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 603 additions and 7 deletions

View file

@ -185,6 +185,9 @@ def apply_metadata(album_info, mapping):
'work',
'mb_workid',
'work_disambig',
'bpm',
'musical_key',
'genre'
)
}

View file

@ -179,7 +179,8 @@ class TrackInfo(object):
disctitle=None, artist_credit=None, data_source=None,
data_url=None, media=None, lyricist=None, composer=None,
composer_sort=None, arranger=None, track_alt=None,
work=None, mb_workid=None, work_disambig=None):
work=None, mb_workid=None, work_disambig=None, bpm=None,
musical_key=None, genre=None):
self.title = title
self.track_id = track_id
self.release_track_id = release_track_id
@ -204,6 +205,9 @@ class TrackInfo(object):
self.work = work
self.mb_workid = mb_workid
self.work_disambig = work_disambig
self.bpm = bpm
self.musical_key = musical_key
self.genre = genre
# As above, work around a bug in python-musicbrainz-ngs.
def decode(self, codec='utf-8'):

View file

@ -255,6 +255,16 @@ class BeatportTrack(BeatportObject):
self.url = "https://beatport.com/track/{0}/{1}" \
.format(data['slug'], data['id'])
self.track_number = data.get('trackNumber')
if 'bpm' in data:
self.bpm = data['bpm']
if 'key' in data:
self.musical_key = six.text_type(data['key'].get('shortName'))
# Use 'subgenre' and if not present, 'genre' as a fallback.
if 'subGenres' in data:
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'))
class BeatportPlugin(BeetsPlugin):
@ -433,7 +443,8 @@ class BeatportPlugin(BeetsPlugin):
artist=artist, artist_id=artist_id,
length=length, index=track.track_number,
medium_index=track.track_number,
data_source=u'Beatport', data_url=track.url)
data_source=u'Beatport', data_url=track.url,
bpm=track.bpm, musical_key=track.musical_key)
def _get_artist(self, artists):
"""Returns an artist string (all artists) and an artist_id (the main

View file

@ -70,6 +70,9 @@ New features:
you can now match tracks and albums using the `Deezer`_ database.
Thanks to :user:`rhlahuja`.
:bug:`3355`
* :doc:`/plugins/beatport`: The plugin now gets the musical key, BPM and the
genre for each track.
:bug:`2080`
Fixes:
@ -136,6 +139,8 @@ For plugin developers:
APIs to provide metadata matches for the importer. Refer to the Spotify and
Deezer plugins for examples of using this template class.
:bug:`3355`
* The autotag hooks have been modified such that they now take 'bpm',
'musical_key' and a per-track based 'genre' as attributes.
For packagers:

View file

@ -4,7 +4,9 @@ Beatport Plugin
The ``beatport`` plugin adds support for querying the `Beatport`_ catalogue
during the autotagging process. This can potentially be helpful for users
whose collection includes a lot of diverse electronic music releases, for which
both MusicBrainz and (to a lesser degree) Discogs show no matches.
both MusicBrainz and (to a lesser degree) `Discogs`_ show no matches.
.. _Discogs: https://discogs.com
Installation
------------
@ -21,15 +23,18 @@ run the :ref:`import-cmd` command after enabling the plugin, it will ask you
to authorize with Beatport by visiting the site in a browser. On the site
you will be asked to enter your username and password to authorize beets
to query the Beatport API. You will then be displayed with a single line of
text that you should paste into your terminal. This will store the
authentication data for subsequent runs and you will not be required to
repeat the above steps.
text that you should paste as a whole into your terminal. This will store the
authentication data for subsequent runs and you will not be required to repeat
the above steps.
Matches from Beatport should now show up alongside matches
from MusicBrainz and other sources.
If you have a Beatport ID or a URL for a release or track you want to tag, you
can just enter one of the two at the "enter Id" prompt in the importer.
can just enter one of the two at the "enter Id" prompt in the importer. You can
also search for an id like so:
beet import path/to/music/library --search-id id
.. _requests: https://docs.python-requests.org/en/latest/
.. _requests_oauthlib: https://github.com/requests/requests-oauthlib

567
test/test_beatport.py Normal file
View file

@ -0,0 +1,567 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for the 'beatport' plugin.
"""
from __future__ import division, absolute_import, print_function
import unittest
from test import _common
from test.helper import TestHelper
import six
from datetime import timedelta
from beetsplug import beatport
from beets import library
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:
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.
"""
results = {
"id": 1742984,
"type": "release",
"name": "Charade",
"slug": "charade",
"releaseDate": "2016-04-11",
"publishDate": "2016-04-11",
"audioFormat": "",
"category": "Release",
"currentStatus": "General Content",
"catalogNumber": "GR089",
"description": "",
"label": {
"id": 24539,
"name": "Gravitas Recordings",
"type": "label",
"slug": "gravitas-recordings"
},
"artists": [{
"id": 326158,
"name": "Supersillyus",
"slug": "supersillyus",
"type": "artist"
}],
"genres": [{
"id": 9,
"name": "Breaks",
"slug": "breaks",
"type": "genre"
}],
}
return results
def _make_tracks_response(self):
"""Return a list that mimics a response from the beatport API.
The results were retrived 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.
"""
results = [{
"id": 7817567,
"type": "track",
"sku": "track-7817567",
"name": "Mirage a Trois",
"trackNumber": 1,
"mixName": "Original Mix",
"title": "Mirage a Trois (Original Mix)",
"slug": "mirage-a-trois-original-mix",
"releaseDate": "2016-04-11",
"publishDate": "2016-04-11",
"currentStatus": "General Content",
"length": "7:05",
"lengthMs": 425421,
"bpm": 90,
"key": {
"standard": {
"letter": "G",
"sharp": False,
"flat": False,
"chord": "minor"
},
"shortName": "Gmin"
},
"artists": [{
"id": 326158,
"name": "Supersillyus",
"slug": "supersillyus",
"type": "artist"
}],
"genres": [{
"id": 9,
"name": "Breaks",
"slug": "breaks",
"type": "genre"
}],
"subGenres": [{
"id": 209,
"name": "Glitch Hop",
"slug": "glitch-hop",
"type": "subgenre"
}],
"release": {
"id": 1742984,
"name": "Charade",
"type": "release",
"slug": "charade"
},
"label": {
"id": 24539,
"name": "Gravitas Recordings",
"type": "label",
"slug": "gravitas-recordings",
"status": True
}
}, {
"id": 7817568,
"type": "track",
"sku": "track-7817568",
"name": "Aeon Bahamut",
"trackNumber": 2,
"mixName": "Original Mix",
"title": "Aeon Bahamut (Original Mix)",
"slug": "aeon-bahamut-original-mix",
"releaseDate": "2016-04-11",
"publishDate": "2016-04-11",
"currentStatus": "General Content",
"length": "7:38",
"lengthMs": 458000,
"bpm": 100,
"key": {
"standard": {
"letter": "G",
"sharp": False,
"flat": False,
"chord": "major"
},
"shortName": "Gmaj"
},
"artists": [{
"id": 326158,
"name": "Supersillyus",
"slug": "supersillyus",
"type": "artist"
}],
"genres": [{
"id": 9,
"name": "Breaks",
"slug": "breaks",
"type": "genre"
}],
"subGenres": [{
"id": 209,
"name": "Glitch Hop",
"slug": "glitch-hop",
"type": "subgenre"
}],
"release": {
"id": 1742984,
"name": "Charade",
"type": "release",
"slug": "charade"
},
"label": {
"id": 24539,
"name": "Gravitas Recordings",
"type": "label",
"slug": "gravitas-recordings",
"status": True
}
}, {
"id": 7817569,
"type": "track",
"sku": "track-7817569",
"name": "Trancendental Medication",
"trackNumber": 3,
"mixName": "Original Mix",
"title": "Trancendental Medication (Original Mix)",
"slug": "trancendental-medication-original-mix",
"releaseDate": "2016-04-11",
"publishDate": "2016-04-11",
"currentStatus": "General Content",
"length": "1:08",
"lengthMs": 68571,
"bpm": 141,
"key": {
"standard": {
"letter": "F",
"sharp": False,
"flat": False,
"chord": "major"
},
"shortName": "Fmaj"
},
"artists": [{
"id": 326158,
"name": "Supersillyus",
"slug": "supersillyus",
"type": "artist"
}],
"genres": [{
"id": 9,
"name": "Breaks",
"slug": "breaks",
"type": "genre"
}],
"subGenres": [{
"id": 209,
"name": "Glitch Hop",
"slug": "glitch-hop",
"type": "subgenre"
}],
"release": {
"id": 1742984,
"name": "Charade",
"type": "release",
"slug": "charade"
},
"label": {
"id": 24539,
"name": "Gravitas Recordings",
"type": "label",
"slug": "gravitas-recordings",
"status": True
}
}, {
"id": 7817570,
"type": "track",
"sku": "track-7817570",
"name": "A List of Instructions for When I'm Human",
"trackNumber": 4,
"mixName": "Original Mix",
"title": "A List of Instructions for When I'm Human (Original Mix)",
"slug": "a-list-of-instructions-for-when-im-human-original-mix",
"releaseDate": "2016-04-11",
"publishDate": "2016-04-11",
"currentStatus": "General Content",
"length": "6:57",
"lengthMs": 417913,
"bpm": 88,
"key": {
"standard": {
"letter": "A",
"sharp": False,
"flat": False,
"chord": "minor"
},
"shortName": "Amin"
},
"artists": [{
"id": 326158,
"name": "Supersillyus",
"slug": "supersillyus",
"type": "artist"
}],
"genres": [{
"id": 9,
"name": "Breaks",
"slug": "breaks",
"type": "genre"
}],
"subGenres": [{
"id": 209,
"name": "Glitch Hop",
"slug": "glitch-hop",
"type": "subgenre"
}],
"release": {
"id": 1742984,
"name": "Charade",
"type": "release",
"slug": "charade"
},
"label": {
"id": 24539,
"name": "Gravitas Recordings",
"type": "label",
"slug": "gravitas-recordings",
"status": True
}
}, {
"id": 7817571,
"type": "track",
"sku": "track-7817571",
"name": "The Great Shenanigan",
"trackNumber": 5,
"mixName": "Original Mix",
"title": "The Great Shenanigan (Original Mix)",
"slug": "the-great-shenanigan-original-mix",
"releaseDate": "2016-04-11",
"publishDate": "2016-04-11",
"currentStatus": "General Content",
"length": "9:49",
"lengthMs": 589875,
"bpm": 123,
"key": {
"standard": {
"letter": "E",
"sharp": False,
"flat": True,
"chord": "major"
},
"shortName": "E♭maj"
},
"artists": [{
"id": 326158,
"name": "Supersillyus",
"slug": "supersillyus",
"type": "artist"
}],
"genres": [{
"id": 9,
"name": "Breaks",
"slug": "breaks",
"type": "genre"
}],
"subGenres": [{
"id": 209,
"name": "Glitch Hop",
"slug": "glitch-hop",
"type": "subgenre"
}],
"release": {
"id": 1742984,
"name": "Charade",
"type": "release",
"slug": "charade"
},
"label": {
"id": 24539,
"name": "Gravitas Recordings",
"type": "label",
"slug": "gravitas-recordings",
"status": True
}
}, {
"id": 7817572,
"type": "track",
"sku": "track-7817572",
"name": "Charade",
"trackNumber": 6,
"mixName": "Original Mix",
"title": "Charade (Original Mix)",
"slug": "charade-original-mix",
"releaseDate": "2016-04-11",
"publishDate": "2016-04-11",
"currentStatus": "General Content",
"length": "7:05",
"lengthMs": 425423,
"bpm": 123,
"key": {
"standard": {
"letter": "A",
"sharp": False,
"flat": False,
"chord": "major"
},
"shortName": "Amaj"
},
"artists": [{
"id": 326158,
"name": "Supersillyus",
"slug": "supersillyus",
"type": "artist"
}],
"genres": [{
"id": 9,
"name": "Breaks",
"slug": "breaks",
"type": "genre"
}],
"subGenres": [{
"id": 209,
"name": "Glitch Hop",
"slug": "glitch-hop",
"type": "subgenre"
}],
"release": {
"id": 1742984,
"name": "Charade",
"type": "release",
"slug": "charade"
},
"label": {
"id": 24539,
"name": "Gravitas Recordings",
"type": "label",
"slug": "gravitas-recordings",
"status": True
}
}]
return results
def setUp(self):
self.setup_beets()
self.load_plugins('beatport')
self.lib = library.Library(':memory:')
# Set up 'album'.
response_release = self._make_release_response()
self.album = beatport.BeatportRelease(response_release)
# Set up 'tracks'.
response_tracks = self._make_tracks_response()
self.tracks = [beatport.BeatportTrack(t) for t in response_tracks]
# 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()
def tearDown(self):
self.unload_plugins()
self.teardown_beets()
def mk_test_album(self):
items = [_common.item() for _ in range(6)]
for item in items:
item.album = 'Charade'
item.catalognum = 'GR089'
item.label = 'Gravitas Recordings'
item.artist = 'Supersillyus'
item.year = 2016
item.comp = False
item.label_name = 'Gravitas Recordings'
item.genre = 'Glitch Hop'
item.year = 2016
item.month = 4
item.day = 11
item.mix_name = 'Original Mix'
items[0].title = 'Mirage a Trois'
items[1].title = 'Aeon Bahamut'
items[2].title = 'Trancendental Medication'
items[3].title = 'A List of Instructions for When I\'m Human'
items[4].title = 'The Great Shenanigan'
items[5].title = 'Charade'
items[0].length = timedelta(minutes=7, seconds=5).total_seconds()
items[1].length = timedelta(minutes=7, seconds=38).total_seconds()
items[2].length = timedelta(minutes=1, seconds=8).total_seconds()
items[3].length = timedelta(minutes=6, seconds=57).total_seconds()
items[4].length = timedelta(minutes=9, seconds=49).total_seconds()
items[5].length = timedelta(minutes=7, seconds=5).total_seconds()
items[0].url = 'mirage-a-trois-original-mix'
items[1].url = 'aeon-bahamut-original-mix'
items[2].url = 'trancendental-medication-original-mix'
items[3].url = 'a-list-of-instructions-for-when-im-human-original-mix'
items[4].url = 'the-great-shenanigan-original-mix'
items[5].url = 'charade-original-mix'
counter = 0
for item in items:
counter += 1
item.track_number = counter
items[0].bpm = 90
items[1].bpm = 100
items[2].bpm = 141
items[3].bpm = 88
items[4].bpm = 123
items[5].bpm = 123
items[0].musical_key = 'Gmin'
items[1].musical_key = 'Gmaj'
items[2].musical_key = 'Fmaj'
items[3].musical_key = 'Amin'
items[4].musical_key = 'E♭maj'
items[5].musical_key = 'Amaj'
for item in items:
self.lib.add(item)
album = self.lib.add_album(items)
album.store()
return album
# Test BeatportRelease.
def test_album_name_applied(self):
self.assertEqual(self.album.name, self.test_album['album'])
def test_catalog_number_applied(self):
self.assertEqual(self.album.catalog_number,
self.test_album['catalognum'])
def test_label_applied(self):
self.assertEqual(self.album.label_name, self.test_album['label'])
def test_category_applied(self):
self.assertEqual(self.album.category, 'Release')
def test_album_url_applied(self):
self.assertEqual(self.album.url,
'https://beatport.com/release/charade/1742984')
# Test BeatportTrack.
def test_title_applied(self):
for track, test_track in zip(self.tracks, self.test_tracks):
self.assertEqual(track.name, test_track.title)
def test_mix_name_applied(self):
for track, test_track in zip(self.tracks, self.test_tracks):
self.assertEqual(track.mix_name, test_track.mix_name)
def test_length_applied(self):
for track, test_track in zip(self.tracks, self.test_tracks):
self.assertEqual(int(track.length.total_seconds()),
int(test_track.length))
def test_track_url_applied(self):
# Specify beatport ids here because an 'item.id' is beets-internal.
ids = [
7817567,
7817568,
7817569,
7817570,
7817571,
7817572,
]
# Concatenate with 'id' to pass strict equality test.
for track, test_track, id in zip(self.tracks, self.test_tracks, ids):
self.assertEqual(
track.url, 'https://beatport.com/track/' +
test_track.url + '/' + six.text_type(id))
def test_bpm_applied(self):
for track, test_track in zip(self.tracks, self.test_tracks):
self.assertEqual(track.bpm, test_track.bpm)
def test_musical_key_applied(self):
for track, test_track in zip(self.tracks, self.test_tracks):
self.assertEqual(track.musical_key, test_track.musical_key)
def test_genre_applied(self):
for track, test_track in zip(self.tracks, self.test_tracks):
self.assertEqual(track.genre, test_track.genre)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')

View file

@ -24,6 +24,7 @@ deps =
python-mpd2
coverage
discogs-client
requests_oauthlib
[_flake8]
deps =