merge master branch

This commit is contained in:
Johannes Knutsen 2010-08-09 09:49:07 +02:00
commit 2897f8e5f6
12 changed files with 84 additions and 167 deletions

View File

@ -15,6 +15,10 @@ Another great release.
- Exit early if not Python >= 2.6, < 3.
- Include Sphinx scripts for building docs, pylintrc, tests and test data in
the packages created by ``setup.py`` for i.e. PyPI.
- Backend API:
- The ``id`` field of :class:`mopidy.models.Track` has been removed, as it is
no longer needed after the CPID refactoring.
0.1.0a3 (2010-08-03)

View File

@ -9,7 +9,6 @@ from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController,
BaseLibraryController, BasePlaybackController,
BaseStoredPlaylistsController)
from mopidy.models import Artist, Album, Track, Playlist
from mopidy.utils import spotify_uri_to_int
logger = logging.getLogger('mopidy.backends.despotify')
@ -128,10 +127,6 @@ class DespotifyStoredPlaylistsController(BaseStoredPlaylistsController):
class DespotifyTranslator(object):
@classmethod
def to_mopidy_id(cls, spotify_uri):
return spotify_uri_to_int(spotify_uri)
@classmethod
def to_mopidy_artist(cls, spotify_artist):
return Artist(
@ -160,7 +155,6 @@ class DespotifyTranslator(object):
date=date,
length=spotify_track.length,
bitrate=320,
id=cls.to_mopidy_id(spotify_track.get_uri()),
)
@classmethod

View File

@ -13,7 +13,6 @@ from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController,
BaseLibraryController, BasePlaybackController,
BaseStoredPlaylistsController)
from mopidy.models import Artist, Album, Track, Playlist
from mopidy.utils import spotify_uri_to_int
import alsaaudio
@ -39,8 +38,7 @@ class LibspotifyBackend(BaseBackend):
def __init__(self, *args, **kwargs):
super(LibspotifyBackend, self).__init__(*args, **kwargs)
self.current_playlist = LibspotifyCurrentPlaylistController(
backend=self)
self.current_playlist = BaseCurrentPlaylistController(backend=self)
self.library = LibspotifyLibraryController(backend=self)
self.playback = LibspotifyPlaybackController(backend=self)
self.stored_playlists = LibspotifyStoredPlaylistsController(
@ -60,15 +58,17 @@ class LibspotifyBackend(BaseBackend):
return spotify
class LibspotifyCurrentPlaylistController(BaseCurrentPlaylistController):
pass
class LibspotifyLibraryController(BaseLibraryController):
def find_exact(self, **query):
return self.search(**query)
def lookup(self, uri):
spotify_track = Link.from_string(uri).as_track()
return LibspotifyTranslator.to_mopidy_track(spotify_track)
def refresh(self, uri=None):
pass # TODO
def search(self, **query):
spotify_query = []
for (field, values) in query.iteritems():
@ -85,14 +85,6 @@ class LibspotifyLibraryController(BaseLibraryController):
logger.debug(u'In search method, search for: %s' % spotify_query)
my_end, other_end = multiprocessing.Pipe()
self.backend.spotify.search(spotify_query.encode(ENCODING), other_end)
my_end.poll(None)
logger.debug(u'In search method, receiving search results')
playlist = my_end.recv()
logger.debug(u'In search method, done receiving search results')
logger.debug(['%s' % t.name for t in playlist.tracks])
return playlist
find_exact = search
class LibspotifyPlaybackController(BasePlaybackController):
@ -118,20 +110,38 @@ class LibspotifyPlaybackController(BasePlaybackController):
# TODO
return False
def _seek(self, time_position):
pass # TODO
def _stop(self):
self.backend.spotify.session.play(0)
return True
class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController):
pass
def create(self, name):
pass # TODO
def delete(self, playlist):
pass # TODO
def lookup(self, uri):
pass # TODO
def refresh(self):
pass # TODO
def rename(self, playlist, new_name):
pass # TODO
def save(self, playlist):
pass # TODO
def search(self, query):
pass # TODO
class LibspotifyTranslator(object):
@classmethod
def to_mopidy_id(cls, spotify_uri):
return spotify_uri_to_int(spotify_uri)
@classmethod
def to_mopidy_artist(cls, spotify_artist):
if not spotify_artist.is_loaded():
@ -166,7 +176,6 @@ class LibspotifyTranslator(object):
date=date,
length=spotify_track.duration(),
bitrate=320,
id=cls.to_mopidy_id(uri),
)
@classmethod
@ -253,15 +262,25 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread):
def search(self, query, connection):
"""Search method used by Mopidy backend"""
self.connected.wait()
def callback(results, userdata):
logger.debug(u'In search callback, translating search results')
logger.debug(u'In SessionManager.search().callback(), '
'translating search results')
logger.debug(results.tracks())
# TODO Include results from results.albums(), etc. too
playlist = Playlist(tracks=[
LibspotifyTranslator.to_mopidy_track(t)
for t in results.tracks()])
logger.debug(u'In search callback, sending search results')
logger.debug(u'In SessionManager.search().callback(), '
'sending search results')
logger.debug(['%s' % t.name for t in playlist.tracks])
connection.send(playlist)
logger.debug(u'In SessionManager.search().callback(), '
'done sending search results')
logger.debug(u'In SessionManager.search(), '
'waiting for Spotify connection')
self.connected.wait()
logger.debug(u'In SessionManager.search(), '
'sending search query')
self.session.search(query, callback)
logger.debug(u'In SessionManager.search(), '
'done sending search query')

View File

@ -103,8 +103,6 @@ class Track(ImmutableObject):
:type length: integer
:param bitrate: bitrate in kbit/s
:type bitrate: integer
:param id: track ID (unique and non-changing as long as the process lives)
:type id: integer
"""
#: The track URI. Read-only.
@ -128,9 +126,6 @@ class Track(ImmutableObject):
#: The track's bitrate in kbit/s. Read-only.
bitrate = None
#: The track ID. Read-only.
id = None
def __init__(self, *args, **kwargs):
self._artists = frozenset(kwargs.pop('artists', []))
super(Track, self).__init__(*args, **kwargs)

View File

@ -58,52 +58,6 @@ def unpickle_connection(pickled_connection):
(func, args) = pickle.loads(pickled_connection)
return func(*args)
def spotify_uri_to_int(uri, output_bits=31):
"""
Stable one-way translation from Spotify URI to 31-bit integer.
Spotify track URIs has 62^22 possible values, which requires 131 bits of
storage. The original MPD server uses 32-bit unsigned integers for track
IDs. GMPC seems to think the track ID is a signed integer, thus we use 31
output bits.
In other words, this function throws away 100 bits of information. Since we
only use the track IDs to identify a track within a single Mopidy instance,
this information loss is acceptable. The chances of getting two different
tracks with the same track ID loaded in the same Mopidy instance is still
rather slim. 1 to 2,147,483,648 to be exact.
Normal usage, with data loss::
>>> spotify_uri_to_int('spotify:track:5KRRcT67VNIZUygEbMoIC1')
624351954
No data loss, may be converted back into a Spotify URI::
>>> spotify_uri_to_int('spotify:track:5KRRcT67VNIZUygEbMoIC1',
... output_bits=131)
101411513484007705241035418492696638725L
:param uri: Spotify URI on the format ``spotify:track:*``
:type uri: string
:param output_bits: number of bits of information kept in the return value
:type output_bits: int
:rtype: int
"""
CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
BITS_PER_CHAR = 6 # int(math.ceil(math.log(len(CHARS), 2)))
key = uri.split(':')[-1]
full_id = 0
for i, char in enumerate(key):
full_id ^= CHARS.index(char) << BITS_PER_CHAR * i
compressed_id = 0
while full_id != 0:
compressed_id ^= (full_id & (2 ** output_bits - 1))
full_id >>= output_bits
return int(compressed_id)
def parse_m3u(file_path):
"""
Convert M3U file list of uris
@ -219,7 +173,6 @@ def _convert_mpd_data(data, tracks, music_dir):
track_kwargs['uri'] = path_to_uri(music_dir, path)
track_kwargs['length'] = int(data.get('time', 0)) * 1000
track_kwargs['id'] = len(tracks)
track = Track(**track_kwargs)
tracks.add(track)

View File

@ -65,22 +65,13 @@ class BaseCurrentPlaylistControllerTest(object):
cp_track = self.controller.cp_tracks[1]
self.assertEqual(cp_track, self.controller.get(cpid=cp_track[0]))
@populate_playlist
def test_get_by_id(self):
cp_track = self.controller.cp_tracks[1]
self.assertEqual(cp_track, self.controller.get(id=cp_track[1].id))
@populate_playlist
def test_get_by_id_raises_error_for_invalid_id(self):
self.assertRaises(LookupError, lambda: self.controller.get(id=1337))
@populate_playlist
def test_get_by_uri(self):
cp_track = self.controller.cp_tracks[1]
self.assertEqual(cp_track, self.controller.get(uri=cp_track[1].uri))
@populate_playlist
def test_get_by_uri_raises_error_for_invalid_id(self):
def test_get_by_uri_raises_error_for_invalid_uri(self):
test = lambda: self.controller.get(uri='foobar')
self.assertRaises(LookupError, test)
@ -106,28 +97,6 @@ class BaseCurrentPlaylistControllerTest(object):
self.controller.load(tracks)
self.assertEqual(tracks, self.controller.tracks)
def test_get_by_id_returns_unique_match(self):
track = Track(id=1)
self.controller.load([Track(id=13), track, Track(id=17)])
self.assertEqual(track, self.controller.get(id=1)[1])
def test_get_by_id_raises_error_if_multiple_matches(self):
track = Track(id=1)
self.controller.load([Track(id=13), track, track])
try:
self.controller.get(id=1)
self.fail(u'Should raise LookupError if multiple matches')
except LookupError as e:
self.assertEqual(u'"id=1" match multiple tracks', e[0])
def test_get_by_id_raises_error_if_no_match(self):
self.controller.playlist = Playlist(tracks=[Track(id=13), Track(id=17)])
try:
self.controller.get(id=1)
self.fail(u'Should raise LookupError if no match')
except LookupError as e:
self.assertEqual(u'"id=1" match no tracks', e[0])
def test_get_by_uri_returns_unique_match(self):
track = Track(uri='a')
self.controller.load([Track(uri='z'), track, Track(uri='y')])
@ -152,20 +121,20 @@ class BaseCurrentPlaylistControllerTest(object):
self.assertEqual(u'"uri=a" match no tracks', e[0])
def test_get_by_multiple_criteria_returns_elements_matching_all(self):
track1 = Track(id=1, uri='a')
track2 = Track(id=1, uri='b')
track3 = Track(id=2, uri='b')
track1 = Track(uri='a', name='x')
track2 = Track(uri='b', name='x')
track3 = Track(uri='b', name='y')
self.controller.load([track1, track2, track3])
self.assertEqual(track1, self.controller.get(id=1, uri='a')[1])
self.assertEqual(track2, self.controller.get(id=1, uri='b')[1])
self.assertEqual(track3, self.controller.get(id=2, uri='b')[1])
self.assertEqual(track1, self.controller.get(uri='a', name='x')[1])
self.assertEqual(track2, self.controller.get(uri='b', name='x')[1])
self.assertEqual(track3, self.controller.get(uri='b', name='y')[1])
def test_get_by_criteria_that_is_not_present_in_all_elements(self):
track1 = Track(id=1)
track1 = Track()
track2 = Track(uri='b')
track3 = Track(id=2)
track3 = Track()
self.controller.load([track1, track2, track3])
self.assertEqual(track1, self.controller.get(id=1)[1])
self.assertEqual(track2, self.controller.get(uri='b')[1])
@populate_playlist
def test_load_replaces_playlist(self):
@ -244,18 +213,18 @@ class BaseCurrentPlaylistControllerTest(object):
track1 = self.controller.tracks[1]
track2 = self.controller.tracks[2]
version = self.controller.version
self.controller.remove(id=track1.id)
self.controller.remove(uri=track1.uri)
self.assert_(version < self.controller.version)
self.assert_(track1 not in self.controller.tracks)
self.assertEqual(track2, self.controller.tracks[1])
@populate_playlist
def test_removing_track_that_does_not_exist(self):
test = lambda: self.controller.remove(id=12345)
test = lambda: self.controller.remove(uri='/nonexistant')
self.assertRaises(LookupError, test)
def test_removing_from_empty_playlist(self):
test = lambda: self.controller.remove(id=12345)
test = lambda: self.controller.remove(uri='/nonexistant')
self.assertRaises(LookupError, test)
@populate_playlist
@ -1025,7 +994,7 @@ class BaseStoredPlaylistsControllerTest(object):
except LookupError as e:
self.assertEqual(u'"name=b" match multiple playlists', e[0])
def test_get_by_id_raises_keyerror_if_no_match(self):
def test_get_by_name_raises_keyerror_if_no_match(self):
self.stored.playlists = [Playlist(name='a'), Playlist(name='b')]
try:
self.stored.get(name='c')
@ -1080,10 +1049,10 @@ class BaseLibraryControllerTest(object):
Album(name='album2', artists=artists[1:2]),
Album()]
tracks = [Track(name='track1', length=4000, artists=artists[:1],
album=albums[0], uri='file://' + data_folder('uri1'), id=0),
album=albums[0], uri='file://' + data_folder('uri1')),
Track(name='track2', length=4000, artists=artists[1:2],
album=albums[1], uri='file://' + data_folder('uri2'), id=1),
Track(id=3)]
album=albums[1], uri='file://' + data_folder('uri2')),
Track()]
def setUp(self):
self.backend = self.backend_class(mixer=DummyMixer())

View File

@ -15,13 +15,13 @@ uris = [
class DespotifyCurrentPlaylistControllerTest(
BaseCurrentPlaylistControllerTest, unittest.TestCase):
tracks = [Track(uri=uri, id=i, length=4464) for i, uri in enumerate(uris)]
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
backend_class = DespotifyBackend
class DespotifyPlaybackControllerTest(
BasePlaybackControllerTest, unittest.TestCase):
tracks = [Track(uri=uri, id=i, length=4464) for i, uri in enumerate(uris)]
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
backend_class = DespotifyBackend

View File

@ -23,7 +23,7 @@ generate_song = lambda i: path_to_uri(song % i)
# FIXME can be switched to generic test
class GStreamerCurrentPlaylistControllerTest(BaseCurrentPlaylistControllerTest,
unittest.TestCase):
tracks = [Track(uri=generate_song(i), id=i, length=4464)
tracks = [Track(uri=generate_song(i), length=4464)
for i in range(1, 4)]
backend_class = GStreamerBackend
@ -31,7 +31,7 @@ class GStreamerCurrentPlaylistControllerTest(BaseCurrentPlaylistControllerTest,
class GStreamerPlaybackControllerTest(BasePlaybackControllerTest,
unittest.TestCase):
tracks = [Track(uri=generate_song(i), id=i, length=4464)
tracks = [Track(uri=generate_song(i), length=4464)
for i in range(1, 4)]
backend_class = GStreamerBackend
@ -42,7 +42,7 @@ class GStreamerPlaybackControllerTest(BasePlaybackControllerTest,
def add_track(self, path):
uri = path_to_uri(data_folder(path))
track = Track(uri=uri, id=1, length=4464)
track = Track(uri=uri, length=4464)
self.backend.current_playlist.add(track)
def test_uri_handler(self):

View File

@ -15,13 +15,13 @@ uris = [
class LibspotifyCurrentPlaylistControllerTest(
BaseCurrentPlaylistControllerTest, unittest.TestCase):
tracks = [Track(uri=uri, id=i, length=4464) for i, uri in enumerate(uris)]
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
backend_class = LibspotifyBackend
class LibspotifyPlaybackControllerTest(
BasePlaybackControllerTest, unittest.TestCase):
tracks = [Track(uri=uri, id=i, length=4464) for i, uri in enumerate(uris)]
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
backend_class = LibspotifyBackend

View File

@ -222,12 +222,6 @@ class TrackTest(unittest.TestCase):
self.assertEqual(track.bitrate, bitrate)
self.assertRaises(AttributeError, setattr, track, 'bitrate', None)
def test_id(self):
track_id = 17
track = Track(id=track_id)
self.assertEqual(track.id, track_id)
self.assertRaises(AttributeError, setattr, track, 'id', None)
def test_invalid_kwarg(self):
test = lambda: Track(foo='baz')
self.assertRaises(TypeError, test)
@ -291,20 +285,14 @@ class TrackTest(unittest.TestCase):
self.assertEqual(track1, track2)
self.assertEqual(hash(track1), hash(track2))
def test_eq_id(self):
track1 = Track(id=100)
track2 = Track(id=100)
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, id=2)
track_no=1, date=date, length=100, bitrate=100)
track2 = Track(uri=u'uri', name=u'name', artists=artists, album=album,
track_no=1, date=date, length=100, bitrate=100, id=2)
track_no=1, date=date, length=100, bitrate=100)
self.assertEqual(track1, track2)
self.assertEqual(hash(track1), hash(track2))
@ -362,20 +350,14 @@ class TrackTest(unittest.TestCase):
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
def test_ne_id(self):
track1 = Track(id=100)
track2 = Track(id=200)
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, id=2)
track_no=1, date=dt.date.today(), length=100, bitrate=100)
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, id=1)
length=200, bitrate=200)
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))

View File

@ -55,7 +55,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_addid_with_songpos_out_of_bounds_should_ack(self):
needle = Track(uri='dummy://foo', id=137)
needle = Track(uri='dummy://foo')
self.b.library._library = [Track(), Track(), needle, Track()]
self.b.current_playlist.load(
[Track(), Track(), Track(), Track(), Track()])
@ -78,9 +78,10 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_delete_songpos(self):
self.b.current_playlist.load(
[Track(id=1), Track(id=2), Track(id=3), Track(id=4), Track(id=5)])
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'delete "2"')
result = self.h.handle_request(u'delete "%d"' %
self.b.current_playlist.cp_tracks[2][0])
self.assertEqual(len(self.b.current_playlist.tracks), 4)
self.assert_(u'OK' in result)
@ -94,7 +95,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_delete_open_range(self):
self.b.current_playlist.load(
[Track(id=1), Track(id=2), Track(id=3), Track(id=4), Track(id=5)])
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'delete "1:"')
self.assertEqual(len(self.b.current_playlist.tracks), 1)
@ -102,7 +103,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_delete_closed_range(self):
self.b.current_playlist.load(
[Track(id=1), Track(id=2), Track(id=3), Track(id=4), Track(id=5)])
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'delete "1:3"')
self.assertEqual(len(self.b.current_playlist.tracks), 3)

View File

@ -146,7 +146,7 @@ expected_tracks = []
def generate_track(path, ident):
uri = path_to_uri(data_folder(path))
track = Track(name='trackname', artists=expected_artists, track_no=1,
album=expected_albums[0], length=4000, uri=uri, id=ident)
album=expected_albums[0], length=4000, uri=uri)
expected_tracks.append(track)
generate_track('song1.mp3', 6)
@ -170,7 +170,7 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
data_folder(''))
uri = path_to_uri(data_folder('song1.mp3'))
track = Track(name='trackname', artists=expected_artists, track_no=1,
album=expected_albums[0], length=4000, uri=uri, id=0)
album=expected_albums[0], length=4000, uri=uri)
self.assertEqual(set([track]), tracks)
def test_advanced_cache(self):
@ -189,4 +189,4 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
tracks = parse_mpd_tag_cache(data_folder('blank_tag_cache'),
data_folder(''))
uri = path_to_uri(data_folder('song1.mp3'))
self.assertEqual(set([Track(uri=uri, length=4000, id=0)]), tracks)
self.assertEqual(set([Track(uri=uri, length=4000)]), tracks)