diff --git a/.gitignore b/.gitignore index 40e3e1a7e..230d50f2e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ .tox .coverage .idea +.ackrc # file patterns @@ -32,4 +33,6 @@ beets.egg-info/* build/* docs/_build/* man +htmlcov/* +.noseids *.egg diff --git a/test/test_echonest.py b/test/test_echonest.py index 43f006dfc..5480cd62c 100644 --- a/test/test_echonest.py +++ b/test/test_echonest.py @@ -13,9 +13,10 @@ # included in all copies or substantial portions of the Software. +import os.path from mock import Mock, patch -from _common import unittest +from _common import unittest, RSRC from helper import TestHelper from beets.library import Item @@ -30,6 +31,8 @@ class EchonestCliTest(unittest.TestCase, TestHelper): self.setup_beets() self.load_plugins('echonest') + # Prevent 'beet echonest' from writing files + self.config['import']['write'] = False def tearDown(self): self.teardown_beets() @@ -38,49 +41,122 @@ class EchonestCliTest(unittest.TestCase, TestHelper): @patch.object(Item, 'write') @patch('pyechonest.song.profile') @patch('pyechonest.track.track_from_id') - def test_store_data(self, echonest_track, echonest_profile, item_write): - profile = Mock( - artist_name='artist', - title='title', - id='echonestid', - audio_summary={ - 'duration': 10, - 'energy': 0.5, - 'liveness': 0.5, - 'loudness': 0.5, - 'speechiness': 0.5, - 'danceability': 0.5, - 'tempo': 120, - 'key': 2, - 'mode': 0 - }, - ) + def test_mbid_profile(self, echonest_track, echonest_profile, item_write): + """Retrieve song info from MusicBrainz ID.""" + item = self.add_item(title='title', length=10, mb_trackid='1') + profile = self.profile(item, energy=0.5, tempo=120, key=2, mode=0) echonest_profile.return_value = [profile] echonest_track.return_value = Mock(song_id='echonestid') - item = Item( - mb_trackid='01234', - artist='artist', - title='title', - length=10, - ) - item.add(self.lib) - self.assertNotIn('danceability', item) + self.assertNotIn('energy', item) + self.assertNotEqual(item['bpm'], 120) self.assertNotIn('initialkey', item) self.run_command('echonest') item.load() - self.assertEqual(item['danceability'], 0.5) - self.assertEqual(item['liveness'], 0.5) + self.assertEqual(item['energy'], 0.5) self.assertEqual(item['bpm'], 120) self.assertEqual(item['initial_key'], 'C#m') + @patch('pyechonest.track.track_from_id') + @patch('pyechonest.song.search') + def test_song_search(self, echonest_search, echonest_track): + item = self.add_item(title='title', length=10, mb_trackid='1') + echonest_search.return_value = [self.profile(item, energy=0.1)] + echonest_track.return_value = [] + + self.run_command('echonest') + item.load() + self.assertEqual(item['energy'], 0.1) + self.assertEqual(1, echonest_track.call_count) + + @patch('pyechonest.song.profile') + @patch('pyechonest.song.search') + @patch('pyechonest.track.track_from_filename') + def test_analyze(self, echonest_track, echonest_search, echonest_profile): + item = self.add_item(title='title', length=10, + path=os.path.join(RSRC, 'min.mp3')) + echonest_search.return_value = [] + echonest_profile.return_value = [self.profile(item, energy=0.2)] + echonest_track.return_value = self.track(item) + + self.run_command('echonest') + item.load() + self.assertEqual(item['energy'], 0.2) + self.assertEqual(1, echonest_search.call_count) + self.assertEqual(item.path, + echonest_track.call_args[1]['filename']) + + @patch('pyechonest.song.profile') + @patch('pyechonest.song.search') + @patch('pyechonest.track.track_from_filename') + def test_analyze_convert(self, echonest_track, echonest_search, + echonest_profile): + item = self.add_item(title='title', length=10, format='FLAC', + path=os.path.join(RSRC, 'min.flac')) + echonest_search.return_value = [] + echonest_profile.return_value = [self.profile(item, energy=0.2)] + echonest_track.return_value = self.track(item) + + self.run_command('echonest') + item.load() + self.assertEqual(item['energy'], 0.2) + # Assert uploaded file was converted + self.assertNotEqual(item.path, + echonest_track.call_args[1]['filename']) + + @patch('pyechonest.song.profile') + @patch('pyechonest.song.search') + @patch('pyechonest.track.track_from_filename') + # Force truncation + @patch('beetsplug.echonest.UPLOAD_MAX_SIZE', 0) + def test_analyze_truncate(self, echonest_track, echonest_search, + echonest_profile): + item = self.add_item(title='title', length=10, format='MP3', + path=os.path.join(RSRC, 'min.mp3')) + echonest_search.return_value = [] + echonest_profile.return_value = [self.profile(item, energy=0.2)] + echonest_track.return_value = self.track(item) + + self.run_command('echonest') + item.load() + self.assertEqual(item['energy'], 0.2) + self.assertEqual(1, echonest_search.call_count) + self.assertNotEqual(item.path, + echonest_track.call_args[1]['filename']) + def test_custom_field_range_query(self): item = Item(liveness=2.2) item.add(self.lib) item = self.lib.items('liveness:2.2..3').get() self.assertEqual(item['liveness'], 2.2) + def profile(self, item, **values): + """Return a mock Echonest Profile object. + + The fields are set to match the item. Additional values are + passed to the `audio_summary` dictionary of the profile. + """ + audio_summary = {'duration': item.length} + audio_summary.update(values) + return Mock( + artist_name=item.artist, + title=item.title, + id='echonestid', + audio_summary=audio_summary + ) + + def track(self, item, **values): + """Return a mock Echonest Track object. + """ + values.update( + duration=item.length, + artist_name=item.artist, + title=item.title, + song_id='echonestid', + ) + return Mock(**values) + def suite(): return unittest.TestLoader().loadTestsFromName(__name__)