Merge remote branch 'adamcik/feature/musicbrainz-id-support' into develop

This commit is contained in:
Stein Magnus Jodal 2010-11-03 00:08:32 +01:00
commit 2941c83bb8
10 changed files with 341 additions and 60 deletions

View File

@ -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

View File

@ -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 = '''

View File

@ -38,6 +38,31 @@ class ImmutableObject(object):
def __ne__(self, other):
return not self.__eq__(other)
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] = values.pop(public_key, self.__dict__[key])
for key in values.keys():
if hasattr(self, key):
data[key] = values.pop(key)
if values:
raise TypeError("copy() got an unexpected keyword argument '%s'" % key)
return self.__class__(**data)
class Artist(ImmutableObject):
"""
@ -45,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.
@ -53,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):
"""
@ -64,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.
@ -75,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)
@ -103,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.
@ -126,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)
@ -178,31 +218,3 @@ class Playlist(ImmutableObject):
def mpd_format(self, *args, **kwargs):
return translator.playlist_to_mpd_format(self, *args, **kwargs)
def copy(self, uri=None, name=None, tracks=None, last_modified=None):
"""
Create a new playlist object with the given values. The values that are
not given are taken from the object the method is called on.
Does not change the object on which it is called.
:param uri: playlist URI
:type uri: string
:param name: playlist name
:type name: string
:param tracks: playlist's tracks
:type tracks: list of :class:`Track` elements
:param last_modified: playlist's modification time
:type last_modified: :class:`datetime.datetime`
:rtype: :class:`Playlist`
"""
if uri is None:
uri = self.uri
if name is None:
name = self.name
if tracks is None:
tracks = self.tracks
if last_modified is None:
last_modified = self.last_modified
return Playlist(uri=uri, name=name, tracks=tracks,
last_modified=last_modified)

View File

@ -16,6 +16,8 @@ def translator(data):
artist_kwargs = {}
track_kwargs = {}
# FIXME replace with data.get('foo', None) ?
if 'album' in data:
album_kwargs['name'] = data['album']
@ -39,6 +41,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)]

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -5,6 +5,50 @@ from mopidy.models import Artist, Album, Track, Playlist
from tests import SkipTest
class GenericCopyTets(unittest.TestCase):
def compare(self, orig, other):
self.assertEqual(orig, other)
self.assertNotEqual(id(orig), id(other))
def test_copying_track(self):
track = Track()
self.compare(track, track.copy())
def test_copying_artist(self):
artist = Artist()
self.compare(artist, artist.copy())
def test_copying_album(self):
album = Album()
self.compare(album, album.copy())
def test_copying_playlist(self):
playlist = Playlist()
self.compare(playlist, playlist.copy())
def test_copying_track_with_basic_values(self):
track = Track(name='foo', uri='bar')
copy = track.copy(name='baz')
self.assertEqual('baz', copy.name)
self.assertEqual('bar', copy.uri)
def test_copying_track_with_missing_values(self):
track = Track(uri='bar')
copy = track.copy(name='baz')
self.assertEqual('baz', copy.name)
self.assertEqual('bar', copy.uri)
def test_copying_track_with_private_internal_value(self):
artists1 = [Artist(name='foo')]
artists2 = [Artist(name='bar')]
track = Track(artists=artists1)
copy = track.copy(artists=artists2)
self.assertEqual(copy.artists, artists2)
def test_copying_track_with_invalid_key(self):
test = lambda: Track().copy(invalid_key=True)
self.assertRaises(TypeError, test)
class ArtistTest(unittest.TestCase):
def test_uri(self):
uri = u'an_uri'
@ -18,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)
@ -34,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))
@ -58,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))
@ -90,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)
@ -127,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))
@ -164,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))
@ -222,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)
@ -285,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))
@ -350,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))

View File

@ -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']