diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 26c2ad6b..51522ead 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -96,33 +96,54 @@ def _convert_mpd_data(data, tracks, music_dir): track_kwargs = {} album_kwargs = {} + artist_kwargs = {} + albumartist_kwargs = {} if 'track' in data: album_kwargs['num_tracks'] = int(data['track'].split('/')[1]) track_kwargs['track_no'] = int(data['track'].split('/')[0]) if 'artist' in data: - artist = Artist(name=data['artist']) - track_kwargs['artists'] = [artist] - album_kwargs['artists'] = [artist] + artist_kwargs['name'] = data['artist'] + albumartist_kwargs['name'] = data['artist'] + + if 'albumartist' in data: + albumartist_kwargs['name'] = data['albumartist'] - # FIXME Newer mpd tag caches support albumartist names if 'album' in data: album_kwargs['name'] = data['album'] - album = Album(**album_kwargs) - track_kwargs['album'] = album if 'title' in data: track_kwargs['name'] = data['title'] - # FIXME what if file is uri - generated tag cache needs to allways make - # LOCAL_MUSIC_PATH relative paths or this code must handle uris + if 'musicbrainz_trackid' in data: + track_kwargs['musicbrainz_id'] = data['musicbrainz_trackid'] + + if 'musicbrainz_albumid' in data: + album_kwargs['musicbrainz_id'] = data['musicbrainz_albumid'] + + if 'musicbrainz_artistid' in data: + artist_kwargs['musicbrainz_id'] = data['musicbrainz_artistid'] + + if 'musicbrainz_albumartistid' in data: + albumartist_kwargs['musicbrainz_id'] = data['musicbrainz_albumartistid'] + if data['file'][0] == '/': path = data['file'][1:] else: path = data['file'] - # FIXME newer mpd tag caches provide musicbrainz ids + if artist_kwargs: + artist = Artist(**artist_kwargs) + track_kwargs['artists'] = [artist] + + if albumartist_kwargs: + albumartist = Artist(**albumartist_kwargs) + album_kwargs['artists'] = [albumartist] + + if album_kwargs: + album = Album(**album_kwargs) + track_kwargs['album'] = album track_kwargs['uri'] = path_to_uri(music_dir, path) track_kwargs['length'] = int(data.get('time', 0)) * 1000 diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index e15e1ba5..0073c113 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -41,6 +41,20 @@ def track_to_mpd_format(track, position=None, cpid=None): if position is not None and cpid is not None: result.append(('Pos', position)) result.append(('Id', cpid)) + if track.album is not None and track.album.musicbrainz_id is not None: + result.append(('MUSICBRAINZ_ALBUMID', track.album.musicbrainz_id)) + # FIXME don't use first and best artist? + # FIXME don't duplicate following code? + if track.album is not None and track.album.artists: + artists = filter(lambda a: a.musicbrainz_id is not None, track.album.artists) + if artists: + result.append(('MUSICBRAINZ_ALBUMARTISTID', artists[0].musicbrainz_id)) + if track.artists: + artists = filter(lambda a: a.musicbrainz_id is not None, track.artists) + if artists: + result.append(('MUSICBRAINZ_ARTISTID', artists[0].musicbrainz_id)) + if track.musicbrainz_id is not None: + result.append(('MUSICBRAINZ_TRACKID', track.musicbrainz_id)) return result MPD_KEY_ORDER = ''' diff --git a/mopidy/models.py b/mopidy/models.py index 7dd75660..60569004 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -38,15 +38,29 @@ class ImmutableObject(object): def __ne__(self, other): return not self.__eq__(other) - def copy(self, **kwargs): + def copy(self, **values): + """ + Copy the model with ``field`` updated to new value. + + Examples:: + + # Returns a track with a new name + Track(name='foo').copy(name='bar') + # Return an album with a new number of tracks + Album(num_tracks=2).copy(num_tracks=5) + + :param values: the model field to modify + :type values: dict + :rtype: new instance of the model being copied + """ data = {} for key in self.__dict__.keys(): public_key = key.lstrip('_') - data[public_key] = kwargs.pop(public_key, self.__dict__[key]) - for key in kwargs.keys(): + data[public_key] = values.pop(public_key, self.__dict__[key]) + for key in values.keys(): if hasattr(self, key): - data[key] = kwargs.pop(key) - if kwargs: + data[key] = values.pop(key) + if values: raise TypeError("copy() got an unexpected keyword argument '%s'" % key) return self.__class__(**data) @@ -56,6 +70,8 @@ class Artist(ImmutableObject): :type uri: string :param name: artist name :type name: string + :param musicbrainz_id: MusicBrainz ID + :type musicbrainz_id: string """ #: The artist URI. Read-only. @@ -64,6 +80,9 @@ class Artist(ImmutableObject): #: The artist name. Read-only. name = None + #: The MusicBrainz ID of the artist. Read-only. + musicbrainz_id = None + class Album(ImmutableObject): """ @@ -75,6 +94,8 @@ class Album(ImmutableObject): :type artists: list of :class:`Artist` :param num_tracks: number of tracks in album :type num_tracks: integer + :param musicbrainz_id: MusicBrainz ID + :type musicbrainz_id: string """ #: The album URI. Read-only. @@ -86,6 +107,9 @@ class Album(ImmutableObject): #: The number of tracks in the album. Read-only. num_tracks = 0 + #: The MusicBrainz ID of the album. Read-only. + musicbrainz_id = None + def __init__(self, *args, **kwargs): self._artists = frozenset(kwargs.pop('artists', [])) super(Album, self).__init__(*args, **kwargs) @@ -114,6 +138,8 @@ class Track(ImmutableObject): :type length: integer :param bitrate: bitrate in kbit/s :type bitrate: integer + :param musicbrainz_id: MusicBrainz ID + :type musicbrainz_id: string """ #: The track URI. Read-only. @@ -137,6 +163,9 @@ class Track(ImmutableObject): #: The track's bitrate in kbit/s. Read-only. bitrate = None + #: The MusicBrainz ID of the track. Read-only. + musicbrainz_id = None + def __init__(self, *args, **kwargs): self._artists = frozenset(kwargs.pop('artists', [])) super(Track, self).__init__(*args, **kwargs) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 4ccccbdb..a7d2e664 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -18,6 +18,8 @@ def translator(data): artist_kwargs = {} track_kwargs = {} + # FIXME replace with data.get('foo', None) ? + if 'album' in data: album_kwargs['name'] = data['album'] @@ -41,6 +43,18 @@ def translator(data): if 'album-artist' in data: albumartist_kwargs['name'] = data['album-artist'] + if 'musicbrainz-trackid' in data: + track_kwargs['musicbrainz_id'] = data['musicbrainz-trackid'] + + if 'musicbrainz-artistid' in data: + artist_kwargs['musicbrainz_id'] = data['musicbrainz-artistid'] + + if 'musicbrainz-albumid' in data: + album_kwargs['musicbrainz_id'] = data['musicbrainz-albumid'] + + if 'musicbrainz-albumartistid' in data: + albumartist_kwargs['musicbrainz_id'] = data['musicbrainz-albumartistid'] + if albumartist_kwargs: album_kwargs['artists'] = [Artist(**albumartist_kwargs)] diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index 2f97e45c..b7fd212c 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -136,3 +136,28 @@ class MPDTagCacheToTracksTest(unittest.TestCase): data_folder('')) uri = path_to_uri(data_folder('song1.mp3')) self.assertEqual(set([Track(uri=uri, length=4000)]), tracks) + + def test_musicbrainz_tagcache(self): + tracks = parse_mpd_tag_cache(data_folder('musicbrainz_tag_cache'), + data_folder('')) + artist = list(expected_tracks[0].artists)[0].copy( + musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897') + albumartist = list(expected_tracks[0].artists)[0].copy( + name='albumartistname', + musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897') + album = expected_tracks[0].album.copy(artists=[albumartist], + musicbrainz_id='cb5f1603-d314-4c9c-91e5-e295cfb125d2') + track = expected_tracks[0].copy(artists=[artist], album=album, + musicbrainz_id='90488461-8c1f-4a4e-826b-4c6dc70801f0') + + self.assertEqual(track, list(tracks)[0]) + + def test_albumartist_tag_cache(self): + tracks = parse_mpd_tag_cache(data_folder('albumartist_tag_cache'), + data_folder('')) + uri = path_to_uri(data_folder('song1.mp3')) + artist = Artist(name='albumartistname') + album = expected_albums[0].copy(artists=[artist]) + track = Track(name='trackname', artists=expected_artists, track_no=1, + album=album, length=4000, uri=uri) + self.assertEqual(track, list(tracks)[0]) diff --git a/tests/data/albumartist_tag_cache b/tests/data/albumartist_tag_cache new file mode 100644 index 00000000..29942a75 --- /dev/null +++ b/tests/data/albumartist_tag_cache @@ -0,0 +1,16 @@ +info_begin +mpd_version: 0.14.2 +fs_charset: UTF-8 +info_end +songList begin +key: song1.mp3 +file: /song1.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +AlbumArtist: albumartistname +Track: 1/2 +Date: 2006 +mtime: 1272319626 +songList end diff --git a/tests/data/musicbrainz_tag_cache b/tests/data/musicbrainz_tag_cache new file mode 100644 index 00000000..0e9dca46 --- /dev/null +++ b/tests/data/musicbrainz_tag_cache @@ -0,0 +1,20 @@ +info_begin +mpd_version: 0.16.0 +fs_charset: UTF-8 +info_end +songList begin +key: song1.mp3 +file: /song1.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +AlbumArtist: albumartistname +Track: 1/2 +Date: 2006 +MUSICBRAINZ_ALBUMID: cb5f1603-d314-4c9c-91e5-e295cfb125d2 +MUSICBRAINZ_ALBUMARTISTID: 7364dea6-ca9a-48e3-be01-b44ad0d19897 +MUSICBRAINZ_ARTISTID: 7364dea6-ca9a-48e3-be01-b44ad0d19897 +MUSICBRAINZ_TRACKID: 90488461-8c1f-4a4e-826b-4c6dc70801f0 +mtime: 1272319626 +songList end diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 77a25e15..7e4500ea 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -10,6 +10,17 @@ from mopidy.models import Album, Artist, Playlist, Track from tests import data_folder, SkipTest class TrackMpdFormatTest(unittest.TestCase): + track = Track( + uri=u'a uri', + artists=[Artist(name=u'an artist')], + name=u'a name', + album=Album(name=u'an album', num_tracks=13, + artists=[Artist(name=u'an other artist')]), + track_no=7, + date=dt.date(1977, 1, 1), + length=137000, + ) + def setUp(self): settings.LOCAL_MUSIC_PATH = '/dir/subdir' mtime.set_fake_time(1234567) @@ -43,17 +54,7 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Id', 2) in result) def test_track_to_mpd_format_for_nonempty_track(self): - track = Track( - uri=u'a uri', - artists=[Artist(name=u'an artist')], - name=u'a name', - album=Album(name=u'an album', num_tracks=13, - artists=[Artist(name=u'an other artist')]), - track_no=7, - date=dt.date(1977, 1, 1), - length=137000, - ) - result = translator.track_to_mpd_format(track, position=9, cpid=122) + result = translator.track_to_mpd_format(self.track, position=9, cpid=122) self.assert_(('file', 'a uri') in result) self.assert_(('Time', 137) in result) self.assert_(('Artist', 'an artist') in result) @@ -66,6 +67,30 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Id', 122) in result) self.assertEqual(len(result), 10) + def test_track_to_mpd_format_musicbrainz_trackid(self): + track = self.track.copy(musicbrainz_id='foo') + result = translator.track_to_mpd_format(track) + self.assert_(('MUSICBRAINZ_TRACKID', 'foo') in result) + + def test_track_to_mpd_format_musicbrainz_albumid(self): + album = self.track.album.copy(musicbrainz_id='foo') + track = self.track.copy(album=album) + result = translator.track_to_mpd_format(track) + self.assert_(('MUSICBRAINZ_ALBUMID', 'foo') in result) + + def test_track_to_mpd_format_musicbrainz_albumid(self): + artist = list(self.track.artists)[0].copy(musicbrainz_id='foo') + album = self.track.album.copy(artists=[artist]) + track = self.track.copy(album=album) + result = translator.track_to_mpd_format(track) + self.assert_(('MUSICBRAINZ_ALBUMARTISTID', 'foo') in result) + + def test_track_to_mpd_format_musicbrainz_artistid(self): + artist = list(self.track.artists)[0].copy(musicbrainz_id='foo') + track = self.track.copy(artists=[artist]) + result = translator.track_to_mpd_format(track) + self.assert_(('MUSICBRAINZ_ARTISTID', 'foo') in result) + def test_artists_to_mpd_format(self): artists = [Artist(name=u'ABBA'), Artist(name=u'Beatles')] translated = translator.artists_to_mpd_format(artists) diff --git a/tests/models_test.py b/tests/models_test.py index 2c1dfec7..0b44f337 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -62,6 +62,13 @@ class ArtistTest(unittest.TestCase): self.assertEqual(artist.name, name) self.assertRaises(AttributeError, setattr, artist, 'name', None) + def test_musicbrainz_id(self): + mb_id = u'mb-id' + artist = Artist(musicbrainz_id=mb_id) + self.assertEqual(artist.musicbrainz_id, mb_id) + self.assertRaises(AttributeError, setattr, artist, + 'musicbrainz_id', None) + def test_invalid_kwarg(self): test = lambda: Artist(foo='baz') self.assertRaises(TypeError, test) @@ -78,9 +85,15 @@ class ArtistTest(unittest.TestCase): self.assertEqual(artist1, artist2) self.assertEqual(hash(artist1), hash(artist2)) + def test_eq_musibrainz_id(self): + artist1 = Artist(musicbrainz_id=u'id') + artist2 = Artist(musicbrainz_id=u'id') + self.assertEqual(artist1, artist2) + self.assertEqual(hash(artist1), hash(artist2)) + def test_eq(self): - artist1 = Artist(uri=u'uri', name=u'name') - artist2 = Artist(uri=u'uri', name=u'name') + artist1 = Artist(uri=u'uri', name=u'name', musicbrainz_id='id') + artist2 = Artist(uri=u'uri', name=u'name', musicbrainz_id='id') self.assertEqual(artist1, artist2) self.assertEqual(hash(artist1), hash(artist2)) @@ -102,9 +115,15 @@ class ArtistTest(unittest.TestCase): self.assertNotEqual(artist1, artist2) self.assertNotEqual(hash(artist1), hash(artist2)) + def test_ne_musicbrainz_id(self): + artist1 = Artist(musicbrainz_id=u'id1') + artist2 = Artist(musicbrainz_id=u'id2') + self.assertNotEqual(artist1, artist2) + self.assertNotEqual(hash(artist1), hash(artist2)) + def test_ne(self): - artist1 = Artist(uri=u'uri1', name=u'name1') - artist2 = Artist(uri=u'uri2', name=u'name2') + artist1 = Artist(uri=u'uri1', name=u'name1', musicbrainz_id='id1') + artist2 = Artist(uri=u'uri2', name=u'name2', musicbrainz_id='id2') self.assertNotEqual(artist1, artist2) self.assertNotEqual(hash(artist1), hash(artist2)) @@ -134,6 +153,13 @@ class AlbumTest(unittest.TestCase): self.assertEqual(album.num_tracks, num_tracks) self.assertRaises(AttributeError, setattr, album, 'num_tracks', None) + def test_musicbrainz_id(self): + mb_id = u'mb-id' + album = Album(musicbrainz_id=mb_id) + self.assertEqual(album.musicbrainz_id, mb_id) + self.assertRaises(AttributeError, setattr, album, + 'musicbrainz_id', None) + def test_invalid_kwarg(self): test = lambda: Album(foo='baz') self.assertRaises(TypeError, test) @@ -171,10 +197,16 @@ class AlbumTest(unittest.TestCase): self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) + def test_eq_musibrainz_id(self): + album1 = Album(musicbrainz_id=u'id') + album2 = Album(musicbrainz_id=u'id') + self.assertEqual(album1, album2) + self.assertEqual(hash(album1), hash(album2)) + def test_eq(self): artists = [Artist()] - album1 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2) - album2 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2) + album1 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2, musicbrainz_id='id') + album2 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2, musicbrainz_id='id') self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) @@ -208,11 +240,19 @@ class AlbumTest(unittest.TestCase): self.assertNotEqual(album1, album2) self.assertNotEqual(hash(album1), hash(album2)) + def test_ne_musicbrainz_id(self): + album1 = Album(musicbrainz_id=u'id1') + album2 = Album(musicbrainz_id=u'id2') + self.assertNotEqual(album1, album2) + self.assertNotEqual(hash(album1), hash(album2)) + def test_ne(self): album1 = Album(name=u'name1', uri=u'uri1', - artists=[Artist(name=u'name1')], num_tracks=1) + artists=[Artist(name=u'name1')], num_tracks=1, + musicbrainz_id='id1') album2 = Album(name=u'name2', uri=u'uri2', - artists=[Artist(name=u'name2')], num_tracks=2) + artists=[Artist(name=u'name2')], num_tracks=2, + musicbrainz_id='id2') self.assertNotEqual(album1, album2) self.assertNotEqual(hash(album1), hash(album2)) @@ -266,6 +306,13 @@ class TrackTest(unittest.TestCase): self.assertEqual(track.bitrate, bitrate) self.assertRaises(AttributeError, setattr, track, 'bitrate', None) + def test_musicbrainz_id(self): + mb_id = u'mb-id' + track = Track(musicbrainz_id=mb_id) + self.assertEqual(track.musicbrainz_id, mb_id) + self.assertRaises(AttributeError, setattr, track, + 'musicbrainz_id', None) + def test_invalid_kwarg(self): test = lambda: Track(foo='baz') self.assertRaises(TypeError, test) @@ -329,14 +376,22 @@ class TrackTest(unittest.TestCase): self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) + def test_eq_musibrainz_id(self): + track1 = Track(musicbrainz_id=u'id') + track2 = Track(musicbrainz_id=u'id') + self.assertEqual(track1, track2) + self.assertEqual(hash(track1), hash(track2)) + def test_eq(self): date = dt.date.today() artists = [Artist()] album = Album() track1 = Track(uri=u'uri', name=u'name', artists=artists, album=album, - track_no=1, date=date, length=100, bitrate=100) + track_no=1, date=date, length=100, bitrate=100, + musicbrainz_id='id') track2 = Track(uri=u'uri', name=u'name', artists=artists, album=album, - track_no=1, date=date, length=100, bitrate=100) + track_no=1, date=date, length=100, bitrate=100, + musicbrainz_id='id') self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) @@ -394,14 +449,21 @@ class TrackTest(unittest.TestCase): self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) + def test_ne_musicbrainz_id(self): + track1 = Track(musicbrainz_id=u'id1') + track2 = Track(musicbrainz_id=u'id2') + self.assertNotEqual(track1, track2) + self.assertNotEqual(hash(track1), hash(track2)) + def test_ne(self): track1 = Track(uri=u'uri1', name=u'name1', artists=[Artist(name=u'name1')], album=Album(name=u'name1'), - track_no=1, date=dt.date.today(), length=100, bitrate=100) + track_no=1, date=dt.date.today(), length=100, bitrate=100, + musicbrainz_id='id1') track2 = Track(uri=u'uri2', name=u'name2', artists=[Artist(name=u'name2')], album=Album(name=u'name2'), track_no=2, date=dt.date.today()-dt.timedelta(days=1), - length=200, bitrate=200) + length=200, bitrate=200, musicbrainz_id='id2') self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 141f2ceb..a1b53bcf 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -25,19 +25,26 @@ class TranslatorTest(unittest.TestCase): 'date': FakeGstDate(2006, 1, 1,), 'container-format': u'ID3 tag', 'duration': 4531, + 'musicbrainz-trackid': 'mbtrackid', + 'musicbrainz-albumid': 'mbalbumid', + 'musicbrainz-artistid': 'mbartistid', + 'musicbrainz-albumartistid': 'mbalbumartistid', } self.album = { 'name': 'albumname', 'num_tracks': 2, + 'musicbrainz_id': 'mbalbumid', } self.artist = { 'name': 'name', + 'musicbrainz_id': 'mbartistid', } self.albumartist = { 'name': 'albumartistname', + 'musicbrainz_id': 'mbalbumartistid', } self.track = { @@ -46,6 +53,7 @@ class TranslatorTest(unittest.TestCase): 'date': date(2006, 1, 1), 'track_no': 1, 'length': 4531, + 'musicbrainz_id': 'mbtrackid', } def build_track(self): @@ -78,21 +86,41 @@ class TranslatorTest(unittest.TestCase): del self.track['name'] self.check() + def test_missing_track_musicbrainz_id(self): + del self.data['musicbrainz-trackid'] + del self.track['musicbrainz_id'] + self.check() + def test_missing_album_name(self): del self.data['album'] del self.album['name'] self.check() + def test_missing_album_musicbrainz_id(self): + del self.data['musicbrainz-albumid'] + del self.album['musicbrainz_id'] + self.check() + def test_missing_artist_name(self): del self.data['artist'] del self.artist['name'] self.check() + def test_missing_artist_musicbrainz_id(self): + del self.data['musicbrainz-artistid'] + del self.artist['musicbrainz_id'] + self.check() + def test_missing_album_artist(self): del self.data['album-artist'] del self.albumartist['name'] self.check() + def test_missing_album_artist_musicbrainz_id(self): + del self.data['musicbrainz-albumartistid'] + del self.albumartist['musicbrainz_id'] + self.check() + def test_missing_date(self): del self.data['date'] del self.track['date']