From 188211edfc47eca27a7c092ac15b643f52584c32 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 15 Apr 2010 22:05:36 +0200 Subject: [PATCH 01/31] Attempt at fixing uri handling for windows in tests --- tests/backends/gstreamer_test.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/backends/gstreamer_test.py b/tests/backends/gstreamer_test.py index 8852d9e2..87c8a48f 100644 --- a/tests/backends/gstreamer_test.py +++ b/tests/backends/gstreamer_test.py @@ -1,5 +1,6 @@ import unittest import os +import urllib from mopidy.models import Playlist, Track from mopidy.backends.gstreamer import GStreamerBackend @@ -11,21 +12,21 @@ folder = os.path.dirname(__file__) folder = os.path.join(folder, '..', 'data') folder = os.path.abspath(folder) song = os.path.join(folder, 'song%s.wav') -song = 'file://' + song +generate_song = lambda i: 'file:' + urllib.pathname2url(song % i) # FIXME can be switched to generic test class GStreamerCurrentPlaylistHandlerTest(BaseCurrentPlaylistControllerTest, unittest.TestCase): - tracks = [Track(uri=song % i, id=i, length=4464) for i in range(1, 4)] + tracks = [Track(uri=generate_song(i), id=i, length=4464) for i in range(1, 4)] backend_class = GStreamerBackend class GStreamerPlaybackControllerTest(BasePlaybackControllerTest, unittest.TestCase): - tracks = [Track(uri=song % i, id=i, length=4464) for i in range(1, 4)] + tracks = [Track(uri=generate_song(i), id=i, length=4464) for i in range(1, 4)] backend_class = GStreamerBackend def add_track(self, file): - uri = 'file://' + 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) From 7bd576fcc0c1fd4f0b2ca69d9d18cb1e3030696c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 17:51:52 +0200 Subject: [PATCH 02/31] Add some notes on stuff we should do --- docs/development/roadmap.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index b9a5cc02..7b3f0a56 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -39,6 +39,9 @@ Stuff we really want to do, but just not right now stuff into the various distros) to make Debian/Ubuntu installation a breeze. - Create `Homebrew `_ recipies for all our dependencies and Mopidy itself to make OS X installation a breeze. +- Run frontend tests against a real MPD server to ensure we are in sync. +- Start working with MPD-client maintainers to get rid of weird assumptions + like only searching for first two letters etc. Crazy stuff we had to write down somewhere From 2f34d760386fd9f8be2f3384179677d62a36f1f0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 19:59:47 +0200 Subject: [PATCH 03/31] Add docstring to destroy calls --- mopidy/backends/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 44d82545..9003c940 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -42,6 +42,11 @@ class BaseBackend(object): uri_handlers = [] def destroy(self): + ''' + Call destroy on all sub-components in backend so that they can cleanup + after themselves. + ''' + if self.current_playlist: self.current_playlist.destroy() @@ -218,6 +223,7 @@ class BaseCurrentPlaylistController(object): self.playlist = self.playlist.with_(tracks=before+shuffled+after) def destroy(self): + '''Cleanup after component''' pass @@ -274,6 +280,7 @@ class BaseLibraryController(object): raise NotImplementedError def destroy(self): + '''Cleanup after component''' pass @@ -593,6 +600,7 @@ class BasePlaybackController(object): raise NotImplementedError def destroy(self): + '''Cleanup after component''' pass @@ -708,4 +716,5 @@ class BaseStoredPlaylistsController(object): return filter(lambda p: query in p.name, self._playlists) def destroy(self): + '''Cleanup after component''' pass From 25e0df01e9f42de93fca88d467b704cec0d7e5a5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 20:31:09 +0200 Subject: [PATCH 04/31] Set __all__ for mopidy.backends --- mopidy/backends/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 9003c940..fc4eb6f4 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -9,6 +9,10 @@ from mopidy.utils import get_class logger = logging.getLogger('mopidy.backends.base') +__all__ = ['BaseBackend', 'BasePlaybackController', + 'BaseCurrentPlaylistController', 'BaseStoredPlaylistsController', + 'BaseLibraryController'] + class BaseBackend(object): def __init__(self, core_queue=None, mixer=None): self.core_queue = core_queue From b025d360e87203334866ac10951ce47fa6913692 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 20:33:31 +0200 Subject: [PATCH 05/31] Set __all__ for tests.backends.base --- tests/backends/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/backends/base.py b/tests/backends/base.py index 0d62554d..e76d7b08 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -7,6 +7,10 @@ from mopidy.models import Playlist, Track from tests import SkipTest +__all__ = ['BaseCurrentPlaylistControllerTest', + 'BasePlaybackControllerTest', + 'BaseStoredPlaylistsControllerTest'] + def populate_playlist(func): def wrapper(self): for track in self.tracks: From 23a5363ef7a55e04dbcae4fe350b02f543bce701 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 20:33:50 +0200 Subject: [PATCH 06/31] Use * import for gstreamer --- mopidy/backends/gstreamer.py | 4 +--- tests/backends/gstreamer_test.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index 3bf92370..fbe91b71 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -10,9 +10,7 @@ import gst import logging import threading -from mopidy.backends import (BaseBackend, - BasePlaybackController, - BaseCurrentPlaylistController) +from mopidy.backends import * logger = logging.getLogger(u'backends.gstreamer') diff --git a/tests/backends/gstreamer_test.py b/tests/backends/gstreamer_test.py index 87c8a48f..ef48c6c1 100644 --- a/tests/backends/gstreamer_test.py +++ b/tests/backends/gstreamer_test.py @@ -5,8 +5,7 @@ import urllib from mopidy.models import Playlist, Track from mopidy.backends.gstreamer import GStreamerBackend -from tests.backends.base import (BasePlaybackControllerTest, - BaseCurrentPlaylistControllerTest) +from tests.backends.base import * folder = os.path.dirname(__file__) folder = os.path.join(folder, '..', 'data') From cf205d0eae75235f78bd417675a986e219b8d89d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 20:34:14 +0200 Subject: [PATCH 07/31] Add GStreamerBackendStoredPlaylistsControllerTest --- tests/backends/base.py | 13 +++++++++++++ tests/backends/gstreamer_test.py | 7 +++++++ 2 files changed, 20 insertions(+) diff --git a/tests/backends/base.py b/tests/backends/base.py index e76d7b08..9a53ad14 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -876,3 +876,16 @@ class BasePlaybackControllerTest(object): def test_playing_track_that_isnt_in_playlist(self): test = lambda: self.playback.play(self.tracks[0]) self.assertRaises(AssertionError, test) + + +class BaseStoredPlaylistsControllerTest(object): + backend_class = None + + def setUp(self): + self.backend = self.backend_class(mixer=DummyMixer()) + self.stored = self.backend.stored_playlists + + def test_create(self): + self.stored.create('test') + playlists = filter(lambda p: p.name == 'test', self.stored.playlists) + self.assert_(playlists) diff --git a/tests/backends/gstreamer_test.py b/tests/backends/gstreamer_test.py index ef48c6c1..b123b5b8 100644 --- a/tests/backends/gstreamer_test.py +++ b/tests/backends/gstreamer_test.py @@ -19,6 +19,7 @@ class GStreamerCurrentPlaylistHandlerTest(BaseCurrentPlaylistControllerTest, uni backend_class = GStreamerBackend + class GStreamerPlaybackControllerTest(BasePlaybackControllerTest, unittest.TestCase): tracks = [Track(uri=generate_song(i), id=i, length=4464) for i in range(1, 4)] @@ -44,5 +45,11 @@ class GStreamerPlaybackControllerTest(BasePlaybackControllerTest, unittest.TestC self.playback.play() self.assertEqual(self.playback.state, self.playback.PLAYING) + +class GStreamerBackendStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest, + unittest.TestCase): + + backend_class = GStreamerBackend + if __name__ == '__main__': unittest.main() From 24ac5939f9187c9cd6d8eda56d9bf888783b2e75 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 20:34:30 +0200 Subject: [PATCH 08/31] Basoc GStreamerStoredPlaylistsController --- mopidy/backends/gstreamer.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index fbe91b71..93de072f 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -11,6 +11,7 @@ import logging import threading from mopidy.backends import * +from mopidy.models import Playlist logger = logging.getLogger(u'backends.gstreamer') @@ -27,6 +28,7 @@ class GStreamerBackend(BaseBackend): super(GStreamerBackend, self).__init__(*args, **kwargs) self.playback = GStreamerPlaybackController(self) + self.stored_playlists = GStreamerStoredPlaylistsController(self) self.current_playlist = BaseCurrentPlaylistController(self) @@ -99,3 +101,9 @@ class GStreamerPlaybackController(BasePlaybackController): del bus del bin + + +class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): + def create(self, name): + playlist = Playlist(name=name) + self._playlists.append(playlist) From 2316df7ddc585231658683912827bc2ef4e329b4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 20:41:22 +0200 Subject: [PATCH 09/31] Add test_create_in_playlists test --- mopidy/backends/gstreamer.py | 1 + tests/backends/base.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index 93de072f..bfd06e85 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -107,3 +107,4 @@ class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): def create(self, name): playlist = Playlist(name=name) self._playlists.append(playlist) + return playlist diff --git a/tests/backends/base.py b/tests/backends/base.py index 9a53ad14..2d9a564e 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -886,6 +886,10 @@ class BaseStoredPlaylistsControllerTest(object): self.stored = self.backend.stored_playlists def test_create(self): - self.stored.create('test') - playlists = filter(lambda p: p.name == 'test', self.stored.playlists) - self.assert_(playlists) + playlist = self.stored.create('test') + self.assertEqual(playlist.name, 'test') + + def test_create_in_playlists(self): + playlist = self.stored.create('test') + lists = filter(lambda p: p.name == 'test', self.stored.playlists) + self.assert_(lists) From 54694f63c481a47c5404ac42a8e0695c50febf3f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 20:42:36 +0200 Subject: [PATCH 10/31] Add test_playlists_empty_to_start_with test --- tests/backends/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/backends/base.py b/tests/backends/base.py index 2d9a564e..4be477fd 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -893,3 +893,6 @@ class BaseStoredPlaylistsControllerTest(object): playlist = self.stored.create('test') lists = filter(lambda p: p.name == 'test', self.stored.playlists) self.assert_(lists) + + def test_playlists_empty_to_start_with(self): + self.assert_(not self.stored.playlists) From f4eae3af946ab98481eae5f579d4be12a38538bf Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 20:49:46 +0200 Subject: [PATCH 11/31] Add test_delete_non_existant_playlist and test_delete_playlist --- mopidy/backends/gstreamer.py | 4 ++++ tests/backends/base.py | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index bfd06e85..e87bc918 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -108,3 +108,7 @@ class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): playlist = Playlist(name=name) self._playlists.append(playlist) return playlist + + def delete(self, playlist): + if playlist in self._playlists: + self._playlists.remove(playlist) diff --git a/tests/backends/base.py b/tests/backends/base.py index 4be477fd..7ef011c4 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -891,8 +891,15 @@ class BaseStoredPlaylistsControllerTest(object): def test_create_in_playlists(self): playlist = self.stored.create('test') - lists = filter(lambda p: p.name == 'test', self.stored.playlists) - self.assert_(lists) + self.assert_(self.stored.playlists) def test_playlists_empty_to_start_with(self): self.assert_(not self.stored.playlists) + + def test_delete_non_existant_playlist(self): + self.stored.delete(Playlist()) + + def test_delete_playlist(self): + playlist = self.stored.create('test') + self.stored.delete(playlist) + self.assert_(not self.stored.playlists) From ed87fee4cd3da2f7af03cc34b1641198780cea98 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 20:50:35 +0200 Subject: [PATCH 12/31] Ensure that we tear down stored playlist tests --- tests/backends/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/backends/base.py b/tests/backends/base.py index 7ef011c4..9184a07a 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -885,6 +885,9 @@ class BaseStoredPlaylistsControllerTest(object): self.backend = self.backend_class(mixer=DummyMixer()) self.stored = self.backend.stored_playlists + def tearDown(self): + self.backend.destroy() + def test_create(self): playlist = self.stored.create('test') self.assertEqual(playlist.name, 'test') From 6ae2b9c0186408e7c41f74e292579821a4e9fef3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 20:58:26 +0200 Subject: [PATCH 13/31] Add a bunch of get tests for stored playlists --- tests/backends/base.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/backends/base.py b/tests/backends/base.py index 9184a07a..deadf5b6 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -906,3 +906,16 @@ class BaseStoredPlaylistsControllerTest(object): playlist = self.stored.create('test') self.stored.delete(playlist) self.assert_(not self.stored.playlists) + + def test_get_without_criteria(self): + test = lambda: self.stored.get() + self.assertRaises(LookupError, test) + + def test_get_with_wrong_cirteria(self): + test = lambda: self.stored.get(name='foo') + self.assertRaises(LookupError, test) + + def test_get_with_right_criteria(self): + playlist1 = self.stored.create('test') + playlist2 = self.stored.get(name='test') + self.assertEqual(playlist1, playlist2) From 2ff19745bf3e1a8c76fced660df7b08e82baabb4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 21:02:22 +0200 Subject: [PATCH 14/31] Add search tests for stored playlists --- tests/backends/base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/backends/base.py b/tests/backends/base.py index deadf5b6..1689a1bb 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -919,3 +919,18 @@ class BaseStoredPlaylistsControllerTest(object): playlist1 = self.stored.create('test') playlist2 = self.stored.get(name='test') self.assertEqual(playlist1, playlist2) + + def test_search_returns_empty_list(self): + self.assertEqual([], self.stored.search('test')) + + def test_search_returns_playlist(self): + playlist = self.stored.create('test') + playlists = self.stored.search('test') + self.assert_(playlist in playlists) + + def test_search_returns_mulitple_playlists(self): + playlist1 = self.stored.create('test') + playlist2 = self.stored.create('test2') + playlists = self.stored.search('test') + self.assert_(playlist1 in playlists) + self.assert_(playlist2 in playlists) From 516bfac99166215a424d5ce77bc44b82eac19173 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 21:04:34 +0200 Subject: [PATCH 15/31] SkipTest for lookup and refresh tests --- tests/backends/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/backends/base.py b/tests/backends/base.py index 1689a1bb..e7cd2082 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -934,3 +934,9 @@ class BaseStoredPlaylistsControllerTest(object): playlists = self.stored.search('test') self.assert_(playlist1 in playlists) self.assert_(playlist2 in playlists) + + def test_lookup(self): + raise SkipTest + + def test_refresh(self): + raise SkipTest From c16dd17484175b89fd710c3f2ba37e1623d30216 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 21:10:01 +0200 Subject: [PATCH 16/31] Add rename and rename tests --- mopidy/backends/gstreamer.py | 8 ++++++++ tests/backends/base.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index e87bc918..01eeb390 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -112,3 +112,11 @@ class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): def delete(self, playlist): if playlist in self._playlists: self._playlists.remove(playlist) + + def rename(self, playlist, name): + if playlist not in self._playlists: + return + + renamed = playlist.with_(name=name) + index = self._playlists.index(playlist) + self._playlists[index] = renamed diff --git a/tests/backends/base.py b/tests/backends/base.py index e7cd2082..a7c2a925 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -940,3 +940,11 @@ class BaseStoredPlaylistsControllerTest(object): def test_refresh(self): raise SkipTest + + def test_rename(self): + playlist = self.stored.create('test') + self.stored.rename(playlist, 'test2') + self.stored.get(name='test2') + + def test_rename_unknown_playlist(self): + self.stored.rename(Playlist(), 'test2') From 221d5901fd67aa9aacaff7bb0c37c58a8dcc95fe Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 21:14:00 +0200 Subject: [PATCH 17/31] Add save test --- mopidy/backends/gstreamer.py | 3 +++ tests/backends/base.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index 01eeb390..7113a91d 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -120,3 +120,6 @@ class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): renamed = playlist.with_(name=name) index = self._playlists.index(playlist) self._playlists[index] = renamed + + def save(self, playlist): + self._playlists.append(playlist) diff --git a/tests/backends/base.py b/tests/backends/base.py index a7c2a925..1f6e8735 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -948,3 +948,8 @@ class BaseStoredPlaylistsControllerTest(object): def test_rename_unknown_playlist(self): self.stored.rename(Playlist(), 'test2') + + def test_save(self): + playlist = Playlist('test') + self.stored.save(playlist) + self.assert_(playlist in self.stored.playlists) From 4c160dd418e1e89dc6281c8b81103590d56f39b8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 21:22:52 +0200 Subject: [PATCH 18/31] Add uri handler test --- mopidy/backends/gstreamer.py | 1 + tests/backends/gstreamer_test.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index 7113a91d..53613877 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -30,6 +30,7 @@ class GStreamerBackend(BaseBackend): self.playback = GStreamerPlaybackController(self) self.stored_playlists = GStreamerStoredPlaylistsController(self) self.current_playlist = BaseCurrentPlaylistController(self) + self.uri_handlers = [u'file:'] class GStreamerPlaybackController(BasePlaybackController): diff --git a/tests/backends/gstreamer_test.py b/tests/backends/gstreamer_test.py index b123b5b8..1eff6f38 100644 --- a/tests/backends/gstreamer_test.py +++ b/tests/backends/gstreamer_test.py @@ -30,6 +30,9 @@ class GStreamerPlaybackControllerTest(BasePlaybackControllerTest, unittest.TestC 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) + def test_play_mp3(self): self.add_track('blank.mp3') self.playback.play() From 72d4ee274df83a059f8d547b40a63954fca187c6 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 21:36:17 +0200 Subject: [PATCH 19/31] Add stub utility function m3u_to_uris, now for the tests --- mopidy/utils.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/mopidy/utils.py b/mopidy/utils.py index 0142e15c..a37aa6f1 100644 --- a/mopidy/utils.py +++ b/mopidy/utils.py @@ -91,3 +91,25 @@ def spotify_uri_to_int(uri, output_bits=31): compressed_id ^= (full_id & (2 ** output_bits - 1)) full_id >>= output_bits return int(compressed_id) + +def m3u_to_uris(file_path): + """ + Convert M3U file list of uris + + Example M3U data: + + # This is a comment + Alternative\Band - Song.mp3 + Classical\Other Band - New Song.mp3 + Stuff.mp3 + D:\More Music\Foo.mp3 + http://www.example.com:8000/Listen.pls + http://www.example.com/~user/Mine.mp3 + + - Relative paths of songs should be with respect to location of M3U. + - Paths are normaly platform specific. + - Lines starting with # should be ignored. + - m3u files are latin-1, m3u8 files UTF-8 + - This function does not bother with Extended M3U directives. + """ + pass From ce3d23f8cc7e38de294c6dc86dcdcf5d29deba5c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 22:29:21 +0200 Subject: [PATCH 20/31] Add m3u tests --- mopidy/utils.py | 23 ++++++++++++-- tests/data/comment.m3u | 2 ++ tests/data/empty.m3u | 0 tests/data/encoding.m3u | 1 + tests/data/one.m3u | 1 + tests/utils_test.py | 66 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 tests/data/comment.m3u create mode 100644 tests/data/empty.m3u create mode 100644 tests/data/encoding.m3u create mode 100644 tests/data/one.m3u create mode 100644 tests/utils_test.py diff --git a/mopidy/utils.py b/mopidy/utils.py index a37aa6f1..0275e055 100644 --- a/mopidy/utils.py +++ b/mopidy/utils.py @@ -2,6 +2,7 @@ import logging from multiprocessing.reduction import reduce_connection import os import pickle +import urllib logger = logging.getLogger('mopidy.utils') @@ -109,7 +110,25 @@ def m3u_to_uris(file_path): - Relative paths of songs should be with respect to location of M3U. - Paths are normaly platform specific. - Lines starting with # should be ignored. - - m3u files are latin-1, m3u8 files UTF-8 + - m3u files are latin-1. - This function does not bother with Extended M3U directives. """ - pass + + uris = [] + folder = os.path.dirname(file_path) + + with open(file_path) as m3u: + for line in m3u.readlines(): + line = line.strip().decode('latin1') + + if line.startswith('#'): + continue + + 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) + + return uris diff --git a/tests/data/comment.m3u b/tests/data/comment.m3u new file mode 100644 index 00000000..af37f706 --- /dev/null +++ b/tests/data/comment.m3u @@ -0,0 +1,2 @@ +# test +song1.mp3 diff --git a/tests/data/empty.m3u b/tests/data/empty.m3u new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/encoding.m3u b/tests/data/encoding.m3u new file mode 100644 index 00000000..383aa526 --- /dev/null +++ b/tests/data/encoding.m3u @@ -0,0 +1 @@ +æøå.mp3 diff --git a/tests/data/one.m3u b/tests/data/one.m3u new file mode 100644 index 00000000..c7a3d081 --- /dev/null +++ b/tests/data/one.m3u @@ -0,0 +1 @@ +song1.mp3 diff --git a/tests/utils_test.py b/tests/utils_test.py new file mode 100644 index 00000000..4531745d --- /dev/null +++ b/tests/utils_test.py @@ -0,0 +1,66 @@ +#encoding: utf-8 + +import os +import tempfile +import unittest +import urllib + +from mopidy.utils import m3u_to_uris + +def data(name): + folder = os.path.dirname(__file__) + folder = os.path.join(folder, 'data') + folder = os.path.abspath(folder) + return os.path.join(folder, 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')) + + +class M3UToUriTest(unittest.TestCase): + def test_empty_file(self): + uris = m3u_to_uris(data('empty.m3u')) + self.assertEqual([], uris) + + def test_basic_file(self): + uris = m3u_to_uris(data('one.m3u')) + self.assertEqual([song1_uri], uris) + + def test_file_with_comment(self): + uris = m3u_to_uris(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) + + def test_file_with_multiple_absolute_files(self): + with tempfile.NamedTemporaryFile() as file: + file.write(song1_path+'\n') + file.write('# comment \n') + file.write(song2_path) + file.flush() + + uris = m3u_to_uris(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) + + def test_encoding_is_latin1(self): + uris = m3u_to_uris(data('encoding.m3u')) + self.assertEqual([encoded_uri], uris) From 6a92914de67f3bcd3c2048e02f33523b508991ee Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 22:58:37 +0200 Subject: [PATCH 21/31] Use self.save in store playlists create --- mopidy/backends/gstreamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index 53613877..72b7f62c 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -107,7 +107,7 @@ class GStreamerPlaybackController(BasePlaybackController): class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): def create(self, name): playlist = Playlist(name=name) - self._playlists.append(playlist) + self.save(playlist) return playlist def delete(self, playlist): From a7357bda93cc120abc96258e9d28016ea39d8ce8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 23:03:27 +0200 Subject: [PATCH 22/31] Add PLAYLIST_FOLDER setting --- mopidy/settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/settings.py b/mopidy/settings.py index 6cb2928e..ce132ba7 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -101,6 +101,9 @@ SPOTIFY_LIB_APPKEY = u'~/.mopidy/spotify_appkey.key' #: Path to the libspotify cache. Used by LibspotifyBackend. SPOTIFY_LIB_CACHE = u'~/.mopidy/libspotify_cache' +#: Path to playlist folder with m3u files. +PLAYLIST_FOLDER = u'~/.mopidy/playlists' + # Import user specific settings dotdir = os.path.expanduser(u'~/.mopidy/') settings_file = os.path.join(dotdir, u'settings.py') From 0e22335930ba810de8e5cc365ed563c9e8dcfde5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 23:04:18 +0200 Subject: [PATCH 23/31] Store playlist folder in GStreamerStoredPlaylistsController --- mopidy/backends/gstreamer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index 72b7f62c..08c33e70 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -8,10 +8,12 @@ pygst.require('0.10') import gst import logging +import os import threading from mopidy.backends import * from mopidy.models import Playlist +from mopidy import settings logger = logging.getLogger(u'backends.gstreamer') @@ -105,6 +107,10 @@ class GStreamerPlaybackController(BasePlaybackController): class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): + def __init__(self, *args): + super(GStreamerStoredPlaylistsController, self).__init__(*args) + self._folder = os.path.expanduser(settings.PLAYLIST_FOLDER) + def create(self, name): playlist = Playlist(name=name) self.save(playlist) From 9e7363edaffc6a518046b9cba735b12f3abceee8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 23:06:04 +0200 Subject: [PATCH 24/31] Add FIXME --- mopidy/backends/gstreamer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index 08c33e70..b04d69bb 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -109,6 +109,7 @@ class GStreamerPlaybackController(BasePlaybackController): class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): def __init__(self, *args): super(GStreamerStoredPlaylistsController, self).__init__(*args) + # FIXME need test that ensures that folder is created self._folder = os.path.expanduser(settings.PLAYLIST_FOLDER) def create(self, name): From 3b5e94b4a48ef371bd7b9731fe1203378851b3d8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 23:06:43 +0200 Subject: [PATCH 25/31] Use temporary playlist folder for stored playlist tests --- tests/backends/base.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/backends/base.py b/tests/backends/base.py index 1f6e8735..10f3a0e7 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -1,7 +1,11 @@ +import os import random -import time +import shutil +import tempfile import threading +import time +from mopidy import settings from mopidy.mixers.dummy import DummyMixer from mopidy.models import Playlist, Track @@ -882,11 +886,16 @@ class BaseStoredPlaylistsControllerTest(object): backend_class = None def setUp(self): + self.original_folder = settings.PLAYLIST_FOLDER + settings.PLAYLIST_FOLDER = tempfile.mkdtemp() self.backend = self.backend_class(mixer=DummyMixer()) self.stored = self.backend.stored_playlists def tearDown(self): self.backend.destroy() + if os.path.exists(settings.PLAYLIST_FOLDER): + shutil.rmtree(settings.PLAYLIST_FOLDER) + settings.PLAYLIST_FOLDER = self.original_folder def test_create(self): playlist = self.stored.create('test') From 4f3b2cd45af3847d5cb513ade32461cafa112a3d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 23:07:01 +0200 Subject: [PATCH 26/31] Fix test_save test --- tests/backends/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/backends/base.py b/tests/backends/base.py index 10f3a0e7..0546b6fc 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -959,6 +959,6 @@ class BaseStoredPlaylistsControllerTest(object): self.stored.rename(Playlist(), 'test2') def test_save(self): - playlist = Playlist('test') + playlist = Playlist(name='test') self.stored.save(playlist) self.assert_(playlist in self.stored.playlists) From 827d4502116b93970c08ba68632ef4351181345e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 23:07:31 +0200 Subject: [PATCH 27/31] Add FIXME --- tests/backends/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/backends/base.py b/tests/backends/base.py index 0546b6fc..a03ea95d 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -959,6 +959,7 @@ class BaseStoredPlaylistsControllerTest(object): self.stored.rename(Playlist(), 'test2') 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) From 8af5b553a3512cfd7b907c6cab6f7bf48af64e5b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 23:07:50 +0200 Subject: [PATCH 28/31] Add basic test_created_playlist_is_persisted test --- tests/backends/gstreamer_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/backends/gstreamer_test.py b/tests/backends/gstreamer_test.py index 1eff6f38..f3cacd73 100644 --- a/tests/backends/gstreamer_test.py +++ b/tests/backends/gstreamer_test.py @@ -4,6 +4,7 @@ import urllib from mopidy.models import Playlist, Track from mopidy.backends.gstreamer import GStreamerBackend +from mopidy import settings from tests.backends.base import * @@ -54,5 +55,11 @@ class GStreamerBackendStoredPlaylistsControllerTest(BaseStoredPlaylistsControlle backend_class = GStreamerBackend + def test_created_playlist_is_persisted(self): + self.stored.create('test') + playlist = os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') + self.assert_(os.path.exists(playlist)) + + if __name__ == '__main__': unittest.main() From 67ea51e719d112484e853de52284e03d45cd625b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 23:08:02 +0200 Subject: [PATCH 29/31] Fix test_created_playlist_is_persisted --- mopidy/backends/gstreamer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index b04d69bb..8a954d6d 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -130,4 +130,6 @@ class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): self._playlists[index] = renamed def save(self, playlist): + file = os.path.join(self._folder, playlist.name + '.m3u') + open(file, 'w').close() self._playlists.append(playlist) From 658925dfc101cb4d7ea2dcefd3bb4fdadaed42bd Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 23:19:19 +0200 Subject: [PATCH 30/31] Add additional persistence tests for playlists --- mopidy/backends/gstreamer.py | 10 ++++++++-- tests/backends/gstreamer_test.py | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index 8a954d6d..bab8df1e 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -118,8 +118,14 @@ class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): return playlist def delete(self, playlist): - if playlist in self._playlists: - self._playlists.remove(playlist) + if playlist not in self._playlists: + return + + self._playlists.remove(playlist) + file = os.path.join(self._folder, playlist.name + '.m3u') + + if os.path.exists(file): + os.remove(file) def rename(self, playlist, name): if playlist not in self._playlists: diff --git a/tests/backends/gstreamer_test.py b/tests/backends/gstreamer_test.py index f3cacd73..acef94ec 100644 --- a/tests/backends/gstreamer_test.py +++ b/tests/backends/gstreamer_test.py @@ -57,8 +57,19 @@ class GStreamerBackendStoredPlaylistsControllerTest(BaseStoredPlaylistsControlle def test_created_playlist_is_persisted(self): self.stored.create('test') - playlist = os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') - self.assert_(os.path.exists(playlist)) + file= os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') + self.assert_(os.path.exists(file)) + + def test_saved_playlist_is_persisted(self): + self.stored.save(Playlist(name='test2')) + file= os.path.join(settings.PLAYLIST_FOLDER, 'test2.m3u') + self.assert_(os.path.exists(file)) + + def test_deleted_playlist_get_removed(self): + playlist = self.stored.create('test') + self.stored.delete(playlist) + file= os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') + self.assert_(not os.path.exists(file)) if __name__ == '__main__': From cf05777e9fae826c2470967f7878c70274c95f93 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 26 Apr 2010 23:24:12 +0200 Subject: [PATCH 31/31] Fix renaming of stored playlists on disk --- mopidy/backends/gstreamer.py | 6 ++++++ tests/backends/gstreamer_test.py | 14 +++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/gstreamer.py b/mopidy/backends/gstreamer.py index bab8df1e..594f55be 100644 --- a/mopidy/backends/gstreamer.py +++ b/mopidy/backends/gstreamer.py @@ -9,6 +9,7 @@ pygst.require('0.10') import gst import logging import os +import shutil import threading from mopidy.backends import * @@ -131,10 +132,15 @@ class GStreamerStoredPlaylistsController(BaseStoredPlaylistsController): if playlist not in self._playlists: return + src = os.path.join(self._folder, playlist.name + '.m3u') + dst = os.path.join(self._folder, name + '.m3u') + renamed = playlist.with_(name=name) index = self._playlists.index(playlist) self._playlists[index] = renamed + shutil.move(src, dst) + def save(self, playlist): file = os.path.join(self._folder, playlist.name + '.m3u') open(file, 'w').close() diff --git a/tests/backends/gstreamer_test.py b/tests/backends/gstreamer_test.py index acef94ec..f1f40d11 100644 --- a/tests/backends/gstreamer_test.py +++ b/tests/backends/gstreamer_test.py @@ -57,20 +57,28 @@ class GStreamerBackendStoredPlaylistsControllerTest(BaseStoredPlaylistsControlle def test_created_playlist_is_persisted(self): self.stored.create('test') - file= os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') + file = os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') self.assert_(os.path.exists(file)) def test_saved_playlist_is_persisted(self): self.stored.save(Playlist(name='test2')) - file= os.path.join(settings.PLAYLIST_FOLDER, 'test2.m3u') + file = os.path.join(settings.PLAYLIST_FOLDER, 'test2.m3u') self.assert_(os.path.exists(file)) def test_deleted_playlist_get_removed(self): playlist = self.stored.create('test') self.stored.delete(playlist) - file= os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') + file = os.path.join(settings.PLAYLIST_FOLDER, 'test.m3u') self.assert_(not os.path.exists(file)) + 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(file1)) + self.assert_(os.path.exists(file2)) + if __name__ == '__main__': unittest.main()