diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index ff8c9eda..e45dcf63 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -176,7 +176,8 @@ class BaseCurrentPlaylistController(object): assert start >= 0, 'start must be at least zero' assert end <= len(tracks), 'end can not be larger than playlist length' assert to_position >= 0, 'to_position must be at least zero' - assert to_position <= len(tracks), 'to_position can not be larger than playlist length' + assert to_position <= len(tracks), 'to_position can not be larger ' + \ + 'than playlist length' new_tracks = tracks[:start] + tracks[end:] for track in tracks[start:end]: @@ -218,7 +219,8 @@ class BaseCurrentPlaylistController(object): assert start >= 0, 'start must be at least zero' if end is not None: - assert end <= len(tracks), 'end can not be larger than playlist length' + assert end <= len(tracks), 'end can not be larger than ' + \ + 'playlist length' before = tracks[:start or 0] shuffled = tracks[start:end] diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index 8a79ff11..8af42a77 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -16,7 +16,7 @@ import threading from mopidy.backends import * from mopidy.models import Playlist, Track from mopidy import settings -from mopidy.utils import m3u_to_uris +from mopidy.utils import parse_m3u logger = logging.getLogger(u'backends.gstreamer') @@ -41,7 +41,7 @@ class GStreamerBackend(BaseBackend): self.playback = GStreamerPlaybackController(self) self.stored_playlists = GStreamerStoredPlaylistsController(self) self.current_playlist = BaseCurrentPlaylistController(self) - self.uri_handlers = [u'file:'] + self.uri_handlers = [u'file://'] class GStreamerPlaybackController(BasePlaybackController): @@ -127,11 +127,12 @@ class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): for m3u in glob.glob(os.path.join(self._folder, '*.m3u')): name = os.path.basename(m3u)[:len('.m3u')] - track_uris = m3u_to_uris(m3u) + track_uris = parse_m3u(m3u) tracks = map(lambda u: Track(uri=u), track_uris) playlist = Playlist(tracks=tracks, name=name) # FIXME playlist name needs better handling + # FIXME tracks should come from lib. lookup playlists.append(playlist) @@ -170,8 +171,8 @@ class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): with open(file_path, 'w') as file: for track in playlist.tracks: - if track.uri.startswith('file:'): - file.write(track.uri[len('file:'):] + '\n') + if track.uri.startswith('file://'): + file.write(track.uri[len('file://'):] + '\n') else: file.write(track.uri + '\n') diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index 9d065d9a..3218dbe5 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -37,7 +37,7 @@ class DenonMixer(BaseMixer): self._lock = Lock() def _get_volume(self): - self._lock.acquire(); + self._lock.acquire() self.ensure_open_device() self._device.write('MV?\r') vol = str(self._device.readline()[2:4]) diff --git a/mopidy/mixers/osa.py b/mopidy/mixers/osa.py index 6291cac1..3aeaed5c 100644 --- a/mopidy/mixers/osa.py +++ b/mopidy/mixers/osa.py @@ -17,7 +17,7 @@ class OsaMixer(BaseMixer): and (int(time.time() - self._last_update) < self.CACHE_TTL)) def _get_volume(self): - if not self._valid_cache(): + if not self._valid_cache(): try: self._cache = int(Popen( ['osascript', '-e', diff --git a/mopidy/models.py b/mopidy/models.py index 6d0b0dee..42344677 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -10,13 +10,32 @@ class ImmutableObject(object): """ def __init__(self, *args, **kwargs): - self.__dict__.update(kwargs) + for key, value in kwargs.items(): + if not hasattr(self, key): + raise TypeError('__init__() got an unexpected keyword ' + \ + 'argument \'%s\'' % key) + self.__dict__[key] = value def __setattr__(self, name, value): if name.startswith('_'): return super(ImmutableObject, self).__setattr__(name, value) raise AttributeError('Object is immutable.') + def __hash__(self): + hash_sum = 0 + for key, value in self.__dict__.items(): + hash_sum += hash(key) + hash(value) + return hash_sum + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not self.__eq__(other) + class Artist(ImmutableObject): """ @@ -55,13 +74,13 @@ class Album(ImmutableObject): num_tracks = 0 def __init__(self, *args, **kwargs): - self._artists = kwargs.pop('artists', []) + self._artists = frozenset(kwargs.pop('artists', [])) super(Album, self).__init__(*args, **kwargs) @property def artists(self): """List of :class:`Artist` elements. Read-only.""" - return copy(self._artists) + return list(self._artists) class Track(ImmutableObject): @@ -111,13 +130,13 @@ class Track(ImmutableObject): id = None def __init__(self, *args, **kwargs): - self._artists = kwargs.pop('artists', []) + self._artists = frozenset(kwargs.pop('artists', [])) super(Track, self).__init__(*args, **kwargs) @property def artists(self): """List of :class:`Artist`. Read-only.""" - return copy(self._artists) + return list(self._artists) def mpd_format(self, position=0, search_result=False): """ @@ -151,7 +170,9 @@ class Track(ImmutableObject): :rtype: string """ - return u', '.join([a.name for a in self.artists]) + artists = list(self._artists) + artists.sort(key=lambda a: a.name) + return u', '.join([a.name for a in artists]) class Playlist(ImmutableObject): diff --git a/mopidy/mpd/frontend.py b/mopidy/mpd/frontend.py index c2ac84ea..21d1752b 100644 --- a/mopidy/mpd/frontend.py +++ b/mopidy/mpd/frontend.py @@ -643,7 +643,7 @@ class MpdFrontend(object): - capitalizes the type argument. """ type = type.lower() - pass # TODO + # TODO @handle_pattern(r'^listall "(?P[^"]+)"') def _music_db_listall(self, uri): diff --git a/mopidy/settings.py b/mopidy/settings.py index ce132ba7..886e7189 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -26,7 +26,8 @@ BACKENDS = ( #: The log format used on the console. See #: http://docs.python.org/library/logging.html#formatter-objects for details on #: the format. -CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(asctime)s [%(process)d:%(threadName)s] %(name)s\n %(message)s' +CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \ + ' [%(process)d:%(threadName)s] %(name)s\n %(message)s' #: Protocol frontend to use. Default:: #: @@ -104,6 +105,12 @@ SPOTIFY_LIB_CACHE = u'~/.mopidy/libspotify_cache' #: Path to playlist folder with m3u files. PLAYLIST_FOLDER = u'~/.mopidy/playlists' +#: Path to folder with local music. +MUSIC_FOLDER = u'~/music' + +#: Path to MPD tag_cache for local music +TAG_CACHE = u'~/.mopidy/tag_cache' + # Import user specific settings dotdir = os.path.expanduser(u'~/.mopidy/') settings_file = os.path.join(dotdir, u'settings.py') diff --git a/mopidy/utils.py b/mopidy/utils.py index f0d972ee..3b5de37d 100644 --- a/mopidy/utils.py +++ b/mopidy/utils.py @@ -6,6 +6,8 @@ import urllib logger = logging.getLogger('mopidy.utils') +from mopidy.models import Track, Artist, Album + def flatten(the_list): result = [] for element in the_list: @@ -93,7 +95,7 @@ def spotify_uri_to_int(uri, output_bits=31): full_id >>= output_bits return int(compressed_id) -def m3u_to_uris(file_path): +def parse_m3u(file_path): """ Convert M3U file list of uris @@ -124,11 +126,71 @@ def m3u_to_uris(file_path): if line.startswith('#'): continue - if line.startswith('file:'): + # FIXME what about other URI types? + if line.startswith('file://'): uris.append(line) else: - file = os.path.join(folder, line) - path = urllib.pathname2url(file.encode('utf-8')) - uris.append('file:' + path) + path = os.path.join(folder, line) + path = urllib.pathname2url(path.encode('utf-8')) + uris.append('file://' + path) return uris + +def parse_mpd_tag_cache(tag_cache, music_dir=''): + """ + Converts a MPD tag_cache into a lists of tracks, artists and albums. + """ + with open(tag_cache) as library: + contents = library.read() + + tracks = set() + artists = set() + albums = set() + current = {} + state = None + + for line in contents.split('\n'): + if line == 'songList begin': + state = 'songs' + continue + elif line == 'songList end': + state = None + continue + elif not state: + continue + + key, value = line.split(': ', 1) + + if key == 'key': + _convert_mpd_data(current, tracks, artists, albums, music_dir) + current.clear() + + current[key.lower()] = value + + _convert_mpd_data(current, tracks, artists, albums, music_dir) + + return tracks, artists, albums + +def _convert_mpd_data(data, tracks, artists, albums, music_dir): + if not data: + return + + num_tracks = int(data['track'].split('/')[1]) + track_no = int(data['track'].split('/')[0]) + path = data['file'] + + if path[0] == '/': + path = path[1:] + + path = os.path.join(music_dir, path) + uri = 'file://' + urllib.pathname2url(path) + + artist = Artist(name=data['artist']) + artists.add(artist) + + album = Album(name=data['album'], artists=[artist], num_tracks=num_tracks) + albums.add(album) + + track = Track(name=data['title'], artists=[artist], track_no=track_no, + length=int(data['time'])*1000, uri=uri, album=album) + tracks.add(track) diff --git a/tests/backends/base.py b/tests/backends/base.py index 7dae0a68..1fd3c3e4 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -13,7 +13,8 @@ from tests import SkipTest __all__ = ['BaseCurrentPlaylistControllerTest', 'BasePlaybackControllerTest', - 'BaseStoredPlaylistsControllerTest'] + 'BaseStoredPlaylistsControllerTest', + 'BaseLibraryControllerTest'] def populate_playlist(func): def wrapper(self): @@ -96,7 +97,7 @@ class BaseCurrentPlaylistControllerTest(object): def test_load(self): new_playlist = Playlist() - self.assertNotEqual(new_playlist, self.controller.playlist) + self.assertNotEqual(id(new_playlist), id(self.controller.playlist)) self.controller.load(new_playlist) # FIXME how do we test this without going into internals? self.assertEqual(new_playlist, self.controller._playlist) @@ -175,7 +176,7 @@ class BaseCurrentPlaylistControllerTest(object): playlist1 = self.controller.playlist playlist2 = self.controller.playlist - self.assertNotEqual(playlist1, playlist2) + self.assertNotEqual(id(playlist1), id(playlist2)) @populate_playlist def test_remove(self): @@ -965,9 +966,59 @@ class BaseStoredPlaylistsControllerTest(object): def test_rename_unknown_playlist(self): self.stored.rename(Playlist(), 'test2') + test = lambda: self.stored.get(name='test2') + self.assertRaises(LookupError, test) def test_save(self): # FIXME should we handle playlists without names? playlist = Playlist(name='test') self.stored.save(playlist) self.assert_(playlist in self.stored.playlists) + + +class BaseLibraryControllerTest(object): + def setUp(self): + self.backend = self.backend_class(mixer=DummyMixer()) + self.controller = self.backend.library + + def tearDown(self): + self.backend.destroy() + + def test_refresh(self): + raise SkipTest + + def test_lookup(self): + raise SkipTest + + def test_lookup_unknown_track(self): + raise SkipTest + + def test_find_exact_no_hits(self): + raise SkipTest + + def test_find_exact_artist(self): + raise SkipTest + + def test_find_exact_track(self): + raise SkipTest + + def test_find_exact_album(self): + raise SkipTest + + def test_find_search_no_hits(self): + raise SkipTest + + def test_find_search_artist(self): + raise SkipTest + + def test_find_search_track(self): + raise SkipTest + + def test_find_search_album(self): + raise SkipTest + + def test_find_search_uri(self): + raise SkipTest + + def test_find_search_any(self): + raise SkipTest diff --git a/tests/backends/gstreamer_test.py b/tests/backends/gstreamer_test.py index 71e92a53..8dc454c4 100644 --- a/tests/backends/gstreamer_test.py +++ b/tests/backends/gstreamer_test.py @@ -14,7 +14,7 @@ folder = os.path.dirname(__file__) folder = os.path.join(folder, '..', 'data') folder = os.path.abspath(folder) song = os.path.join(folder, 'song%s.wav') -generate_song = lambda i: 'file:' + urllib.pathname2url(song % i) +generate_song = lambda i: 'file://' + urllib.pathname2url(song % i) # FIXME can be switched to generic test class GStreamerCurrentPlaylistHandlerTest(BaseCurrentPlaylistControllerTest, unittest.TestCase): @@ -28,12 +28,12 @@ class GStreamerPlaybackControllerTest(BasePlaybackControllerTest, unittest.TestC backend_class = GStreamerBackend def add_track(self, file): - uri = 'file:' + urllib.pathname2url(os.path.join(folder, file)) + uri = 'file://' + urllib.pathname2url(os.path.join(folder, file)) track = Track(uri=uri, id=1, length=4464) self.backend.current_playlist.add(track) def test_uri_handler(self): - self.assert_('file:' in self.backend.uri_handlers) + self.assert_('file://' in self.backend.uri_handlers) def test_play_mp3(self): self.add_track('blank.mp3') @@ -57,14 +57,16 @@ class GStreamerBackendStoredPlaylistsControllerTest(BaseStoredPlaylistsControlle backend_class = GStreamerBackend def test_created_playlist_is_persisted(self): + path = os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') + self.assert_(not os.path.exists(path)) self.stored.create('test') - file = os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') - self.assert_(os.path.exists(file)) + self.assert_(os.path.exists(path)) def test_saved_playlist_is_persisted(self): + path = os.path.join(settings.PLAYLIST_FOLDER, 'test2.m3u') + self.assert_(not os.path.exists(path)) self.stored.save(Playlist(name='test2')) - file = os.path.join(settings.PLAYLIST_FOLDER, 'test2.m3u') - self.assert_(os.path.exists(file)) + self.assert_(os.path.exists(path)) def test_deleted_playlist_get_removed(self): playlist = self.stored.create('test') @@ -74,15 +76,16 @@ class GStreamerBackendStoredPlaylistsControllerTest(BaseStoredPlaylistsControlle def test_renamed_playlist_gets_moved(self): playlist = self.stored.create('test') - self.stored.rename(playlist, 'test2') file1 = os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') file2 = os.path.join(settings.PLAYLIST_FOLDER, 'test2.m3u') + self.assert_(not os.path.exists(file2)) + self.stored.rename(playlist, 'test2') self.assert_(not os.path.exists(file1)) self.assert_(os.path.exists(file2)) def test_playlist_contents_get_written_to_disk(self): track = Track(uri=generate_song(1)) - uri = track.uri[len('file:'):] + uri = track.uri[len('file://'):] playlist = Playlist(tracks=[track], name='test') file_path = os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') @@ -95,7 +98,7 @@ class GStreamerBackendStoredPlaylistsControllerTest(BaseStoredPlaylistsControlle def test_playlists_are_loaded_at_startup(self): track = Track(uri=generate_song(1)) - uri = track.uri[len('file:'):] + uri = track.uri[len('file://'):] playlist = Playlist(tracks=[track], name='test') file_path = os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') @@ -121,5 +124,12 @@ class GStreamerBackendStoredPlaylistsControllerTest(BaseStoredPlaylistsControlle def test_save_sets_playlist_uri(self): raise SkipTest + +class GStreamerBackendLibraryControllerTest(BaseStoredPlaylistsControllerTest, + unittest.TestCase): + + backend_class = GStreamerBackend + + if __name__ == '__main__': unittest.main() diff --git a/tests/data/advanced_tag_cache b/tests/data/advanced_tag_cache new file mode 100644 index 00000000..3288275f --- /dev/null +++ b/tests/data/advanced_tag_cache @@ -0,0 +1,102 @@ +info_begin +mpd_version: 0.14.2 +fs_charset: UTF-8 +info_end +directory: subdir1 +begin: subdir1 +directory: subsubdir +begin: subdir1/subsubdir +songList begin +key: song8.mp3 +file: subdir1/subsubdir/song8.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +Track: 1/2 +Date: 2006 +mtime: 1272319626 +key: song9.mp3 +file: subdir1/subsubdir/song9.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +Track: 1/2 +Date: 2006 +mtime: 1272319626 +songList end +end: subdir1/subsubdir +songList begin +key: song4.mp3 +file: subdir1/song4.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +Track: 1/2 +Date: 2006 +mtime: 1272319626 +key: song5.mp3 +file: subdir1/song5.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +Track: 1/2 +Date: 2006 +mtime: 1272319626 +songList end +end: subdir1 +directory: subdir2 +begin: subdir2 +songList begin +key: song6.mp3 +file: subdir2/song6.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +Track: 1/2 +Date: 2006 +mtime: 1272319626 +key: song7.mp3 +file: subdir2/song7.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +Track: 1/2 +Date: 2006 +mtime: 1272319626 +songList end +end: subdir2 +songList begin +key: song1.mp3 +file: /song1.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +Track: 1/2 +Date: 2006 +mtime: 1272319626 +key: song2.mp3 +file: /song2.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +Track: 1/2 +Date: 2006 +mtime: 1272319626 +key: song3.mp3 +file: /song3.mp3 +Time: 4 +Artist: name +Title: trackname +Album: albumname +Track: 1/2 +Date: 2006 +mtime: 1272319626 +songList end diff --git a/tests/data/empty_tag_cache b/tests/data/empty_tag_cache new file mode 100644 index 00000000..84053d90 --- /dev/null +++ b/tests/data/empty_tag_cache @@ -0,0 +1,6 @@ +info_begin +mpd_version: 0.14.2 +fs_charset: UTF-8 +info_end +songList begin +songList end diff --git a/tests/data/simple_tag_cache b/tests/data/simple_tag_cache new file mode 100644 index 00000000..cc71ac6d --- /dev/null +++ b/tests/data/simple_tag_cache @@ -0,0 +1,15 @@ +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 +Track: 1/2 +Date: 2006 +mtime: 1272319626 +songList end diff --git a/tests/models_test.py b/tests/models_test.py index 43d6ca53..ced716f2 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -3,6 +3,8 @@ import unittest from mopidy.models import Artist, Album, Track, Playlist +from tests import SkipTest + class ArtistTest(unittest.TestCase): def test_uri(self): uri = u'an_uri' @@ -16,6 +18,52 @@ class ArtistTest(unittest.TestCase): self.assertEqual(artist.name, name) self.assertRaises(AttributeError, setattr, artist, 'name', None) + def test_invalid_kwarg(self): + test = lambda: Artist(foo='baz') + self.assertRaises(TypeError, test) + + def test_eq_name(self): + artist1 = Artist(name=u'name') + artist2 = Artist(name=u'name') + self.assertEqual(artist1, artist2) + self.assertEqual(hash(artist1), hash(artist2)) + + def test_eq_uri(self): + artist1 = Artist(uri=u'uri') + artist2 = Artist(uri=u'uri') + 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') + self.assertEqual(artist1, artist2) + self.assertEqual(hash(artist1), hash(artist2)) + + def test_eq_none(self): + self.assertNotEqual(Artist(), None) + + def test_eq_other(self): + self.assertNotEqual(Artist(), 'other') + + def test_ne_name(self): + artist1 = Artist(name=u'name1') + artist2 = Artist(name=u'name2') + self.assertNotEqual(artist1, artist2) + self.assertNotEqual(hash(artist1), hash(artist2)) + + def test_ne_uri(self): + artist1 = Artist(uri=u'uri1') + artist2 = Artist(uri=u'uri2') + 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') + self.assertNotEqual(artist1, artist2) + self.assertNotEqual(hash(artist1), hash(artist2)) + class AlbumTest(unittest.TestCase): def test_uri(self): @@ -42,6 +90,86 @@ class AlbumTest(unittest.TestCase): self.assertEqual(album.num_tracks, num_tracks) self.assertRaises(AttributeError, setattr, album, 'num_tracks', None) + def test_invalid_kwarg(self): + test = lambda: Album(foo='baz') + self.assertRaises(TypeError, test) + + def test_eq_name(self): + album1 = Album(name=u'name') + album2 = Album(name=u'name') + self.assertEqual(album1, album2) + self.assertEqual(hash(album1), hash(album2)) + + def test_eq_uri(self): + album1 = Album(uri=u'uri') + album2 = Album(uri=u'uri') + self.assertEqual(album1, album2) + self.assertEqual(hash(album1), hash(album2)) + + def test_eq_artists(self): + artists = [Artist()] + album1 = Album(artists=artists) + album2 = Album(artists=artists) + self.assertEqual(album1, album2) + self.assertEqual(hash(album1), hash(album2)) + + def test_eq_artists_order(self): + artist1 = Artist(name=u'name1') + artist2 = Artist(name=u'name2') + album1 = Album(artists=[artist1, artist2]) + album2 = Album(artists=[artist2, artist1]) + self.assertEqual(album1, album2) + self.assertEqual(hash(album1), hash(album2)) + + def test_eq_num_tracks(self): + album1 = Album(num_tracks=2) + album2 = Album(num_tracks=2) + 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) + self.assertEqual(album1, album2) + self.assertEqual(hash(album1), hash(album2)) + + def test_eq_none(self): + self.assertNotEqual(Album(), None) + + def test_eq_other(self): + self.assertNotEqual(Album(), 'other') + + def test_ne_name(self): + album1 = Album(name=u'name1') + album2 = Album(name=u'name2') + self.assertNotEqual(album1, album2) + self.assertNotEqual(hash(album1), hash(album2)) + + def test_ne_uri(self): + album1 = Album(uri=u'uri1') + album2 = Album(uri=u'uri2') + self.assertNotEqual(album1, album2) + self.assertNotEqual(hash(album1), hash(album2)) + + def test_ne_artists(self): + album1 = Album(artists=[Artist(name=u'name1')]) + album2 = Album(artists=[Artist(name=u'name2')]) + self.assertNotEqual(album1, album2) + self.assertNotEqual(hash(album1), hash(album2)) + + def test_ne_num_tracks(self): + album1 = Album(num_tracks=1) + album2 = Album(num_tracks=2) + 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) + album2 = Album(name=u'name2', uri=u'uri2', artists=[Artist(name=u'name2')], num_tracks=2) + self.assertNotEqual(album1, album2) + self.assertNotEqual(hash(album1), hash(album2)) + class TrackTest(unittest.TestCase): def test_uri(self): @@ -57,9 +185,9 @@ class TrackTest(unittest.TestCase): self.assertRaises(AttributeError, setattr, track, 'name', None) def test_artists(self): - artists = [Artist(), Artist()] + artists = [Artist(name=u'name1'), Artist(name=u'name2')] track = Track(artists=artists) - self.assertEqual(track.artists, artists) + self.assertEqual(set(track.artists), set(artists)) self.assertRaises(AttributeError, setattr, track, 'artists', None) def test_album(self): @@ -137,6 +265,157 @@ class TrackTest(unittest.TestCase): track = Track(artists=[Artist(name=u'ABBA'), Artist(name=u'Beatles')]) self.assertEqual(track.mpd_format_artists(), u'ABBA, Beatles') + def test_invalid_kwarg(self): + test = lambda: Track(foo='baz') + self.assertRaises(TypeError, test) + + def test_eq_uri(self): + track1 = Track(uri=u'uri1') + track2 = Track(uri=u'uri1') + self.assertEqual(track1, track2) + self.assertEqual(hash(track1), hash(track2)) + + def test_eq_name(self): + track1 = Track(name=u'name1') + track2 = Track(name=u'name1') + self.assertEqual(track1, track2) + self.assertEqual(hash(track1), hash(track2)) + + def test_eq_artists(self): + artists = [Artist()] + track1 = Track(artists=artists) + track2 = Track(artists=artists) + self.assertEqual(track1, track2) + self.assertEqual(hash(track1), hash(track2)) + + def test_eq_artists_order(self): + artist1 = Artist(name=u'name1') + artist2 = Artist(name=u'name2') + track1 = Track(artists=[artist1, artist2]) + track2 = Track(artists=[artist2, artist1]) + self.assertEqual(track1, track2) + self.assertEqual(hash(track1), hash(track2)) + + def test_eq_album(self): + album = Album() + track1 = Track(album=album) + track2 = Track(album=album) + self.assertEqual(track1, track2) + self.assertEqual(hash(track1), hash(track2)) + + def test_eq_track_no(self): + track1 = Track(track_no=1) + track2 = Track(track_no=1) + self.assertEqual(track1, track2) + self.assertEqual(hash(track1), hash(track2)) + + def test_eq_date(self): + date = dt.date.today() + track1 = Track(date=date) + track2 = Track(date=date) + self.assertEqual(track1, track2) + self.assertEqual(hash(track1), hash(track2)) + + def test_eq_length(self): + track1 = Track(length=100) + track2 = Track(length=100) + self.assertEqual(track1, track2) + self.assertEqual(hash(track1), hash(track2)) + + def test_eq_bitrate(self): + track1 = Track(bitrate=100) + track2 = Track(bitrate=100) + 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) + track2 = Track(uri=u'uri', name=u'name', artists=artists, album=album, + track_no=1, date=date, length=100, bitrate=100, id=2) + self.assertEqual(track1, track2) + self.assertEqual(hash(track1), hash(track2)) + + def test_eq_none(self): + self.assertNotEqual(Track(), None) + + def test_eq_other(self): + self.assertNotEqual(Track(), 'other') + + def test_ne_uri(self): + track1 = Track(uri=u'uri1') + track2 = Track(uri=u'uri2') + self.assertNotEqual(track1, track2) + self.assertNotEqual(hash(track1), hash(track2)) + + def test_ne_name(self): + track1 = Track(name=u'name1') + track2 = Track(name=u'name2') + self.assertNotEqual(track1, track2) + self.assertNotEqual(hash(track1), hash(track2)) + + def test_ne_artists(self): + track1 = Track(artists=[Artist(name=u'name1')]) + track2 = Track(artists=[Artist(name=u'name2')]) + self.assertNotEqual(track1, track2) + self.assertNotEqual(hash(track1), hash(track2)) + + def test_ne_album(self): + track1 = Track(album=Album(name=u'name1')) + track2 = Track(album=Album(name=u'name2')) + self.assertNotEqual(track1, track2) + self.assertNotEqual(hash(track1), hash(track2)) + + def test_ne_track_no(self): + track1 = Track(track_no=1) + track2 = Track(track_no=2) + self.assertNotEqual(track1, track2) + self.assertNotEqual(hash(track1), hash(track2)) + + def test_ne_date(self): + track1 = Track(date=dt.date.today()) + track2 = Track(date=dt.date.today()-dt.timedelta(days=1)) + self.assertNotEqual(track1, track2) + self.assertNotEqual(hash(track1), hash(track2)) + + def test_ne_length(self): + track1 = Track(length=100) + track2 = Track(length=200) + self.assertNotEqual(track1, track2) + self.assertNotEqual(hash(track1), hash(track2)) + + def test_ne_bitrate(self): + track1 = Track(bitrate=100) + track2 = Track(bitrate=200) + 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) + 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) + self.assertNotEqual(track1, track2) + self.assertNotEqual(hash(track1), hash(track2)) + class PlaylistTest(unittest.TestCase): def test_uri(self): @@ -241,3 +520,11 @@ class PlaylistTest(unittest.TestCase): self.assertEqual(new_playlist.name, u'a name') self.assertEqual(new_playlist.tracks, tracks) self.assertEqual(new_playlist.last_modified, new_last_modified) + + def test_invalid_kwarg(self): + test = lambda: Playlist(foo='baz') + self.assertRaises(TypeError, test) + + def test_eq(self): + # FIXME missing all equal and hash tests + raise SkipTest diff --git a/tests/utils_test.py b/tests/utils_test.py index 4531745d..3952a7d6 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -5,7 +5,10 @@ import tempfile import unittest import urllib -from mopidy.utils import m3u_to_uris +from mopidy.utils import parse_m3u, parse_mpd_tag_cache +from mopidy.models import Track, Artist, Album + +from tests import SkipTest def data(name): folder = os.path.dirname(__file__) @@ -17,31 +20,30 @@ def data(name): song1_path = data('song1.mp3') song2_path = data('song2.mp3') encoded_path = data(u'æøå.mp3') -song1_uri = 'file:' + urllib.pathname2url(song1_path) -song2_uri = 'file:' + urllib.pathname2url(song2_path) -encoded_uri = 'file:' + urllib.pathname2url(encoded_path.encode('utf-8')) +song1_uri = 'file://' + urllib.pathname2url(song1_path) +song2_uri = 'file://' + urllib.pathname2url(song2_path) +encoded_uri = 'file://' + urllib.pathname2url(encoded_path.encode('utf-8')) class M3UToUriTest(unittest.TestCase): def test_empty_file(self): - uris = m3u_to_uris(data('empty.m3u')) + uris = parse_m3u(data('empty.m3u')) self.assertEqual([], uris) def test_basic_file(self): - uris = m3u_to_uris(data('one.m3u')) + uris = parse_m3u(data('one.m3u')) self.assertEqual([song1_uri], uris) def test_file_with_comment(self): - uris = m3u_to_uris(data('comment.m3u')) + uris = parse_m3u(data('comment.m3u')) self.assertEqual([song1_uri], uris) def test_file_with_absolute_files(self): with tempfile.NamedTemporaryFile() as file: file.write(song1_path) file.flush() - - uris = m3u_to_uris(file.name) - self.assertEqual([song1_uri], uris) + uris = parse_m3u(file.name) + self.assertEqual([song1_uri], uris) def test_file_with_multiple_absolute_files(self): with tempfile.NamedTemporaryFile() as file: @@ -49,18 +51,67 @@ class M3UToUriTest(unittest.TestCase): file.write('# comment \n') file.write(song2_path) file.flush() - - uris = m3u_to_uris(file.name) - self.assertEqual([song1_uri, song2_uri], uris) + uris = parse_m3u(file.name) + self.assertEqual([song1_uri, song2_uri], uris) def test_file_with_uri(self): with tempfile.NamedTemporaryFile() as file: file.write(song1_uri) file.flush() - - uris = m3u_to_uris(file.name) - self.assertEqual([song1_uri], uris) + uris = parse_m3u(file.name) + self.assertEqual([song1_uri], uris) def test_encoding_is_latin1(self): - uris = m3u_to_uris(data('encoding.m3u')) + uris = parse_m3u(data('encoding.m3u')) self.assertEqual([encoded_uri], uris) + +expected_artists = [Artist(name='name')] +expected_albums = [Album(name='albumname', artists=expected_artists, num_tracks=2)] +expected_tracks = [] + +def generate_track(path): + uri = 'file://' + urllib.pathname2url(data(path)) + track = Track(name='trackname', artists=expected_artists, track_no=1, + album=expected_albums[0], length=4000, uri=uri) + expected_tracks.append(track) + +generate_track('song1.mp3') +generate_track('song2.mp3') +generate_track('song3.mp3') +generate_track('subdir1/song4.mp3') +generate_track('subdir1/song5.mp3') +generate_track('subdir2/song6.mp3') +generate_track('subdir2/song7.mp3') +generate_track('subdir1/subsubdir/song8.mp3') +generate_track('subdir1/subsubdir/song9.mp3') + +class MPDTagCacheToTracksTest(unittest.TestCase): + def test_emtpy_cache(self): + tracks, artists, albums = parse_mpd_tag_cache(data('empty_tag_cache'), + data('')) + self.assertEqual(set(), tracks) + self.assertEqual(set(), artists) + self.assertEqual(set(), albums) + + def test_simple_cache(self): + tracks, artists, albums = parse_mpd_tag_cache(data('simple_tag_cache'), + data('')) + + self.assertEqual(expected_tracks[0], list(tracks)[0]) + self.assertEqual(set(expected_artists), artists) + self.assertEqual(set(expected_albums), albums) + + def test_advanced_cache(self): + tracks, artists, albums = parse_mpd_tag_cache(data('advanced_tag_cache'), + data('')) + + self.assertEqual(set(expected_tracks), tracks) + self.assertEqual(set(expected_artists), artists) + self.assertEqual(set(expected_albums), albums) + + def test_unicode_cache(self): + raise SkipTest + + def test_misencoded_cache(self): + # FIXME not sure if this can happen + raise SkipTest