beets/test/test_mb.py

656 lines
26 KiB
Python

# 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 MusicBrainz API wrapper.
"""
from test import _common
from beets.autotag import mb
from beets import config
import unittest
from unittest import mock
class MBAlbumInfoTest(_common.TestCase):
def _make_release(self, date_str='2009', tracks=None, track_length=None,
track_artist=False, data_tracks=None,
medium_format='FORMAT'):
release = {
'title': 'ALBUM TITLE',
'id': 'ALBUM ID',
'asin': 'ALBUM ASIN',
'disambiguation': 'R_DISAMBIGUATION',
'release-group': {
'type': 'Album',
'first-release-date': date_str,
'id': 'RELEASE GROUP ID',
'disambiguation': 'RG_DISAMBIGUATION',
},
'artist-credit': [
{
'artist': {
'name': 'ARTIST NAME',
'id': 'ARTIST ID',
'sort-name': 'ARTIST SORT NAME',
},
'name': 'ARTIST CREDIT',
}
],
'date': '3001',
'medium-list': [],
'label-info-list': [{
'catalog-number': 'CATALOG NUMBER',
'label': {'name': 'LABEL NAME'},
}],
'text-representation': {
'script': 'SCRIPT',
'language': 'LANGUAGE',
},
'country': 'COUNTRY',
'status': 'STATUS',
}
i = 0
track_list = []
if tracks:
for recording in tracks:
i += 1
track = {
'id': 'RELEASE TRACK ID %d' % i,
'recording': recording,
'position': i,
'number': 'A1',
}
if track_length:
# Track lengths are distinct from recording lengths.
track['length'] = track_length
if track_artist:
# Similarly, track artists can differ from recording
# artists.
track['artist-credit'] = [
{
'artist': {
'name': 'TRACK ARTIST NAME',
'id': 'TRACK ARTIST ID',
'sort-name': 'TRACK ARTIST SORT NAME',
},
'name': 'TRACK ARTIST CREDIT',
}
]
track_list.append(track)
data_track_list = []
if data_tracks:
for recording in data_tracks:
i += 1
data_track = {
'id': 'RELEASE TRACK ID %d' % i,
'recording': recording,
'position': i,
'number': 'A1',
}
data_track_list.append(data_track)
release['medium-list'].append({
'position': '1',
'track-list': track_list,
'data-track-list': data_track_list,
'format': medium_format,
'title': 'MEDIUM TITLE',
})
return release
def _make_track(self, title, tr_id, duration, artist=False,
video=False, disambiguation=None, remixer=False):
track = {
'title': title,
'id': tr_id,
}
if duration is not None:
track['length'] = duration
if artist:
track['artist-credit'] = [
{
'artist': {
'name': 'RECORDING ARTIST NAME',
'id': 'RECORDING ARTIST ID',
'sort-name': 'RECORDING ARTIST SORT NAME',
},
'name': 'RECORDING ARTIST CREDIT',
}
]
if remixer:
track['artist-relation-list'] = [
{
'type': 'remixer',
'type-id': 'RELATION TYPE ID',
'target': 'RECORDING REMIXER ARTIST ID',
'direction': 'RECORDING RELATION DIRECTION',
'artist':
{
'id': 'RECORDING REMIXER ARTIST ID',
'type': 'RECORDING REMIXER ARTIST TYPE',
'name': 'RECORDING REMIXER ARTIST NAME',
'sort-name': 'RECORDING REMIXER ARTIST SORT NAME'
}
}
]
if video:
track['video'] = 'true'
if disambiguation:
track['disambiguation'] = disambiguation
return track
def test_parse_release_with_year(self):
release = self._make_release('1984')
d = mb.album_info(release)
self.assertEqual(d.album, 'ALBUM TITLE')
self.assertEqual(d.album_id, 'ALBUM ID')
self.assertEqual(d.artist, 'ARTIST NAME')
self.assertEqual(d.artist_id, 'ARTIST ID')
self.assertEqual(d.original_year, 1984)
self.assertEqual(d.year, 3001)
self.assertEqual(d.artist_credit, 'ARTIST CREDIT')
def test_parse_release_type(self):
release = self._make_release('1984')
d = mb.album_info(release)
self.assertEqual(d.albumtype, 'album')
def test_parse_release_full_date(self):
release = self._make_release('1987-03-31')
d = mb.album_info(release)
self.assertEqual(d.original_year, 1987)
self.assertEqual(d.original_month, 3)
self.assertEqual(d.original_day, 31)
def test_parse_tracks(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
release = self._make_release(tracks=tracks)
d = mb.album_info(release)
t = d.tracks
self.assertEqual(len(t), 2)
self.assertEqual(t[0].title, 'TITLE ONE')
self.assertEqual(t[0].track_id, 'ID ONE')
self.assertEqual(t[0].length, 100.0)
self.assertEqual(t[1].title, 'TITLE TWO')
self.assertEqual(t[1].track_id, 'ID TWO')
self.assertEqual(t[1].length, 200.0)
def test_parse_track_indices(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
release = self._make_release(tracks=tracks)
d = mb.album_info(release)
t = d.tracks
self.assertEqual(t[0].medium_index, 1)
self.assertEqual(t[0].index, 1)
self.assertEqual(t[1].medium_index, 2)
self.assertEqual(t[1].index, 2)
def test_parse_medium_numbers_single_medium(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
release = self._make_release(tracks=tracks)
d = mb.album_info(release)
self.assertEqual(d.mediums, 1)
t = d.tracks
self.assertEqual(t[0].medium, 1)
self.assertEqual(t[1].medium, 1)
def test_parse_medium_numbers_two_mediums(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
release = self._make_release(tracks=[tracks[0]])
second_track_list = [{
'id': 'RELEASE TRACK ID 2',
'recording': tracks[1],
'position': '1',
'number': 'A1',
}]
release['medium-list'].append({
'position': '2',
'track-list': second_track_list,
})
d = mb.album_info(release)
self.assertEqual(d.mediums, 2)
t = d.tracks
self.assertEqual(t[0].medium, 1)
self.assertEqual(t[0].medium_index, 1)
self.assertEqual(t[0].index, 1)
self.assertEqual(t[1].medium, 2)
self.assertEqual(t[1].medium_index, 1)
self.assertEqual(t[1].index, 2)
def test_parse_release_year_month_only(self):
release = self._make_release('1987-03')
d = mb.album_info(release)
self.assertEqual(d.original_year, 1987)
self.assertEqual(d.original_month, 3)
def test_no_durations(self):
tracks = [self._make_track('TITLE', 'ID', None)]
release = self._make_release(tracks=tracks)
d = mb.album_info(release)
self.assertEqual(d.tracks[0].length, None)
def test_track_length_overrides_recording_length(self):
tracks = [self._make_track('TITLE', 'ID', 1.0 * 1000.0)]
release = self._make_release(tracks=tracks, track_length=2.0 * 1000.0)
d = mb.album_info(release)
self.assertEqual(d.tracks[0].length, 2.0)
def test_no_release_date(self):
release = self._make_release(None)
d = mb.album_info(release)
self.assertFalse(d.original_year)
self.assertFalse(d.original_month)
self.assertFalse(d.original_day)
def test_various_artists_defaults_false(self):
release = self._make_release(None)
d = mb.album_info(release)
self.assertFalse(d.va)
def test_detect_various_artists(self):
release = self._make_release(None)
release['artist-credit'][0]['artist']['id'] = \
mb.VARIOUS_ARTISTS_ID
d = mb.album_info(release)
self.assertTrue(d.va)
def test_parse_artist_sort_name(self):
release = self._make_release(None)
d = mb.album_info(release)
self.assertEqual(d.artist_sort, 'ARTIST SORT NAME')
def test_parse_releasegroupid(self):
release = self._make_release(None)
d = mb.album_info(release)
self.assertEqual(d.releasegroup_id, 'RELEASE GROUP ID')
def test_parse_asin(self):
release = self._make_release(None)
d = mb.album_info(release)
self.assertEqual(d.asin, 'ALBUM ASIN')
def test_parse_catalognum(self):
release = self._make_release(None)
d = mb.album_info(release)
self.assertEqual(d.catalognum, 'CATALOG NUMBER')
def test_parse_textrepr(self):
release = self._make_release(None)
d = mb.album_info(release)
self.assertEqual(d.script, 'SCRIPT')
self.assertEqual(d.language, 'LANGUAGE')
def test_parse_country(self):
release = self._make_release(None)
d = mb.album_info(release)
self.assertEqual(d.country, 'COUNTRY')
def test_parse_status(self):
release = self._make_release(None)
d = mb.album_info(release)
self.assertEqual(d.albumstatus, 'STATUS')
def test_parse_media(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
release = self._make_release(None, tracks=tracks)
d = mb.album_info(release)
self.assertEqual(d.media, 'FORMAT')
def test_parse_disambig(self):
release = self._make_release(None)
d = mb.album_info(release)
self.assertEqual(d.albumdisambig, 'R_DISAMBIGUATION')
self.assertEqual(d.releasegroupdisambig, 'RG_DISAMBIGUATION')
def test_parse_disctitle(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
release = self._make_release(None, tracks=tracks)
d = mb.album_info(release)
t = d.tracks
self.assertEqual(t[0].disctitle, 'MEDIUM TITLE')
self.assertEqual(t[1].disctitle, 'MEDIUM TITLE')
def test_missing_language(self):
release = self._make_release(None)
del release['text-representation']['language']
d = mb.album_info(release)
self.assertEqual(d.language, None)
def test_parse_recording_artist(self):
tracks = [self._make_track('a', 'b', 1, True)]
release = self._make_release(None, tracks=tracks)
track = mb.album_info(release).tracks[0]
self.assertEqual(track.artist, 'RECORDING ARTIST NAME')
self.assertEqual(track.artist_id, 'RECORDING ARTIST ID')
self.assertEqual(track.artist_sort, 'RECORDING ARTIST SORT NAME')
self.assertEqual(track.artist_credit, 'RECORDING ARTIST CREDIT')
def test_track_artist_overrides_recording_artist(self):
tracks = [self._make_track('a', 'b', 1, True)]
release = self._make_release(None, tracks=tracks, track_artist=True)
track = mb.album_info(release).tracks[0]
self.assertEqual(track.artist, 'TRACK ARTIST NAME')
self.assertEqual(track.artist_id, 'TRACK ARTIST ID')
self.assertEqual(track.artist_sort, 'TRACK ARTIST SORT NAME')
self.assertEqual(track.artist_credit, 'TRACK ARTIST CREDIT')
def test_parse_recording_remixer(self):
tracks = [self._make_track('a', 'b', 1, remixer=True)]
release = self._make_release(None, tracks=tracks)
track = mb.album_info(release).tracks[0]
self.assertEqual(track.remixer, 'RECORDING REMIXER ARTIST NAME')
def test_data_source(self):
release = self._make_release()
d = mb.album_info(release)
self.assertEqual(d.data_source, 'MusicBrainz')
def test_ignored_media(self):
config['match']['ignored_media'] = ['IGNORED1', 'IGNORED2']
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
release = self._make_release(tracks=tracks, medium_format="IGNORED1")
d = mb.album_info(release)
self.assertEqual(len(d.tracks), 0)
def test_no_ignored_media(self):
config['match']['ignored_media'] = ['IGNORED1', 'IGNORED2']
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
release = self._make_release(tracks=tracks,
medium_format="NON-IGNORED")
d = mb.album_info(release)
self.assertEqual(len(d.tracks), 2)
def test_skip_data_track(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('[data track]', 'ID DATA TRACK',
100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
release = self._make_release(tracks=tracks)
d = mb.album_info(release)
self.assertEqual(len(d.tracks), 2)
self.assertEqual(d.tracks[0].title, 'TITLE ONE')
self.assertEqual(d.tracks[1].title, 'TITLE TWO')
def test_skip_audio_data_tracks_by_default(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
data_tracks = [self._make_track('TITLE AUDIO DATA', 'ID DATA TRACK',
100.0 * 1000.0)]
release = self._make_release(tracks=tracks, data_tracks=data_tracks)
d = mb.album_info(release)
self.assertEqual(len(d.tracks), 2)
self.assertEqual(d.tracks[0].title, 'TITLE ONE')
self.assertEqual(d.tracks[1].title, 'TITLE TWO')
def test_no_skip_audio_data_tracks_if_configured(self):
config['match']['ignore_data_tracks'] = False
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
data_tracks = [self._make_track('TITLE AUDIO DATA', 'ID DATA TRACK',
100.0 * 1000.0)]
release = self._make_release(tracks=tracks, data_tracks=data_tracks)
d = mb.album_info(release)
self.assertEqual(len(d.tracks), 3)
self.assertEqual(d.tracks[0].title, 'TITLE ONE')
self.assertEqual(d.tracks[1].title, 'TITLE TWO')
self.assertEqual(d.tracks[2].title, 'TITLE AUDIO DATA')
def test_skip_video_tracks_by_default(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE VIDEO', 'ID VIDEO', 100.0 * 1000.0,
False, True),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
release = self._make_release(tracks=tracks)
d = mb.album_info(release)
self.assertEqual(len(d.tracks), 2)
self.assertEqual(d.tracks[0].title, 'TITLE ONE')
self.assertEqual(d.tracks[1].title, 'TITLE TWO')
def test_skip_video_data_tracks_by_default(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
data_tracks = [self._make_track('TITLE VIDEO', 'ID VIDEO',
100.0 * 1000.0, False, True)]
release = self._make_release(tracks=tracks, data_tracks=data_tracks)
d = mb.album_info(release)
self.assertEqual(len(d.tracks), 2)
self.assertEqual(d.tracks[0].title, 'TITLE ONE')
self.assertEqual(d.tracks[1].title, 'TITLE TWO')
def test_no_skip_video_tracks_if_configured(self):
config['match']['ignore_data_tracks'] = False
config['match']['ignore_video_tracks'] = False
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE VIDEO', 'ID VIDEO', 100.0 * 1000.0,
False, True),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
release = self._make_release(tracks=tracks)
d = mb.album_info(release)
self.assertEqual(len(d.tracks), 3)
self.assertEqual(d.tracks[0].title, 'TITLE ONE')
self.assertEqual(d.tracks[1].title, 'TITLE VIDEO')
self.assertEqual(d.tracks[2].title, 'TITLE TWO')
def test_no_skip_video_data_tracks_if_configured(self):
config['match']['ignore_data_tracks'] = False
config['match']['ignore_video_tracks'] = False
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0)]
data_tracks = [self._make_track('TITLE VIDEO', 'ID VIDEO',
100.0 * 1000.0, False, True)]
release = self._make_release(tracks=tracks, data_tracks=data_tracks)
d = mb.album_info(release)
self.assertEqual(len(d.tracks), 3)
self.assertEqual(d.tracks[0].title, 'TITLE ONE')
self.assertEqual(d.tracks[1].title, 'TITLE TWO')
self.assertEqual(d.tracks[2].title, 'TITLE VIDEO')
def test_track_disambiguation(self):
tracks = [self._make_track('TITLE ONE', 'ID ONE', 100.0 * 1000.0),
self._make_track('TITLE TWO', 'ID TWO', 200.0 * 1000.0,
disambiguation="SECOND TRACK")]
release = self._make_release(tracks=tracks)
d = mb.album_info(release)
t = d.tracks
self.assertEqual(len(t), 2)
self.assertEqual(t[0].trackdisambig, None)
self.assertEqual(t[1].trackdisambig, "SECOND TRACK")
class ParseIDTest(_common.TestCase):
def test_parse_id_correct(self):
id_string = "28e32c71-1450-463e-92bf-e0a46446fc11"
out = mb._parse_id(id_string)
self.assertEqual(out, id_string)
def test_parse_id_non_id_returns_none(self):
id_string = "blah blah"
out = mb._parse_id(id_string)
self.assertEqual(out, None)
def test_parse_id_url_finds_id(self):
id_string = "28e32c71-1450-463e-92bf-e0a46446fc11"
id_url = "https://musicbrainz.org/entity/%s" % id_string
out = mb._parse_id(id_url)
self.assertEqual(out, id_string)
class ArtistFlatteningTest(_common.TestCase):
def _credit_dict(self, suffix=''):
return {
'artist': {
'name': 'NAME' + suffix,
'sort-name': 'SORT' + suffix,
},
'name': 'CREDIT' + suffix,
}
def _add_alias(self, credit_dict, suffix='', locale='', primary=False):
alias = {
'alias': 'ALIAS' + suffix,
'locale': locale,
'sort-name': 'ALIASSORT' + suffix
}
if primary:
alias['primary'] = 'primary'
if 'alias-list' not in credit_dict['artist']:
credit_dict['artist']['alias-list'] = []
credit_dict['artist']['alias-list'].append(alias)
def test_single_artist(self):
a, s, c = mb._flatten_artist_credit([self._credit_dict()])
self.assertEqual(a, 'NAME')
self.assertEqual(s, 'SORT')
self.assertEqual(c, 'CREDIT')
def test_two_artists(self):
a, s, c = mb._flatten_artist_credit(
[self._credit_dict('a'), ' AND ', self._credit_dict('b')]
)
self.assertEqual(a, 'NAMEa AND NAMEb')
self.assertEqual(s, 'SORTa AND SORTb')
self.assertEqual(c, 'CREDITa AND CREDITb')
def test_alias(self):
credit_dict = self._credit_dict()
self._add_alias(credit_dict, suffix='en', locale='en', primary=True)
self._add_alias(credit_dict, suffix='en_GB', locale='en_GB',
primary=True)
self._add_alias(credit_dict, suffix='fr', locale='fr')
self._add_alias(credit_dict, suffix='fr_P', locale='fr', primary=True)
self._add_alias(credit_dict, suffix='pt_BR', locale='pt_BR')
# test no alias
config['import']['languages'] = ['']
flat = mb._flatten_artist_credit([credit_dict])
self.assertEqual(flat, ('NAME', 'SORT', 'CREDIT'))
# test en primary
config['import']['languages'] = ['en']
flat = mb._flatten_artist_credit([credit_dict])
self.assertEqual(flat, ('ALIASen', 'ALIASSORTen', 'CREDIT'))
# test en_GB en primary
config['import']['languages'] = ['en_GB', 'en']
flat = mb._flatten_artist_credit([credit_dict])
self.assertEqual(flat, ('ALIASen_GB', 'ALIASSORTen_GB', 'CREDIT'))
# test en en_GB primary
config['import']['languages'] = ['en', 'en_GB']
flat = mb._flatten_artist_credit([credit_dict])
self.assertEqual(flat, ('ALIASen', 'ALIASSORTen', 'CREDIT'))
# test fr primary
config['import']['languages'] = ['fr']
flat = mb._flatten_artist_credit([credit_dict])
self.assertEqual(flat, ('ALIASfr_P', 'ALIASSORTfr_P', 'CREDIT'))
# test for not matching non-primary
config['import']['languages'] = ['pt_BR', 'fr']
flat = mb._flatten_artist_credit([credit_dict])
self.assertEqual(flat, ('ALIASfr_P', 'ALIASSORTfr_P', 'CREDIT'))
class MBLibraryTest(unittest.TestCase):
def test_match_track(self):
with mock.patch('musicbrainzngs.search_recordings') as p:
p.return_value = {
'recording-list': [{
'title': 'foo',
'id': 'bar',
'length': 42,
}],
}
ti = list(mb.match_track('hello', 'there'))[0]
p.assert_called_with(artist='hello', recording='there', limit=5)
self.assertEqual(ti.title, 'foo')
self.assertEqual(ti.track_id, 'bar')
def test_match_album(self):
mbid = 'd2a6f856-b553-40a0-ac54-a321e8e2da99'
with mock.patch('musicbrainzngs.search_releases') as sp:
sp.return_value = {
'release-list': [{
'id': mbid,
}],
}
with mock.patch('musicbrainzngs.get_release_by_id') as gp:
gp.return_value = {
'release': {
'title': 'hi',
'id': mbid,
'medium-list': [{
'track-list': [{
'id': 'baz',
'recording': {
'title': 'foo',
'id': 'bar',
'length': 42,
},
'position': 9,
'number': 'A1',
}],
'position': 5,
}],
'artist-credit': [{
'artist': {
'name': 'some-artist',
'id': 'some-id',
},
}],
'release-group': {
'id': 'another-id',
}
}
}
ai = list(mb.match_album('hello', 'there'))[0]
sp.assert_called_with(artist='hello', release='there', limit=5)
gp.assert_called_with(mbid, mock.ANY)
self.assertEqual(ai.tracks[0].title, 'foo')
self.assertEqual(ai.album, 'hi')
def test_match_track_empty(self):
with mock.patch('musicbrainzngs.search_recordings') as p:
til = list(mb.match_track(' ', ' '))
self.assertFalse(p.called)
self.assertEqual(til, [])
def test_match_album_empty(self):
with mock.patch('musicbrainzngs.search_releases') as p:
ail = list(mb.match_album(' ', ' '))
self.assertFalse(p.called)
self.assertEqual(ail, [])
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')