From 775b276169642f8b0e73e6a9ea1057c97d272ede Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 5 Apr 2013 15:40:18 +0200 Subject: [PATCH 01/45] local: Use new config system --- mopidy/backends/local/actor.py | 2 ++ mopidy/backends/local/library.py | 9 ++++---- mopidy/backends/local/playlists.py | 24 ++++++++++--------- tests/backends/base/playlists.py | 17 ++------------ tests/backends/local/events_test.py | 19 ++++++++------- tests/backends/local/library_test.py | 20 ++++++---------- tests/backends/local/playback_test.py | 17 ++++++-------- tests/backends/local/playlists_test.py | 32 +++++++++++++++++--------- tests/backends/local/tracklist_test.py | 17 ++++++-------- 9 files changed, 73 insertions(+), 84 deletions(-) diff --git a/mopidy/backends/local/actor.py b/mopidy/backends/local/actor.py index abad75ca..a1655dd9 100644 --- a/mopidy/backends/local/actor.py +++ b/mopidy/backends/local/actor.py @@ -16,6 +16,8 @@ class LocalBackend(pykka.ThreadingActor, base.Backend): def __init__(self, config, audio): super(LocalBackend, self).__init__() + self.config = config + self.library = LocalLibraryProvider(backend=self) self.playback = base.BasePlaybackProvider(audio=audio, backend=self) self.playlists = LocalPlaylistsProvider(backend=self) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index f2a1a520..2b1c93f7 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import logging -from mopidy import settings from mopidy.backends import base from mopidy.models import Album, SearchResult @@ -15,15 +14,17 @@ class LocalLibraryProvider(base.BaseLibraryProvider): def __init__(self, *args, **kwargs): super(LocalLibraryProvider, self).__init__(*args, **kwargs) self._uri_mapping = {} + self._music_path = self.backend.config['local']['music_path'] + self._playlist_path = self.backend.config['local']['playlist_path'] + self._tag_cache_file = self.backend.config['local']['tag_cache_file'] self.refresh() def refresh(self, uri=None): - tracks = parse_mpd_tag_cache( - settings.LOCAL_TAG_CACHE_FILE, settings.LOCAL_MUSIC_PATH) + tracks = parse_mpd_tag_cache(self._tag_cache_file, self._music_path) logger.info( 'Loading tracks from %s using %s', - settings.LOCAL_MUSIC_PATH, settings.LOCAL_TAG_CACHE_FILE) + self._music_path, self._tag_cache_file) for track in tracks: self._uri_mapping[track.uri] = track diff --git a/mopidy/backends/local/playlists.py b/mopidy/backends/local/playlists.py index 53f7aaae..063d044d 100644 --- a/mopidy/backends/local/playlists.py +++ b/mopidy/backends/local/playlists.py @@ -5,7 +5,6 @@ import logging import os import shutil -from mopidy import settings from mopidy.backends import base, listener from mopidy.models import Playlist from mopidy.utils import formatting, path @@ -19,7 +18,8 @@ logger = logging.getLogger('mopidy.backends.local') class LocalPlaylistsProvider(base.BasePlaylistsProvider): def __init__(self, *args, **kwargs): super(LocalPlaylistsProvider, self).__init__(*args, **kwargs) - self._path = settings.LOCAL_PLAYLIST_PATH + self._music_path = self.backend.config['local']['music_path'] + self._playlist_path = self.backend.config['local']['playlist_path'] self.refresh() def create(self, name): @@ -42,16 +42,16 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): return playlist def refresh(self): - logger.info('Loading playlists from %s', self._path) + logger.info('Loading playlists from %s', self._playlist_path) playlists = [] - for m3u in glob.glob(os.path.join(self._path, '*.m3u')): + for m3u in glob.glob(os.path.join(self._playlist_path, '*.m3u')): uri = path.path_to_uri(m3u) name = os.path.splitext(os.path.basename(m3u))[0] tracks = [] - for track_uri in parse_m3u(m3u, settings.LOCAL_MUSIC_PATH): + for track_uri in parse_m3u(m3u, self._music_path): try: # TODO We must use core.library.lookup() to support tracks # from other backends @@ -86,13 +86,13 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): def _get_m3u_path(self, name): name = formatting.slugify(name) - file_path = os.path.join(self._path, name + '.m3u') - path.check_file_path_is_inside_base_dir(file_path, self._path) + file_path = os.path.join(self._playlist_path, name + '.m3u') + path.check_file_path_is_inside_base_dir(file_path, self._playlist_path) return file_path def _save_m3u(self, playlist): file_path = path.uri_to_path(playlist.uri) - path.check_file_path_is_inside_base_dir(file_path, self._path) + path.check_file_path_is_inside_base_dir(file_path, self._playlist_path) with open(file_path, 'w') as file_handle: for track in playlist.tracks: if track.uri.startswith('file://'): @@ -103,16 +103,18 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): def _delete_m3u(self, uri): file_path = path.uri_to_path(uri) - path.check_file_path_is_inside_base_dir(file_path, self._path) + path.check_file_path_is_inside_base_dir(file_path, self._playlist_path) if os.path.exists(file_path): os.remove(file_path) def _rename_m3u(self, playlist): src_file_path = path.uri_to_path(playlist.uri) - path.check_file_path_is_inside_base_dir(src_file_path, self._path) + path.check_file_path_is_inside_base_dir( + src_file_path, self._playlist_path) dst_file_path = self._get_m3u_path(playlist.name) - path.check_file_path_is_inside_base_dir(dst_file_path, self._path) + path.check_file_path_is_inside_base_dir( + dst_file_path, self._playlist_path) shutil.move(src_file_path, dst_file_path) diff --git a/tests/backends/base/playlists.py b/tests/backends/base/playlists.py index 00e32a6f..ad5648f9 100644 --- a/tests/backends/base/playlists.py +++ b/tests/backends/base/playlists.py @@ -1,25 +1,17 @@ from __future__ import unicode_literals -import os -import shutil -import tempfile - import pykka -from mopidy import audio, core, settings +from mopidy import audio, core from mopidy.models import Playlist -from tests import unittest, path_to_data_dir +from tests import unittest class PlaylistsControllerTest(object): config = {} def setUp(self): - settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp() - settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache') - settings.LOCAL_MUSIC_PATH = path_to_data_dir('') - self.audio = audio.DummyAudio.start().proxy() self.backend = self.backend_class.start( config=self.config, audio=self.audio).proxy() @@ -28,11 +20,6 @@ class PlaylistsControllerTest(object): def tearDown(self): pykka.ActorRegistry.stop_all() - if os.path.exists(settings.LOCAL_PLAYLIST_PATH): - shutil.rmtree(settings.LOCAL_PLAYLIST_PATH) - - settings.runtime.clear() - def test_create_returns_playlist_with_name_set(self): playlist = self.core.playlists.create('test') self.assertEqual(playlist.name, 'test') diff --git a/tests/backends/local/events_test.py b/tests/backends/local/events_test.py index 5ccf0886..83d77a2f 100644 --- a/tests/backends/local/events_test.py +++ b/tests/backends/local/events_test.py @@ -1,4 +1,5 @@ -from mopidy import settings +from __future__ import unicode_literals + from mopidy.backends.local import actor from tests import unittest, path_to_data_dir @@ -7,12 +8,10 @@ from tests.backends.base import events class LocalBackendEventsTest(events.BackendEventsTest, unittest.TestCase): backend_class = actor.LocalBackend - # TODO: setup config - - def setUp(self): - settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache') - super(LocalBackendEventsTest, self).setUp() - - def tearDown(self): - super(LocalBackendEventsTest, self).tearDown() - settings.runtime.clear() + config = { + 'local': { + 'music_path': path_to_data_dir(''), + 'playlist_path': '', + 'tag_cache_file': path_to_data_dir('empty_tag_cache'), + } + } diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index ca90e40b..e582c788 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from mopidy import settings from mopidy.backends.local import actor from tests import unittest, path_to_data_dir @@ -9,15 +8,10 @@ from tests.backends.base.library import LibraryControllerTest class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase): backend_class = actor.LocalBackend - # TODO: setup config - - def setUp(self): - settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache') - settings.LOCAL_MUSIC_PATH = path_to_data_dir('') - - super(LocalLibraryControllerTest, self).setUp() - - def tearDown(self): - settings.runtime.clear() - - super(LocalLibraryControllerTest, self).tearDown() + config = { + 'local': { + 'music_path': path_to_data_dir(''), + 'playlist_path': '', + 'tag_cache_file': path_to_data_dir('library_tag_cache'), + } + } diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index e9b3954c..4c304590 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from mopidy import settings from mopidy.backends.local import actor from mopidy.core import PlaybackState from mopidy.models import Track @@ -13,17 +12,15 @@ from tests.backends.local import generate_song class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): backend_class = actor.LocalBackend + config = { + 'local': { + 'music_path': path_to_data_dir(''), + 'playlist_path': '', + 'tag_cache_file': path_to_data_dir('empty_tag_cache'), + } + } tracks = [ Track(uri=generate_song(i), length=4464) for i in range(1, 4)] - # TODO: setup config - - def setUp(self): - settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache') - super(LocalPlaybackControllerTest, self).setUp() - - def tearDown(self): - super(LocalPlaybackControllerTest, self).tearDown() - settings.runtime.clear() def add_track(self, path): uri = path_to_uri(path_to_data_dir(path)) diff --git a/tests/backends/local/playlists_test.py b/tests/backends/local/playlists_test.py index 3dbc3a2a..8528adf4 100644 --- a/tests/backends/local/playlists_test.py +++ b/tests/backends/local/playlists_test.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals import os +import shutil +import tempfile -from mopidy import settings from mopidy.backends.local import actor from mopidy.models import Track from mopidy.utils.path import path_to_uri @@ -17,25 +18,34 @@ class LocalPlaylistsControllerTest( PlaylistsControllerTest, unittest.TestCase): backend_class = actor.LocalBackend - # TODO: setup config + config = { + 'local': { + 'music_path': path_to_data_dir(''), + 'tag_cache_file': path_to_data_dir('library_tag_cache'), + } + } def setUp(self): - settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache') + self.config['local']['playlist_path'] = tempfile.mkdtemp() + self.playlist_path = self.config['local']['playlist_path'] + super(LocalPlaylistsControllerTest, self).setUp() def tearDown(self): super(LocalPlaylistsControllerTest, self).tearDown() - settings.runtime.clear() + + if os.path.exists(self.playlist_path): + shutil.rmtree(self.playlist_path) def test_created_playlist_is_persisted(self): - path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u') + path = os.path.join(self.playlist_path, 'test.m3u') self.assertFalse(os.path.exists(path)) self.core.playlists.create('test') self.assertTrue(os.path.exists(path)) def test_create_slugifies_playlist_name(self): - path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test-foo-bar.m3u') + path = os.path.join(self.playlist_path, 'test-foo-bar.m3u') self.assertFalse(os.path.exists(path)) playlist = self.core.playlists.create('test FOO baR') @@ -43,7 +53,7 @@ class LocalPlaylistsControllerTest( self.assertTrue(os.path.exists(path)) def test_create_slugifies_names_which_tries_to_change_directory(self): - path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test-foo-bar.m3u') + path = os.path.join(self.playlist_path, 'test-foo-bar.m3u') self.assertFalse(os.path.exists(path)) playlist = self.core.playlists.create('../../test FOO baR') @@ -51,8 +61,8 @@ class LocalPlaylistsControllerTest( self.assertTrue(os.path.exists(path)) def test_saved_playlist_is_persisted(self): - path1 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test1.m3u') - path2 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test2-foo-bar.m3u') + path1 = os.path.join(self.playlist_path, 'test1.m3u') + path2 = os.path.join(self.playlist_path, 'test2-foo-bar.m3u') playlist = self.core.playlists.create('test1') @@ -67,7 +77,7 @@ class LocalPlaylistsControllerTest( self.assertTrue(os.path.exists(path2)) def test_deleted_playlist_is_removed(self): - path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u') + path = os.path.join(self.playlist_path, 'test.m3u') self.assertFalse(os.path.exists(path)) playlist = self.core.playlists.create('test') @@ -90,7 +100,7 @@ class LocalPlaylistsControllerTest( self.assertEqual(track_path, contents.strip()) def test_playlists_are_loaded_at_startup(self): - playlist_path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u') + playlist_path = os.path.join(self.playlist_path, 'test.m3u') track = Track(uri=path_to_uri(path_to_data_dir('uri2'))) playlist = self.core.playlists.create('test') diff --git a/tests/backends/local/tracklist_test.py b/tests/backends/local/tracklist_test.py index 24c400fa..3fc8a0be 100644 --- a/tests/backends/local/tracklist_test.py +++ b/tests/backends/local/tracklist_test.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from mopidy import settings from mopidy.backends.local import actor from mopidy.models import Track @@ -11,14 +10,12 @@ from tests.backends.local import generate_song class LocalTracklistControllerTest(TracklistControllerTest, unittest.TestCase): backend_class = actor.LocalBackend + config = { + 'local': { + 'music_path': path_to_data_dir(''), + 'playlist_path': '', + 'tag_cache_file': path_to_data_dir('empty_tag_cache'), + } + } tracks = [ Track(uri=generate_song(i), length=4464) for i in range(1, 4)] - # TODO: setup config - - def setUp(self): - settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('empty_tag_cache') - super(LocalTracklistControllerTest, self).setUp() - - def tearDown(self): - super(LocalTracklistControllerTest, self).tearDown() - settings.runtime.clear() From 5894e7afe03d92bee416040f2b1586126ee87674 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 5 Apr 2013 15:41:59 +0200 Subject: [PATCH 02/45] stream: Use new config system --- mopidy/backends/stream/__init__.py | 5 ++--- mopidy/backends/stream/actor.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/stream/__init__.py b/mopidy/backends/stream/__init__.py index 9a393bed..17b85d33 100644 --- a/mopidy/backends/stream/__init__.py +++ b/mopidy/backends/stream/__init__.py @@ -23,9 +23,8 @@ protocols = __doc__ = """A backend for playing music for streaming music. -This backend will handle streaming of URIs in -:attr:`mopidy.settings.STREAM_PROTOCOLS` assuming the right plugins are -installed. +This backend will handle streaming of URIs matching the ``stream/protocols`` +config value, assuming the needed GStreamer plugins are installed. **Issues** diff --git a/mopidy/backends/stream/actor.py b/mopidy/backends/stream/actor.py index d6eb31d3..86df447d 100644 --- a/mopidy/backends/stream/actor.py +++ b/mopidy/backends/stream/actor.py @@ -5,7 +5,7 @@ import urlparse import pykka -from mopidy import audio as audio_lib, settings +from mopidy import audio as audio_lib from mopidy.backends import base from mopidy.models import Track @@ -21,7 +21,7 @@ class StreamBackend(pykka.ThreadingActor, base.Backend): self.playlists = None self.uri_schemes = audio_lib.supported_uri_schemes( - settings.STREAM_PROTOCOLS) + config['stream']['protocols']) # TODO: Should we consider letting lookup know how to expand common playlist From 85c2b9b1ebb7a425e2edefd461b888b4f6f7b1e0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 5 Apr 2013 15:44:37 +0200 Subject: [PATCH 03/45] lastfm: Use new config system --- mopidy/frontends/lastfm/actor.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/mopidy/frontends/lastfm/actor.py b/mopidy/frontends/lastfm/actor.py index 1e157d4f..0e5b9c6e 100644 --- a/mopidy/frontends/lastfm/actor.py +++ b/mopidy/frontends/lastfm/actor.py @@ -5,7 +5,7 @@ import time import pykka -from mopidy import exceptions, settings +from mopidy import exceptions from mopidy.core import CoreListener try: @@ -22,21 +22,17 @@ API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd' class LastfmFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super(LastfmFrontend, self).__init__() + self.config = config self.lastfm = None self.last_start_time = None def on_start(self): try: - username = settings.LASTFM_USERNAME - password_hash = pylast.md5(settings.LASTFM_PASSWORD) self.lastfm = pylast.LastFMNetwork( api_key=API_KEY, api_secret=API_SECRET, - username=username, password_hash=password_hash) + username=self.config['lastfm']['username'], + password_hash=pylast.md5(self.config['lastfm']['password'])) logger.info('Connected to Last.fm') - except exceptions.SettingsError as e: - logger.info('Last.fm scrobbler not started') - logger.debug('Last.fm settings error: %s', e) - self.stop() except (pylast.NetworkError, pylast.MalformedResponseError, pylast.WSError) as e: logger.error('Error during Last.fm setup: %s', e) From bcc7c3a890499cddb7d687559a0942761b22c2e7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 5 Apr 2013 15:48:24 +0200 Subject: [PATCH 04/45] mpris: Use new config system --- mopidy/frontends/mpris/__init__.py | 4 ++-- mopidy/frontends/mpris/actor.py | 7 ++++--- mopidy/frontends/mpris/objects.py | 7 ++++--- tests/frontends/mpris/player_interface_test.py | 2 +- .../frontends/mpris/playlists_interface_test.py | 2 +- tests/frontends/mpris/root_interface_test.py | 16 ++++++++-------- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py index 79806c47..4dde2e20 100644 --- a/mopidy/frontends/mpris/__init__.py +++ b/mopidy/frontends/mpris/__init__.py @@ -32,8 +32,8 @@ An example of an MPRIS client is the `Ubuntu Sound Menu Ubuntu Sound Menu. The package is named ``python-indicate`` in Ubuntu/Debian. -- An ``.desktop`` file for Mopidy installed at the path set in - :attr:`mopidy.settings.DESKTOP_FILE`. See :ref:`install-desktop-file` for +- An ``.desktop`` file for Mopidy installed at the path set in the + ``mpris/desktop_file`` config value. See :ref:`install-desktop-file` for details. **Default config** diff --git a/mopidy/frontends/mpris/actor.py b/mopidy/frontends/mpris/actor.py index 11f87922..92805bd3 100644 --- a/mopidy/frontends/mpris/actor.py +++ b/mopidy/frontends/mpris/actor.py @@ -4,7 +4,6 @@ import logging import pykka -from mopidy import settings from mopidy.core import CoreListener from mopidy.frontends.mpris import objects @@ -20,13 +19,14 @@ except ImportError as import_error: class MprisFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super(MprisFrontend, self).__init__() + self.config = config self.core = core self.indicate_server = None self.mpris_object = None def on_start(self): try: - self.mpris_object = objects.MprisObject(self.core) + self.mpris_object = objects.MprisObject(self.config, self.core) self._send_startup_notification() except Exception as e: logger.error('MPRIS frontend setup failed (%s)', e) @@ -53,7 +53,8 @@ class MprisFrontend(pykka.ThreadingActor, CoreListener): logger.debug('Sending startup notification...') self.indicate_server = indicate.Server() self.indicate_server.set_type('music.mopidy') - self.indicate_server.set_desktop_file(settings.DESKTOP_FILE) + self.indicate_server.set_desktop_file( + self.config['mpris']['desktop_file']) self.indicate_server.show() logger.debug('Startup notification sent') diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index 04a72676..696e39bd 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -13,7 +13,6 @@ except ImportError as import_error: from mopidy.exceptions import OptionalDependencyError raise OptionalDependencyError(import_error) -from mopidy import settings from mopidy.core import PlaybackState from mopidy.utils.process import exit_process @@ -36,7 +35,8 @@ class MprisObject(dbus.service.Object): properties = None - def __init__(self, core): + def __init__(self, config, core): + self.config = config self.core = core self.properties = { ROOT_IFACE: self._get_root_iface_properties(), @@ -175,7 +175,8 @@ class MprisObject(dbus.service.Object): ### Root interface properties def get_DesktopEntry(self): - return os.path.splitext(os.path.basename(settings.DESKTOP_FILE))[0] + return os.path.splitext(os.path.basename( + self.config['mpris']['desktop_file']))[0] def get_SupportedUriSchemes(self): return dbus.Array(self.core.uri_schemes.get(), signature='s') diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index ec4a17a9..e1e13084 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -28,7 +28,7 @@ class PlayerInterfaceTest(unittest.TestCase): objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() - self.mpris = objects.MprisObject(core=self.core) + self.mpris = objects.MprisObject(config={}, core=self.core) def tearDown(self): pykka.ActorRegistry.stop_all() diff --git a/tests/frontends/mpris/playlists_interface_test.py b/tests/frontends/mpris/playlists_interface_test.py index 745a858c..67f9e9be 100644 --- a/tests/frontends/mpris/playlists_interface_test.py +++ b/tests/frontends/mpris/playlists_interface_test.py @@ -25,7 +25,7 @@ class PlayerInterfaceTest(unittest.TestCase): objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() - self.mpris = objects.MprisObject(core=self.core) + self.mpris = objects.MprisObject(config={}, core=self.core) foo = self.core.playlists.create('foo').get() foo = foo.copy(last_modified=datetime.datetime(2012, 3, 1, 6, 0, 0)) diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index 36d689a2..806b2162 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -5,7 +5,7 @@ import sys import mock import pykka -from mopidy import core, exceptions, settings +from mopidy import core, exceptions from mopidy.backends import dummy try: @@ -19,11 +19,17 @@ from tests import unittest @unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux') class RootInterfaceTest(unittest.TestCase): def setUp(self): + config = { + 'mpris': { + 'desktop_file': '/tmp/foo.desktop', + } + } + objects.exit_process = mock.Mock() objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() - self.mpris = objects.MprisObject(core=self.core) + self.mpris = objects.MprisObject(config=config, core=self.core) def tearDown(self): pykka.ActorRegistry.stop_all() @@ -66,15 +72,9 @@ class RootInterfaceTest(unittest.TestCase): result = self.mpris.Get(objects.ROOT_IFACE, 'Identity') self.assertEquals(result, 'Mopidy') - def test_desktop_entry_is_mopidy(self): - result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry') - self.assertEquals(result, 'mopidy') - def test_desktop_entry_is_based_on_DESKTOP_FILE_setting(self): - settings.runtime['DESKTOP_FILE'] = '/tmp/foo.desktop' result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry') self.assertEquals(result, 'foo') - settings.runtime.clear() def test_supported_uri_schemes_includes_backend_uri_schemes(self): result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedUriSchemes') From 8a8a78e025cf30bba4460d714668a3148d6e9940 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 5 Apr 2013 15:52:38 +0200 Subject: [PATCH 05/45] http: Use new config system --- mopidy/frontends/http/__init__.py | 28 ++++++++++++++-------------- mopidy/frontends/http/actor.py | 11 ++++++----- tests/frontends/http/events_test.py | 11 ++++++++++- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index d588a376..1b93fb8a 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -61,19 +61,19 @@ Setup The frontend is enabled by default if all dependencies are available. -When it is enabled it starts a web server at the port specified by -:attr:`mopidy.settings.HTTP_SERVER_PORT`. +When it is enabled it starts a web server at the port specified by the +``http/port`` config value. .. warning:: Security As a simple security measure, the web server is by default only available - from localhost. To make it available from other computers, change - :attr:`mopidy.settings.HTTP_SERVER_HOSTNAME`. Before you do so, note that - the HTTP frontend does not feature any form of user authentication or - authorization. Anyone able to access the web server can use the full core - API of Mopidy. Thus, you probably only want to make the web server - available from your local network or place it behind a web proxy which - takes care or user authentication. You have been warned. + from localhost. To make it available from other computers, change the + ``http/hostname`` config value. Before you do so, note that the HTTP + frontend does not feature any form of user authentication or authorization. + Anyone able to access the web server can use the full core API of Mopidy. + Thus, you probably only want to make the web server available from your + local network or place it behind a web proxy which takes care or user + authentication. You have been warned. Using a web based Mopidy client @@ -81,10 +81,11 @@ Using a web based Mopidy client The web server can also host any static files, for example the HTML, CSS, JavaScript, and images needed for a web based Mopidy client. To host static -files, change :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` to point to the -root directory of your web client, e.g.:: +files, change the ``http/static_dir`` to point to the root directory of your +web client, e.g.:: - HTTP_SERVER_STATIC_DIR = u'/home/alice/dev/the-client' + [http] + static_dir = /home/alice/dev/the-client If the directory includes a file named ``index.html``, it will be served on the root of Mopidy's web server. @@ -405,8 +406,7 @@ Example to get started with 2. Create an empty directory for your web client. -3. Change the setting :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` to point - to your new directory. +3. Change the ``http/static_dir`` config value to point to your new directory. 4. Start/restart Mopidy. diff --git a/mopidy/frontends/http/actor.py b/mopidy/frontends/http/actor.py index 54085471..149cbc7f 100644 --- a/mopidy/frontends/http/actor.py +++ b/mopidy/frontends/http/actor.py @@ -6,7 +6,7 @@ import os import pykka -from mopidy import exceptions, models, settings +from mopidy import exceptions, models from mopidy.core import CoreListener try: @@ -25,6 +25,7 @@ logger = logging.getLogger('mopidy.frontends.http') class HttpFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super(HttpFrontend, self).__init__() + self.config = config self.core = core self._setup_server() self._setup_websocket_plugin() @@ -35,8 +36,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): cherrypy.config.update({ 'engine.autoreload_on': False, 'server.socket_host': ( - settings.HTTP_SERVER_HOSTNAME.encode('utf-8')), - 'server.socket_port': settings.HTTP_SERVER_PORT, + self.config['http']['hostname'].encode('utf-8')), + 'server.socket_port': self.config['http']['port'], }) def _setup_websocket_plugin(self): @@ -48,8 +49,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): root.mopidy = MopidyResource() root.mopidy.ws = ws.WebSocketResource(self.core) - if settings.HTTP_SERVER_STATIC_DIR: - static_dir = settings.HTTP_SERVER_STATIC_DIR + if self.config['http']['static_dir']: + static_dir = self.config['http']['static_dir'] else: static_dir = os.path.join(os.path.dirname(__file__), 'data') logger.debug('HTTP server will serve "%s" at /', static_dir) diff --git a/tests/frontends/http/events_test.py b/tests/frontends/http/events_test.py index 7661ac6e..c334eefa 100644 --- a/tests/frontends/http/events_test.py +++ b/tests/frontends/http/events_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import json try: @@ -24,7 +26,14 @@ from tests import unittest @mock.patch('cherrypy.engine.publish') class HttpEventsTest(unittest.TestCase): def setUp(self): - self.http = actor.HttpFrontend(config=None, core=mock.Mock()) + config = { + 'http': { + 'hostname': '127.0.0.1', + 'port': 6680, + 'static_dir': None, + } + } + self.http = actor.HttpFrontend(config=config, core=mock.Mock()) def test_track_playback_paused_is_broadcasted(self, publish): publish.reset_mock() From c452f0115a386db2e8aa89d26f0a6b877241ba93 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 5 Apr 2013 16:07:00 +0200 Subject: [PATCH 06/45] mpd: Use new config system --- mopidy/frontends/mpd/actor.py | 15 +++--- mopidy/frontends/mpd/dispatcher.py | 15 ++++-- mopidy/frontends/mpd/protocol/connection.py | 3 +- mopidy/frontends/mpd/session.py | 5 +- mopidy/frontends/mpd/translator.py | 21 ++++---- mopidy/scanner.py | 4 +- tests/frontends/mpd/dispatcher_test.py | 7 ++- tests/frontends/mpd/protocol/__init__.py | 10 +++- .../mpd/protocol/authentication_test.py | 41 ++++++-------- .../frontends/mpd/protocol/connection_test.py | 17 ------ .../frontends/mpd/protocol/reflection_test.py | 53 ++++++++++--------- tests/frontends/mpd/translator_test.py | 46 +++++++--------- 12 files changed, 117 insertions(+), 120 deletions(-) diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py index e288c24e..45ed753e 100644 --- a/mopidy/frontends/mpd/actor.py +++ b/mopidy/frontends/mpd/actor.py @@ -5,7 +5,6 @@ import sys import pykka -from mopidy import settings from mopidy.core import CoreListener from mopidy.frontends.mpd import session from mopidy.utils import encoding, network, process @@ -16,17 +15,21 @@ logger = logging.getLogger('mopidy.frontends.mpd') class MpdFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super(MpdFrontend, self).__init__() - hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) - port = settings.MPD_SERVER_PORT + hostname = network.format_hostname(config['mpd']['hostname']) + port = config['mpd']['port'] # NOTE kwargs dict keys must be bytestrings to work on Python < 2.6.5 # See https://github.com/mopidy/mopidy/issues/302 for details. try: network.Server( hostname, port, - protocol=session.MpdSession, protocol_kwargs={b'core': core}, - max_connections=settings.MPD_SERVER_MAX_CONNECTIONS, - timeout=settings.MPD_SERVER_CONNECTION_TIMEOUT) + protocol=session.MpdSession, + protocol_kwargs={ + b'config': config, + b'core': core, + }, + max_connections=config['mpd']['max_connections'], + timeout=config['mpd']['connection_timeout']) except IOError as error: logger.error( 'MPD server startup failed: %s', diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 4f0001ac..dc665abc 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -5,7 +5,6 @@ import re import pykka -from mopidy import settings from mopidy.frontends.mpd import exceptions, protocol logger = logging.getLogger('mopidy.frontends.mpd.dispatcher') @@ -22,13 +21,15 @@ class MpdDispatcher(object): _noidle = re.compile(r'^noidle$') - def __init__(self, session=None, core=None): + def __init__(self, session=None, config=None, core=None): + self.config = config self.authenticated = False self.command_list_receiving = False self.command_list_ok = False self.command_list = [] self.command_list_index = None - self.context = MpdContext(self, session=session, core=core) + self.context = MpdContext( + self, session=session, config=config, core=core) def handle_request(self, request, current_command_list_index=None): """Dispatch incoming requests to the correct handler.""" @@ -82,7 +83,7 @@ class MpdDispatcher(object): def _authenticate_filter(self, request, response, filter_chain): if self.authenticated: return self._call_next_filter(request, response, filter_chain) - elif settings.MPD_SERVER_PASSWORD is None: + elif self.config['mpd']['password'] is None: self.authenticated = True return self._call_next_filter(request, response, filter_chain) else: @@ -223,6 +224,9 @@ class MpdContext(object): #: The current :class:`mopidy.frontends.mpd.MpdSession`. session = None + #: The Mopidy configuration. + config = None + #: The Mopidy core API. An instance of :class:`mopidy.core.Core`. core = None @@ -232,9 +236,10 @@ class MpdContext(object): #: The subsytems that we want to be notified about in idle mode. subscriptions = None - def __init__(self, dispatcher, session=None, core=None): + def __init__(self, dispatcher, session=None, config=None, core=None): self.dispatcher = dispatcher self.session = session + self.config = config self.core = core self.events = set() self.subscriptions = set() diff --git a/mopidy/frontends/mpd/protocol/connection.py b/mopidy/frontends/mpd/protocol/connection.py index f7898d21..44696705 100644 --- a/mopidy/frontends/mpd/protocol/connection.py +++ b/mopidy/frontends/mpd/protocol/connection.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from mopidy import settings from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import ( MpdPasswordError, MpdPermissionError) @@ -40,7 +39,7 @@ def password_(context, password): This is used for authentication with the server. ``PASSWORD`` is simply the plaintext password. """ - if password == settings.MPD_SERVER_PASSWORD: + if password == context.config['mpd']['password']: context.dispatcher.authenticated = True else: raise MpdPasswordError('incorrect password', command='password') diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index 8a5deecd..14173308 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -18,9 +18,10 @@ class MpdSession(network.LineProtocol): encoding = protocol.ENCODING delimiter = r'\r?\n' - def __init__(self, connection, core=None): + def __init__(self, connection, config=None, core=None): super(MpdSession, self).__init__(connection) - self.dispatcher = dispatcher.MpdDispatcher(session=self, core=core) + self.dispatcher = dispatcher.MpdDispatcher( + session=self, config=config, core=core) def on_start(self): logger.info('New MPD connection from [%s]:%s', self.host, self.port) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 15ca181d..d820b0e0 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -5,7 +5,6 @@ import re import shlex import urllib -from mopidy import settings from mopidy.frontends.mpd import protocol from mopidy.frontends.mpd.exceptions import MpdArgError from mopidy.models import TlTrack @@ -216,12 +215,14 @@ def query_from_mpd_search_format(mpd_query): return query -def tracks_to_tag_cache_format(tracks): +def tracks_to_tag_cache_format(tracks, music_path): """ Format list of tracks for output to MPD tag cache :param tracks: the tracks :type tracks: list of :class:`mopidy.models.Track` + :param music_path: the path to the music dir + :type music_path: string :rtype: list of lists of two-tuples """ result = [ @@ -231,14 +232,15 @@ def tracks_to_tag_cache_format(tracks): ('info_end',) ] tracks.sort(key=lambda t: t.uri) - _add_to_tag_cache(result, *tracks_to_directory_tree(tracks)) + folders, files = tracks_to_directory_tree(tracks, music_path) + _add_to_tag_cache(result, folders, files, music_path) return result -def _add_to_tag_cache(result, folders, files): - base_path = settings.LOCAL_MUSIC_PATH.encode('utf-8') +def _add_to_tag_cache(result, folders, files, music_path): + base_path = music_path.encode('utf-8') - for path, entry in folders.items(): + for path, (entry_folders, entry_files) in folders.items(): try: text_path = path.decode('utf-8') except UnicodeDecodeError: @@ -247,7 +249,7 @@ def _add_to_tag_cache(result, folders, files): result.append(('directory', text_path)) result.append(('mtime', get_mtime(os.path.join(base_path, path)))) result.append(('begin', name)) - _add_to_tag_cache(result, *entry) + _add_to_tag_cache(result, entry_folders, entry_files, music_path) result.append(('end', name)) result.append(('songList begin',)) @@ -273,7 +275,7 @@ def _add_to_tag_cache(result, folders, files): result.append(('songList end',)) -def tracks_to_directory_tree(tracks): +def tracks_to_directory_tree(tracks, music_path): directories = ({}, []) for track in tracks: @@ -282,8 +284,7 @@ def tracks_to_directory_tree(tracks): absolute_track_dir_path = os.path.dirname(uri_to_path(track.uri)) relative_track_dir_path = re.sub( - '^' + re.escape(settings.LOCAL_MUSIC_PATH), b'', - absolute_track_dir_path) + '^' + re.escape(music_path), b'', absolute_track_dir_path) for part in split_path(relative_track_dir_path): path = os.path.join(path, part) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 4b057774..8bc57349 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -42,6 +42,7 @@ from mopidy.utils import log, path, versioning def main(): options = parse_options() + config = {} # TODO Read config from new config system log.setup_root_logger() log.setup_console_logging(options.verbosity_level) @@ -67,7 +68,8 @@ def main(): logging.info('Done scanning; writing tag cache...') - for row in mpd_translator.tracks_to_tag_cache_format(tracks): + for row in mpd_translator.tracks_to_tag_cache_format( + tracks, config['mpd']['music_path']): if len(row) == 1: print ('%s' % row).encode('utf-8') else: diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py index 3c32cd32..35e18c3b 100644 --- a/tests/frontends/mpd/dispatcher_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -13,9 +13,14 @@ from tests import unittest class MpdDispatcherTest(unittest.TestCase): def setUp(self): + config = { + 'mpd': { + 'password': None, + } + } self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() - self.dispatcher = MpdDispatcher() + self.dispatcher = MpdDispatcher(config=config) def tearDown(self): pykka.ActorRegistry.stop_all() diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index 9d24c3fa..e62141ee 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -23,12 +23,20 @@ class MockConnection(mock.Mock): class BaseTestCase(unittest.TestCase): + def get_config(self): + return { + 'mpd': { + 'password': None, + } + } + def setUp(self): self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.connection = MockConnection() - self.session = session.MpdSession(self.connection, core=self.core) + self.session = session.MpdSession( + self.connection, config=self.get_config(), core=self.core) self.dispatcher = self.session.dispatcher self.context = self.dispatcher.context diff --git a/tests/frontends/mpd/protocol/authentication_test.py b/tests/frontends/mpd/protocol/authentication_test.py index 26b03f45..2597ddef 100644 --- a/tests/frontends/mpd/protocol/authentication_test.py +++ b/tests/frontends/mpd/protocol/authentication_test.py @@ -1,63 +1,56 @@ from __future__ import unicode_literals -from mopidy import settings - from tests.frontends.mpd import protocol -class AuthenticationTest(protocol.BaseTestCase): - def test_authentication_with_valid_password_is_accepted(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' +class AuthenticationActiveTest(protocol.BaseTestCase): + def get_config(self): + config = super(AuthenticationActiveTest, self).get_config() + config['mpd']['password'] = 'topsecret' + return config + def test_authentication_with_valid_password_is_accepted(self): self.sendRequest('password "topsecret"') self.assertTrue(self.dispatcher.authenticated) self.assertInResponse('OK') def test_authentication_with_invalid_password_is_not_accepted(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - self.sendRequest('password "secret"') self.assertFalse(self.dispatcher.authenticated) self.assertEqualResponse('ACK [3@0] {password} incorrect password') - def test_authentication_with_anything_when_password_check_turned_off(self): - settings.MPD_SERVER_PASSWORD = None - - self.sendRequest('any request at all') - self.assertTrue(self.dispatcher.authenticated) - self.assertEqualResponse('ACK [5@0] {} unknown command "any"') - def test_anything_when_not_authenticated_should_fail(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - self.sendRequest('any request at all') self.assertFalse(self.dispatcher.authenticated) self.assertEqualResponse( u'ACK [4@0] {any} you don\'t have permission for "any"') def test_close_is_allowed_without_authentication(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - self.sendRequest('close') self.assertFalse(self.dispatcher.authenticated) def test_commands_is_allowed_without_authentication(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - self.sendRequest('commands') self.assertFalse(self.dispatcher.authenticated) self.assertInResponse('OK') def test_notcommands_is_allowed_without_authentication(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - self.sendRequest('notcommands') self.assertFalse(self.dispatcher.authenticated) self.assertInResponse('OK') def test_ping_is_allowed_without_authentication(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - self.sendRequest('ping') self.assertFalse(self.dispatcher.authenticated) self.assertInResponse('OK') + + +class AuthenticationInactiveTest(protocol.BaseTestCase): + def test_authentication_with_anything_when_password_check_turned_off(self): + self.sendRequest('any request at all') + self.assertTrue(self.dispatcher.authenticated) + self.assertEqualResponse('ACK [5@0] {} unknown command "any"') + + def test_any_password_is_not_accepted_when_password_check_turned_off(self): + self.sendRequest('password "secret"') + self.assertEqualResponse('ACK [3@0] {password} incorrect password') diff --git a/tests/frontends/mpd/protocol/connection_test.py b/tests/frontends/mpd/protocol/connection_test.py index 840ce48f..01deb7a7 100644 --- a/tests/frontends/mpd/protocol/connection_test.py +++ b/tests/frontends/mpd/protocol/connection_test.py @@ -2,8 +2,6 @@ from __future__ import unicode_literals from mock import patch -from mopidy import settings - from tests.frontends.mpd import protocol @@ -26,21 +24,6 @@ class ConnectionHandlerTest(protocol.BaseTestCase): self.assertEqualResponse( 'ACK [4@0] {kill} you don\'t have permission for "kill"') - def test_valid_password_is_accepted(self): - settings.MPD_SERVER_PASSWORD = 'topsecret' - self.sendRequest('password "topsecret"') - self.assertEqualResponse('OK') - - def test_invalid_password_is_not_accepted(self): - settings.MPD_SERVER_PASSWORD = 'topsecret' - self.sendRequest('password "secret"') - self.assertEqualResponse('ACK [3@0] {password} incorrect password') - - def test_any_password_is_not_accepted_when_password_check_turned_off(self): - settings.MPD_SERVER_PASSWORD = None - self.sendRequest('password "secret"') - self.assertEqualResponse('ACK [3@0] {password} incorrect password') - def test_ping(self): self.sendRequest('ping') self.assertEqualResponse('OK') diff --git a/tests/frontends/mpd/protocol/reflection_test.py b/tests/frontends/mpd/protocol/reflection_test.py index f2720473..16f4579f 100644 --- a/tests/frontends/mpd/protocol/reflection_test.py +++ b/tests/frontends/mpd/protocol/reflection_test.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -from mopidy import settings - from tests.frontends.mpd import protocol @@ -29,19 +27,6 @@ class ReflectionHandlerTest(protocol.BaseTestCase): self.assertNotInResponse('command: sticker') self.assertInResponse('OK') - def test_commands_show_less_if_auth_required_and_not_authed(self): - settings.MPD_SERVER_PASSWORD = u'secret' - self.sendRequest('commands') - # Not requiring auth - self.assertInResponse('command: close') - self.assertInResponse('command: commands') - self.assertInResponse('command: notcommands') - self.assertInResponse('command: password') - self.assertInResponse('command: ping') - # Requiring auth - self.assertNotInResponse('command: play') - self.assertNotInResponse('command: status') - def test_decoders(self): self.sendRequest('decoders') self.assertInResponse('OK') @@ -53,8 +38,35 @@ class ReflectionHandlerTest(protocol.BaseTestCase): self.assertInResponse('command: kill') self.assertInResponse('OK') + def test_tagtypes(self): + self.sendRequest('tagtypes') + self.assertInResponse('OK') + + def test_urlhandlers(self): + self.sendRequest('urlhandlers') + self.assertInResponse('OK') + self.assertInResponse('handler: dummy') + + +class ReflectionWhenNotAuthedTest(protocol.BaseTestCase): + def get_config(self): + config = super(ReflectionWhenNotAuthedTest, self).get_config() + config['mpd']['password'] = 'topsecret' + return config + + def test_commands_show_less_if_auth_required_and_not_authed(self): + self.sendRequest('commands') + # Not requiring auth + self.assertInResponse('command: close') + self.assertInResponse('command: commands') + self.assertInResponse('command: notcommands') + self.assertInResponse('command: password') + self.assertInResponse('command: ping') + # Requiring auth + self.assertNotInResponse('command: play') + self.assertNotInResponse('command: status') + def test_notcommands_returns_more_if_auth_required_and_not_authed(self): - settings.MPD_SERVER_PASSWORD = u'secret' self.sendRequest('notcommands') # Not requiring auth self.assertNotInResponse('command: close') @@ -65,12 +77,3 @@ class ReflectionHandlerTest(protocol.BaseTestCase): # Requiring auth self.assertInResponse('command: play') self.assertInResponse('command: status') - - def test_tagtypes(self): - self.sendRequest('tagtypes') - self.assertInResponse('OK') - - def test_urlhandlers(self): - self.sendRequest('urlhandlers') - self.assertInResponse('OK') - self.assertInResponse('handler: dummy') diff --git a/tests/frontends/mpd/translator_test.py b/tests/frontends/mpd/translator_test.py index 088ae137..5df65c5c 100644 --- a/tests/frontends/mpd/translator_test.py +++ b/tests/frontends/mpd/translator_test.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import datetime import os -from mopidy import settings from mopidy.utils.path import mtime, uri_to_path from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, TlTrack, Playlist, Track @@ -24,11 +23,10 @@ class TrackMpdFormatTest(unittest.TestCase): ) def setUp(self): - settings.LOCAL_MUSIC_PATH = '/dir/subdir' + self.music_path = '/dir/subdir' mtime.set_fake_time(1234567) def tearDown(self): - settings.runtime.clear() mtime.undo_fake() def test_track_to_mpd_format_for_empty_track(self): @@ -137,15 +135,14 @@ class QueryFromMpdListFormatTest(unittest.TestCase): class TracksToTagCacheFormatTest(unittest.TestCase): def setUp(self): - settings.LOCAL_MUSIC_PATH = '/dir/subdir' + self.music_path = '/dir/subdir' mtime.set_fake_time(1234567) def tearDown(self): - settings.runtime.clear() mtime.undo_fake() def translate(self, track): - base_path = settings.LOCAL_MUSIC_PATH.encode('utf-8') + base_path = self.music_path.encode('utf-8') result = dict(translator.track_to_mpd_format(track)) result['file'] = uri_to_path(result['file'])[len(base_path) + 1:] result['key'] = os.path.basename(result['file']) @@ -177,11 +174,11 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.fail("Couldn't find end %s in result" % directory) def test_empty_tag_cache_has_header(self): - result = translator.tracks_to_tag_cache_format([]) + result = translator.tracks_to_tag_cache_format([], self.music_path) result = self.consume_headers(result) def test_empty_tag_cache_has_song_list(self): - result = translator.tracks_to_tag_cache_format([]) + result = translator.tracks_to_tag_cache_format([], self.music_path) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -190,12 +187,12 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_header(self): track = Track(uri='file:///dir/subdir/song.mp3') - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) def test_tag_cache_has_song_list(self): track = Track(uri='file:///dir/subdir/song.mp3') - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -205,7 +202,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_formated_track(self): track = Track(uri='file:///dir/subdir/song.mp3') formated = self.translate(track) - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -216,7 +213,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_formated_track_with_key_and_mtime(self): track = Track(uri='file:///dir/subdir/song.mp3') formated = self.translate(track) - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -227,7 +224,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_directories(self): track = Track(uri='file:///dir/subdir/folder/song.mp3') formated = self.translate(track) - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) folder, result = self.consume_directory(result) @@ -241,7 +238,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_diretory_header_is_right(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) folder, result = self.consume_directory(result) @@ -253,7 +250,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_sub_directories(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') formated = self.translate(track) - result = translator.tracks_to_tag_cache_format([track]) + result = translator.tracks_to_tag_cache_format([track], self.music_path) result = self.consume_headers(result) @@ -281,7 +278,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): formated.extend(self.translate(tracks[0])) formated.extend(self.translate(tracks[1])) - result = translator.tracks_to_tag_cache_format(tracks) + result = translator.tracks_to_tag_cache_format(tracks, self.music_path) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -299,7 +296,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): formated.append(self.translate(tracks[0])) formated.append(self.translate(tracks[1])) - result = translator.tracks_to_tag_cache_format(tracks) + result = translator.tracks_to_tag_cache_format(tracks, self.music_path) result = self.consume_headers(result) folder, result = self.consume_directory(result) @@ -315,13 +312,10 @@ class TracksToTagCacheFormatTest(unittest.TestCase): class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): - settings.LOCAL_MUSIC_PATH = '/root/' - - def tearDown(self): - settings.runtime.clear() + self.music_path = '/root/' def test_no_tracks_gives_emtpy_tree(self): - tree = translator.tracks_to_directory_tree([]) + tree = translator.tracks_to_directory_tree([], self.music_path) self.assertEqual(tree, ({}, [])) def test_top_level_files(self): @@ -330,18 +324,18 @@ class TracksToDirectoryTreeTest(unittest.TestCase): Track(uri='file:///root/file2.mp3'), Track(uri='file:///root/file3.mp3'), ] - tree = translator.tracks_to_directory_tree(tracks) + tree = translator.tracks_to_directory_tree(tracks, self.music_path) self.assertEqual(tree, ({}, tracks)) def test_single_file_in_subdir(self): tracks = [Track(uri='file:///root/dir/file1.mp3')] - tree = translator.tracks_to_directory_tree(tracks) + tree = translator.tracks_to_directory_tree(tracks, self.music_path) expected = ({'dir': ({}, tracks)}, []) self.assertEqual(tree, expected) def test_single_file_in_sub_subdir(self): tracks = [Track(uri='file:///root/dir1/dir2/file1.mp3')] - tree = translator.tracks_to_directory_tree(tracks) + tree = translator.tracks_to_directory_tree(tracks, self.music_path) expected = ({'dir1': ({'dir1/dir2': ({}, tracks)}, [])}, []) self.assertEqual(tree, expected) @@ -353,7 +347,7 @@ class TracksToDirectoryTreeTest(unittest.TestCase): Track(uri='file:///root/dir2/file4.mp3'), Track(uri='file:///root/dir2/sub/file5.mp3'), ] - tree = translator.tracks_to_directory_tree(tracks) + tree = translator.tracks_to_directory_tree(tracks, self.music_path) expected = ( { 'dir1': ({}, [tracks[1], tracks[2]]), From 90efbb6be74d39640ded918230497b316d784ead Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 5 Apr 2013 22:49:33 +0200 Subject: [PATCH 07/45] config: Add a Path config value and an ExpandedPath wrapper. Allows us to easily use expanded paths, without losing the original value for display and storage. In theory we could be using same trick for passwords. --- mopidy/utils/config.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/config.py b/mopidy/utils/config.py index 1a3127b5..675d92af 100644 --- a/mopidy/utils/config.py +++ b/mopidy/utils/config.py @@ -5,6 +5,7 @@ import re import socket from mopidy import exceptions +from mopidy.utils import path def validate_required(value, required): @@ -93,7 +94,7 @@ class ConfigValue(object): class String(ConfigValue): """String values. - Supports: optional choices and secret. + Supports: optional, choices and secret. """ def deserialize(self, value): value = value.strip() @@ -209,6 +210,34 @@ class Port(Integer): self.maximum = 2 ** 16 - 1 +class ExpandedPath(bytes): + def __new__(self, value): + expanded = path.expand_path(value) + return super(ExpandedPath, self).__new__(self, expanded) + + def __init__(self, value): + self.original = value + + +class Path(ConfigValue): + """File system path that will be expanded with mopidy.utils.path.expand_path + + Supports: optional, choices and secret. + """ + def deserialize(self, value): + value = value.strip() + validate_required(value, not self.optional) + validate_choice(value, self.choices) + if not value: + return None + return ExpandedPath(value) + + def serialize(self, value): + if isinstance(value, ExpandedPath): + return value.original + return value + + class ConfigSchema(object): """Logical group of config values that correspond to a config section. @@ -231,6 +260,8 @@ class ConfigSchema(object): return self._schema[key] def format(self, name, values): + # TODO: should the output be encoded utf-8 since we use that in + # serialize for strings? lines = ['[%s]' % name] for key in self._order: value = values.get(key) From 36266064d324dd6c99a1610084ecb3c165045dc0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 5 Apr 2013 22:50:31 +0200 Subject: [PATCH 08/45] config: Switch all paths and files to use the Path in schemas. Also renames static_dir to static_path for better consistency. --- mopidy/backends/local/__init__.py | 6 +++--- mopidy/backends/spotify/__init__.py | 2 +- mopidy/config.py | 2 +- mopidy/frontends/http/__init__.py | 4 ++-- mopidy/frontends/mpris/__init__.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 17fd659e..54a1c7a4 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -55,9 +55,9 @@ class Extension(ext.Extension): def get_config_schema(self): schema = config.ExtensionConfigSchema() - schema['music_path'] = config.String() - schema['playlist_path'] = config.String() - schema['tag_cache_file'] = config.String() + schema['music_path'] = config.Path() + schema['playlist_path'] = config.Path() + schema['tag_cache_file'] = config.Path() return schema def validate_environment(self): diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index b03849eb..6de15aab 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -80,7 +80,7 @@ class Extension(ext.Extension): schema['password'] = config.String(secret=True) schema['bitrate'] = config.Integer(choices=(96, 160, 320)) schema['timeout'] = config.Integer(minimum=0) - schema['cache_path'] = config.String() + schema['cache_path'] = config.Path() schema['proxy_hostname'] = config.Hostname(optional=True) schema['proxy_username'] = config.String(optional=True) schema['proxy_password'] = config.String(optional=True, secret=True) diff --git a/mopidy/config.py b/mopidy/config.py index 85feffcc..4335f877 100644 --- a/mopidy/config.py +++ b/mopidy/config.py @@ -22,7 +22,7 @@ config_schemas = {} # TODO: use ordered dict? config_schemas['logging'] = config.ConfigSchema() config_schemas['logging']['console_format'] = config.String() config_schemas['logging']['debug_format'] = config.String() -config_schemas['logging']['debug_file'] = config.String() +config_schemas['logging']['debug_file'] = config.Path() config_schemas['logging.levels'] = config.LogLevelConfigSchema() diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index d588a376..4ca1d9b4 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -31,7 +31,7 @@ port = 6680 # Change this to have Mopidy serve e.g. files for your JavaScript client. # "/mopidy" will continue to work as usual even if you change this setting. # -static_dir = +static_path = [logging.levels] cherrypy = warning @@ -533,7 +533,7 @@ class Extension(ext.Extension): schema = config.ExtensionConfigSchema() schema['hostname'] = config.Hostname() schema['port'] = config.Port() - schema['static_dir'] = config.String(optional=True) + schema['static_path'] = config.Path(optional=True) return schema def validate_environment(self): diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py index 79806c47..82d15e9d 100644 --- a/mopidy/frontends/mpris/__init__.py +++ b/mopidy/frontends/mpris/__init__.py @@ -79,7 +79,7 @@ class Extension(ext.Extension): def get_config_schema(self): schema = config.ExtensionConfigSchema() - schema['desktop_file'] = config.String() + schema['desktop_file'] = config.Path() return schema def validate_environment(self): From 1ca6ffc6fb9ac0649cea18464c3cc295a82b7aeb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 6 Apr 2013 23:47:03 +0200 Subject: [PATCH 09/45] mpd: Fix two test failures --- tests/frontends/mpd/translator_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/frontends/mpd/translator_test.py b/tests/frontends/mpd/translator_test.py index 5df65c5c..828acf1a 100644 --- a/tests/frontends/mpd/translator_test.py +++ b/tests/frontends/mpd/translator_test.py @@ -312,7 +312,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): - self.music_path = '/root/' + self.music_path = '/root' def test_no_tracks_gives_emtpy_tree(self): tree = translator.tracks_to_directory_tree([], self.music_path) From 42707f50df0bec18a1458a0fbf1020c522275b92 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Apr 2013 00:04:35 +0200 Subject: [PATCH 10/45] audio: Use new config system --- mopidy/audio/actor.py | 43 ++++++++++++++++++++++--------------- mopidy/audio/mixers/auto.py | 4 ++-- mopidy/audio/mixers/fake.py | 4 ++-- mopidy/audio/mixers/nad.py | 16 ++++++-------- tests/audio/actor_test.py | 24 +++++++++++++++------ 5 files changed, 54 insertions(+), 37 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 42dee084..5d92f3c4 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -9,7 +9,6 @@ import logging import pykka -from mopidy import settings from mopidy.utils import process from . import mixers, utils @@ -28,11 +27,14 @@ class Audio(pykka.ThreadingActor): """ Audio output through `GStreamer `_. - **Settings:** + **Default config:** - - :attr:`mopidy.settings.OUTPUT` - - :attr:`mopidy.settings.MIXER` - - :attr:`mopidy.settings.MIXER_TRACK` + .. code-block:: ini + + [audio] + mixer = autoaudiomixer + mixer_track = + output = autoaudiosink """ #: The GStreamer state mapped to :class:`mopidy.audio.PlaybackState` @@ -41,6 +43,8 @@ class Audio(pykka.ThreadingActor): def __init__(self, config): super(Audio, self).__init__() + self._config = config + self._playbin = None self._signal_ids = {} # {(element, event): signal_id} @@ -143,47 +147,51 @@ class Audio(pykka.ThreadingActor): self._playbin.set_state(gst.STATE_NULL) def _setup_output(self): + output_desc = self._config['audio']['output'] try: output = gst.parse_bin_from_description( - settings.OUTPUT, ghost_unconnected_pads=True) + output_desc, ghost_unconnected_pads=True) self._playbin.set_property('audio-sink', output) - logger.info('Audio output set to "%s"', settings.OUTPUT) + logger.info('Audio output set to "%s"', output_desc) except gobject.GError as ex: logger.error( - 'Failed to create audio output "%s": %s', settings.OUTPUT, ex) + 'Failed to create audio output "%s": %s', output_desc, ex) process.exit_process() def _setup_mixer(self): - if not settings.MIXER: + mixer_desc = self._config['audio']['mixer'] + track_desc = self._config['audio']['mixer_track'] + + if mixer_desc is None: logger.info('Not setting up audio mixer') return - if settings.MIXER == 'software': + if mixer_desc == 'software': self._software_mixing = True logger.info('Audio mixer is using software mixing') return try: mixerbin = gst.parse_bin_from_description( - settings.MIXER, ghost_unconnected_pads=False) + mixer_desc, ghost_unconnected_pads=False) except gobject.GError as ex: logger.warning( - 'Failed to create audio mixer "%s": %s', settings.MIXER, ex) + 'Failed to create audio mixer "%s": %s', mixer_desc, ex) return # We assume that the bin will contain a single mixer. mixer = mixerbin.get_by_interface(b'GstMixer') if not mixer: logger.warning( - 'Did not find any audio mixers in "%s"', settings.MIXER) + 'Did not find any audio mixers in "%s"', mixer_desc) return if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: logger.warning( - 'Setting audio mixer "%s" to READY failed', settings.MIXER) + 'Setting audio mixer "%s" to READY failed', mixer_desc) return - track = self._select_mixer_track(mixer, settings.MIXER_TRACK) + track = self._select_mixer_track(mixer, track_desc) if not track: logger.warning('Could not find usable audio mixer track') return @@ -198,8 +206,9 @@ class Audio(pykka.ThreadingActor): def _select_mixer_track(self, mixer, track_label): # Ignore tracks without volumes, then look for track with - # label == settings.MIXER_TRACK, otherwise fallback to first usable - # track hoping the mixer gave them to us in a sensible order. + # label equal to the audio/mixer_track config value, otherwise fallback + # to first usable track hoping the mixer gave them to us in a sensible + # order. usable_tracks = [] for track in mixer.list_tracks(): diff --git a/mopidy/audio/mixers/auto.py b/mopidy/audio/mixers/auto.py index 96359da1..b24bcf4c 100644 --- a/mopidy/audio/mixers/auto.py +++ b/mopidy/audio/mixers/auto.py @@ -6,9 +6,9 @@ This is Mopidy's default mixer. None -**Settings** +**Configuration** -If this wasn't the default, you would set :attr:`mopidy.settings.MIXER` to +If this wasn't the default, you would set the ``audio/mixer`` config value to ``autoaudiomixer`` to use this mixer. """ diff --git a/mopidy/audio/mixers/fake.py b/mopidy/audio/mixers/fake.py index 738491b5..05e86923 100644 --- a/mopidy/audio/mixers/fake.py +++ b/mopidy/audio/mixers/fake.py @@ -4,9 +4,9 @@ None -**Settings** +**Configuration** -Set :attr:`mopidy.settings.MIXER` to ``fakemixer`` to use this mixer. +Set the ``audio/mixer`` config value to ``fakemixer`` to use this mixer. """ from __future__ import unicode_literals diff --git a/mopidy/audio/mixers/nad.py b/mopidy/audio/mixers/nad.py index 8481de55..058333d1 100644 --- a/mopidy/audio/mixers/nad.py +++ b/mopidy/audio/mixers/nad.py @@ -7,10 +7,10 @@ serial cable. .. literalinclude:: ../../../../requirements/external_mixers.txt -**Settings** +**Configuration** -Set :attr:`mopidy.settings.MIXER` to ``nadmixer`` to use it. You probably also -needs to add some properties to the ``MIXER`` setting. +Set the ``audio/mixer`` config value to ``nadmixer`` to use it. You probably +also needs to add some properties to the ``audio/mixer`` config value. Supported properties includes: @@ -34,15 +34,13 @@ Supported properties includes: Configuration examples:: # Minimum configuration, if the amplifier is available at /dev/ttyUSB0 - MIXER = u'nadmixer' + mixer = nadmixer # Minimum configuration, if the amplifier is available elsewhere - MIXER = u'nadmixer port=/dev/ttyUSB3' + mixer = nadmixer port=/dev/ttyUSB3 # Full configuration - MIXER = ( - u'nadmixer port=/dev/ttyUSB0 ' - u'source=aux speakers-a=on speakers-b=off') + mixer = nadmixer port=/dev/ttyUSB0 source=aux speakers-a=on speakers-b=off """ from __future__ import unicode_literals @@ -132,7 +130,7 @@ class NadTalker(pykka.ThreadingActor): calibrating the NAD amplifier's volume. """ - # Serial link settings + # Serial link config BAUDRATE = 115200 BYTESIZE = 8 PARITY = 'N' diff --git a/tests/audio/actor_test.py b/tests/audio/actor_test.py index 51786adb..a40b0572 100644 --- a/tests/audio/actor_test.py +++ b/tests/audio/actor_test.py @@ -6,7 +6,7 @@ import gst import pykka -from mopidy import audio, settings +from mopidy import audio from mopidy.utils.path import path_to_uri from tests import unittest, path_to_data_dir @@ -14,14 +14,18 @@ from tests import unittest, path_to_data_dir class AudioTest(unittest.TestCase): def setUp(self): - settings.MIXER = 'fakemixer track_max_volume=65536' - settings.OUTPUT = 'fakesink' + config = { + 'audio': { + 'mixer': 'fakemixer track_max_volume=65536', + 'mixer_track': None, + 'output': 'fakesink', + } + } self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) - self.audio = audio.Audio.start(config=None).proxy() + self.audio = audio.Audio.start(config=config).proxy() def tearDown(self): pykka.ActorRegistry.stop_all() - settings.runtime.clear() def prepare_uri(self, uri): self.audio.prepare_change() @@ -59,8 +63,14 @@ class AudioTest(unittest.TestCase): self.assertEqual(value, self.audio.get_volume().get()) def test_set_volume_with_mixer_max_below_100(self): - settings.MIXER = 'fakemixer track_max_volume=40' - self.audio = audio.Audio.start(config=None).proxy() + config = { + 'audio': { + 'mixer': 'fakemixer track_max_volume=40', + 'mixer_track': None, + 'output': 'fakesink', + } + } + self.audio = audio.Audio.start(config=config).proxy() for value in range(0, 101): self.assertTrue(self.audio.set_volume(value).get()) From 2276130758519b4240fb6004d74a1b98dab0c26f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Apr 2013 00:33:34 +0200 Subject: [PATCH 11/45] main: Use new config system --- mopidy/__main__.py | 45 +++++++++++++++++++++----------------------- mopidy/utils/path.py | 2 ++ 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 3783d150..94e245d0 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -28,7 +28,7 @@ sys.path.insert( 0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) -from mopidy import exceptions, settings +from mopidy import exceptions from mopidy.audio import Audio from mopidy.config import default_config, config_schemas from mopidy.core import Core @@ -48,8 +48,10 @@ def main(): config_files = options.config.split(':') config_overrides = options.overrides + extensions = [] # Make sure it is defined before the finally block + try: - extensions = [] # Make sure it is defined before the finally block + create_file_structures() logging_config = load_config(config_files, config_overrides) log.setup_logging( logging_config, options.verbosity_level, options.save_debug_log) @@ -58,8 +60,7 @@ def main(): extensions = filter_enabled_extensions(raw_config, extensions) config = validate_config(raw_config, config_schemas, extensions) log.setup_log_levels(config) - check_old_folders() - setup_settings() + check_old_locations() # Anything that wants to exit after this point must use # mopidy.utils.process.exit_process as actors have been started. @@ -68,8 +69,6 @@ def main(): core = setup_core(audio, backends) setup_frontends(config, extensions, core) loop.run() - except exceptions.SettingsError as ex: - logger.error(ex.message) except KeyboardInterrupt: logger.info('Interrupted. Exiting...') except Exception as ex: @@ -167,17 +166,20 @@ def show_config_callback(option, opt, value, parser): sys.exit(0) -def check_old_folders(): - # TODO: add old settings and pre extension storage locations? - old_settings_folder = os.path.expanduser('~/.mopidy') +def check_old_locations(): + dot_mopidy_dir = path.expand_path('~/.mopidy') + if os.path.isdir(dot_mopidy_dir): + logger.warning( + 'Old Mopidy dot dir found at %s. Please migrate your config to ' + 'the ini-file based config format. See release notes for further ' + 'instructions.', dot_mopidy_dir) - if not os.path.isdir(old_settings_folder): - return - - logger.warning( - 'Old settings folder found at %s, settings.py should be moved ' - 'to %s, any cache data should be deleted. See release notes for ' - 'further instructions.', old_settings_folder, path.SETTINGS_PATH) + old_settings_file = path.expand_path('$XDG_CONFIG_DIR/mopidy/settings.py') + if os.path.isfile(old_settings_file): + logger.warning( + 'Old Mopidy settings file found at %s. Please migrate your ' + 'config to the ini-file based config format. See release notes ' + 'for further instructions.', old_settings_file) def load_extensions(): @@ -306,15 +308,10 @@ def validate_config(raw_config, schemas, extensions=None): return config -def setup_settings(): - path.get_or_create_folder(path.SETTINGS_PATH) +def create_file_structures(): path.get_or_create_folder(path.DATA_PATH) - path.get_or_create_file(path.SETTINGS_FILE) - try: - settings.validate() - except exceptions.SettingsError as ex: - logger.error(ex.message) - sys.exit(1) + path.get_or_create_folder(path.CONFIG_PATH) + path.get_or_create_file(path.CONFIG_FILE) def setup_audio(config): diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 4e5a66cd..88b8be3e 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -26,6 +26,8 @@ XDG_DIRS = { 'XDG_MUSIC_DIR': XDG_MUSIC_DIR, } DATA_PATH = os.path.join(unicode(XDG_DATA_DIR), 'mopidy') +CONFIG_PATH = os.path.join(unicode(XDG_CONFIG_DIR), 'mopidy') +CONFIG_FILE = os.path.join(unicode(CONFIG_PATH), 'mopidy.conf') SETTINGS_PATH = os.path.join(unicode(XDG_CONFIG_DIR), 'mopidy') SETTINGS_FILE = os.path.join(unicode(SETTINGS_PATH), 'settings.py') From e92a7628f25a151f3f83fd0b90da7d590cdc757e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Apr 2013 00:36:41 +0200 Subject: [PATCH 12/45] settings: Remove SettingsProxy at mopidy.settings --- mopidy/__init__.py | 5 ----- tests/__init__.py | 5 ----- tests/frontends/mpd/protocol/__init__.py | 3 +-- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 416f4fbf..fc2b611b 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -24,8 +24,3 @@ warnings.filterwarnings('ignore', 'could not open display') __version__ = '0.13.0' - - -from mopidy import settings as default_settings_module -from mopidy.utils.settings import SettingsProxy -settings = SettingsProxy(default_settings_module) diff --git a/tests/__init__.py b/tests/__init__.py index 7f7a9c36..b4e1d283 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -8,11 +8,6 @@ if sys.version_info < (2, 7): else: import unittest # noqa -from mopidy import settings - -# Nuke any local settings to ensure same test env all over -settings.local.clear() - def path_to_data_dir(name): path = os.path.dirname(__file__) diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index e62141ee..21ba1c25 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import mock import pykka -from mopidy import core, settings +from mopidy import core from mopidy.backends import dummy from mopidy.frontends.mpd import session @@ -42,7 +42,6 @@ class BaseTestCase(unittest.TestCase): def tearDown(self): pykka.ActorRegistry.stop_all() - settings.runtime.clear() def sendRequest(self, request): self.connection.response = [] From 777993583f4d7faef3b93c9094fbf5fd86311670 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Apr 2013 00:39:26 +0200 Subject: [PATCH 13/45] settings: Remove settings module --- docs/settings.rst | 4 +- mopidy/settings.py | 285 --------------------------------------------- 2 files changed, 1 insertion(+), 288 deletions(-) delete mode 100644 mopidy/settings.py diff --git a/docs/settings.rst b/docs/settings.rst index 4c9acd96..e16cfbfd 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -218,6 +218,4 @@ used at the same time without any danger of naming collisions. Available settings ================== -.. automodule:: mopidy.settings - :synopsis: Available settings and their default values - :members: +.. note:: TODO: Document config values of the new config system diff --git a/mopidy/settings.py b/mopidy/settings.py deleted file mode 100644 index cde6430a..00000000 --- a/mopidy/settings.py +++ /dev/null @@ -1,285 +0,0 @@ -""" -All available settings and their default values. - -.. warning:: - - Do *not* change settings directly in :mod:`mopidy.settings`. Instead, add a - file called ``~/.config/mopidy/settings.py`` and redefine settings there. -""" - -from __future__ import unicode_literals - -#: The log format used for informational logging. -#: -#: See http://docs.python.org/2/library/logging.html#formatter-objects for -#: details on the format. -CONSOLE_LOG_FORMAT = '%(levelname)-8s %(message)s' - -#: The log format used for debug logging. -#: -#: See http://docs.python.org/library/logging.html#formatter-objects for -#: details on the format. -DEBUG_LOG_FORMAT = '%(levelname)-8s %(asctime)s' + \ - ' [%(process)d:%(threadName)s] %(name)s\n %(message)s' - -#: The file to dump debug log data to when Mopidy is run with the -#: :option:`--save-debug-log` option. -#: -#: Default:: -#: -#: DEBUG_LOG_FILENAME = u'mopidy.log' -DEBUG_LOG_FILENAME = 'mopidy.log' - -#: Location of the Mopidy .desktop file. -#: -#: Used by :mod:`mopidy.frontends.mpris`. -#: -#: Default:: -#: -#: DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop' -DESKTOP_FILE = '/usr/share/applications/mopidy.desktop' - -#: Which address Mopidy's HTTP server should bind to. -#: -#: Used by :mod:`mopidy.frontends.http`. -#: -#: Examples: -#: -#: ``127.0.0.1`` -#: Listens only on the IPv4 loopback interface. Default. -#: ``::1`` -#: Listens only on the IPv6 loopback interface. -#: ``0.0.0.0`` -#: Listens on all IPv4 interfaces. -#: ``::`` -#: Listens on all interfaces, both IPv4 and IPv6. -HTTP_SERVER_HOSTNAME = u'127.0.0.1' - -#: Which TCP port Mopidy's HTTP server should listen to. -#: -#: Used by :mod:`mopidy.frontends.http`. -#: -#: Default: 6680 -HTTP_SERVER_PORT = 6680 - -#: Which directory Mopidy's HTTP server should serve at ``/``. -#: -#: Change this to have Mopidy serve e.g. files for your JavaScript client. -#: ``/mopidy`` will continue to work as usual even if you change this setting. -#: -#: Used by :mod:`mopidy.frontends.http`. -#: -#: Default: None -HTTP_SERVER_STATIC_DIR = None - -#: Your `Last.fm `_ username. -#: -#: Used by :mod:`mopidy.frontends.lastfm`. -LASTFM_USERNAME = '' - -#: Your `Last.fm `_ password. -#: -#: Used by :mod:`mopidy.frontends.lastfm`. -LASTFM_PASSWORD = '' - -#: Path to folder with local music. -#: -#: Used by :mod:`mopidy.backends.local`. -#: -#: Default:: -#: -#: LOCAL_MUSIC_PATH = u'$XDG_MUSIC_DIR' -LOCAL_MUSIC_PATH = '$XDG_MUSIC_DIR' - -#: Path to playlist folder with m3u files for local music. -#: -#: Used by :mod:`mopidy.backends.local`. -#: -#: Default:: -#: -#: LOCAL_PLAYLIST_PATH = u'$XDG_DATA_DIR/mopidy/playlists' -LOCAL_PLAYLIST_PATH = '$XDG_DATA_DIR/mopidy/playlists' - -#: Path to tag cache for local music. -#: -#: Used by :mod:`mopidy.backends.local`. -#: -#: Default:: -#: -#: LOCAL_TAG_CACHE_FILE = u'$XDG_DATA_DIR/mopidy/tag_cache' -LOCAL_TAG_CACHE_FILE = '$XDG_DATA_DIR/mopidy/tag_cache' - -#: Audio mixer to use. -#: -#: Expects a GStreamer mixer to use, typical values are: -#: ``alsamixer``, ``pulsemixer``, ``ossmixer``, and ``oss4mixer``. -#: -#: Setting this to :class:`None` turns off volume control. ``software`` -#: can be used to force software mixing in the application. -#: -#: Default:: -#: -#: MIXER = u'autoaudiomixer' -MIXER = 'autoaudiomixer' - -#: Audio mixer track to use. -#: -#: Name of the mixer track to use. If this is not set we will try to find the -#: master output track. As an example, using ``alsamixer`` you would -#: typically set this to ``Master`` or ``PCM``. -#: -#: Default:: -#: -#: MIXER_TRACK = None -MIXER_TRACK = None - -#: Number of seconds an MPD client can stay inactive before the connection is -#: closed by the server. -#: -#: Used by :mod:`mopidy.frontends.mpd`. -#: -#: Default:: -#: -#: MPD_SERVER_CONNECTION_TIMEOUT = 60 -MPD_SERVER_CONNECTION_TIMEOUT = 60 - -#: Which address Mopidy's MPD server should bind to. -#: -#: Used by :mod:`mopidy.frontends.mpd`. -#: -#: Examples: -#: -#: ``127.0.0.1`` -#: Listens only on the IPv4 loopback interface. Default. -#: ``::1`` -#: Listens only on the IPv6 loopback interface. -#: ``0.0.0.0`` -#: Listens on all IPv4 interfaces. -#: ``::`` -#: Listens on all interfaces, both IPv4 and IPv6. -MPD_SERVER_HOSTNAME = '127.0.0.1' - -#: Which TCP port Mopidy's MPD server should listen to. -#: -#: Used by :mod:`mopidy.frontends.mpd`. -#: -#: Default: 6600 -MPD_SERVER_PORT = 6600 - -#: The password required for connecting to the MPD server. -#: -#: Used by :mod:`mopidy.frontends.mpd`. -#: -#: Default: :class:`None`, which means no password required. -MPD_SERVER_PASSWORD = None - -#: The maximum number of concurrent connections the MPD server will accept. -#: -#: Used by :mod:`mopidy.frontends.mpd`. -#: -#: Default: 20 -MPD_SERVER_MAX_CONNECTIONS = 20 - -#: Audio output to use. -#: -#: Expects a GStreamer sink. Typical values are ``autoaudiosink``, -#: ``alsasink``, ``osssink``, ``oss4sink``, ``pulsesink``, and ``shout2send``, -#: and additional arguments specific to each sink. -#: -#: Default:: -#: -#: OUTPUT = u'autoaudiosink' -OUTPUT = 'autoaudiosink' - -#: Path to the Spotify cache. -#: -#: Used by :mod:`mopidy.backends.spotify`. -#: -#: Default:: -#: -#: SPOTIFY_CACHE_PATH = u'$XDG_CACHE_DIR/mopidy/spotify' -SPOTIFY_CACHE_PATH = '$XDG_CACHE_DIR/mopidy/spotify' - -#: Your Spotify Premium username. -#: -#: Used by :mod:`mopidy.backends.spotify`. -SPOTIFY_USERNAME = '' - -#: Your Spotify Premium password. -#: -#: Used by :mod:`mopidy.backends.spotify`. -SPOTIFY_PASSWORD = '' - -#: Spotify preferred bitrate. -#: -#: Available values are 96, 160, and 320. -#: -#: Used by :mod:`mopidy.backends.spotify`. -#: -#: Default:: -#: -#: SPOTIFY_BITRATE = 160 -SPOTIFY_BITRATE = 160 - -#: Spotify proxy host. -#: -#: Used by :mod:`mopidy.backends.spotify`. -#: -#: Example:: -#: -#: SPOTIFY_PROXY_HOST = u'protocol://host:port' -#: -#: Default:: -#: -#: SPOTIFY_PROXY_HOST = None -SPOTIFY_PROXY_HOST = None - -#: Spotify proxy username. -#: -#: Used by :mod:`mopidy.backends.spotify`. -#: -#: Default:: -#: -#: SPOTIFY_PROXY_USERNAME = None -SPOTIFY_PROXY_USERNAME = None - -#: Spotify proxy password. -#: -#: Used by :mod:`mopidy.backends.spotify`. -#: -#: Default:: -#: -#: SPOTIFY_PROXY_PASSWORD = None -SPOTIFY_PROXY_PASSWORD = None - -#: Max number of seconds to wait for Spotify operations to complete. -#: -#: Used by :mod:`mopidy.backends.spotify`. -#: -#: Default:: -#: -#: SPOTIFY_TIMEOUT = 10 -SPOTIFY_TIMEOUT = 10 - -#: Whitelist of URIs to support streaming from. -#: -#: Used by :mod:`mopidy.backends.stream`. -#: -#: Default:: -#: -#: STREAM_PROTOCOLS = ( -#: u'http', -#: u'https', -#: u'mms', -#: u'rtmp', -#: u'rtmps', -#: u'rtsp', -#: ) -STREAM_PROTOCOLS = ( - 'http', - 'https', - 'mms', - 'rtmp', - 'rtmps', - 'rtsp', -) From 63b7260c01e0d3ed68bad49932112c6bbb0f4103 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Apr 2013 00:41:09 +0200 Subject: [PATCH 14/45] settings: Remove settings utils --- mopidy/utils/settings.py | 173 ----------------------------------- tests/utils/settings_test.py | 150 ------------------------------ 2 files changed, 323 deletions(-) delete mode 100644 mopidy/utils/settings.py delete mode 100644 tests/utils/settings_test.py diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py deleted file mode 100644 index f903a70d..00000000 --- a/mopidy/utils/settings.py +++ /dev/null @@ -1,173 +0,0 @@ -# Absolute import needed to import ~/.config/mopidy/settings.py and not -# ourselves -from __future__ import absolute_import, unicode_literals - -import copy -import getpass -import logging -import os -import pprint -import sys - -from mopidy import exceptions -from mopidy.utils import formatting, path - -logger = logging.getLogger('mopidy.utils.settings') - - -class SettingsProxy(object): - def __init__(self, default_settings_module): - self.default = self._get_settings_dict_from_module( - default_settings_module) - self.local = self._get_local_settings() - self.runtime = {} - - def _get_local_settings(self): - if not os.path.isfile(path.SETTINGS_FILE): - return {} - sys.path.insert(0, path.SETTINGS_PATH) - # pylint: disable = F0401 - import settings as local_settings_module - # pylint: enable = F0401 - return self._get_settings_dict_from_module(local_settings_module) - - def _get_settings_dict_from_module(self, module): - settings = filter( - lambda (key, value): self._is_setting(key), - module.__dict__.iteritems()) - return dict(settings) - - def _is_setting(self, name): - return name.isupper() - - @property - def current(self): - current = copy.copy(self.default) - current.update(self.local) - current.update(self.runtime) - return current - - def __getattr__(self, attr): - if not self._is_setting(attr): - return - - current = self.current # bind locally to avoid copying+updates - if attr not in current: - raise exceptions.SettingsError('Setting "%s" is not set.' % attr) - - value = current[attr] - if isinstance(value, basestring) and len(value) == 0: - raise exceptions.SettingsError('Setting "%s" is empty.' % attr) - if not value: - return value - if attr.endswith('_PATH') or attr.endswith('_FILE'): - value = path.expand_path(value) - return value - - def __setattr__(self, attr, value): - if self._is_setting(attr): - self.runtime[attr] = value - else: - super(SettingsProxy, self).__setattr__(attr, value) - - def validate(self): - if self.get_errors(): - logger.error( - 'Settings validation errors: %s', - formatting.indent(self.get_errors_as_string())) - raise exceptions.SettingsError('Settings validation failed.') - - def _read_from_stdin(self, prompt): - if '_PASSWORD' in prompt: - return ( - getpass.getpass(prompt) - .decode(sys.stdin.encoding, 'ignore')) - else: - sys.stdout.write(prompt) - return ( - sys.stdin.readline().strip() - .decode(sys.stdin.encoding, 'ignore')) - - def get_errors(self): - return validate_settings(self.default, self.local) - - def get_errors_as_string(self): - lines = [] - for (setting, error) in self.get_errors().iteritems(): - lines.append('%s: %s' % (setting, error)) - return '\n'.join(lines) - - -def validate_settings(defaults, settings): - """ - Checks the settings for both errors like misspellings and against a set of - rules for renamed settings, etc. - - Returns mapping from setting names to associated errors. - - :param defaults: Mopidy's default settings - :type defaults: dict - :param settings: the user's local settings - :type settings: dict - :rtype: dict - """ - errors = {} - - changed = { - 'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME', - 'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT', - 'GSTREAMER_AUDIO_SINK': 'OUTPUT', - 'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH', - 'LOCAL_OUTPUT_OVERRIDE': 'OUTPUT', - 'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH', - 'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE', - 'MIXER_ALSA_CONTROL': None, - 'MIXER_EXT_PORT': None, - 'MIXER_EXT_SPEAKERS_A': None, - 'MIXER_EXT_SPEAKERS_B': None, - 'MIXER_MAX_VOLUME': None, - 'SERVER': None, - 'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME', - 'SERVER_PORT': 'MPD_SERVER_PORT', - 'SPOTIFY_HIGH_BITRATE': 'SPOTIFY_BITRATE', - 'SPOTIFY_LIB_APPKEY': None, - 'SPOTIFY_LIB_CACHE': 'SPOTIFY_CACHE_PATH', - } - - must_be_iterable = [ - 'STREAM_PROTOCOLS', - ] - - for setting, value in settings.iteritems(): - if setting in changed: - if changed[setting] is None: - errors[setting] = 'Deprecated setting. It may be removed.' - else: - errors[setting] = 'Deprecated setting. Use %s.' % ( - changed[setting],) - - elif setting == 'OUTPUTS': - errors[setting] = ( - 'Deprecated setting, please change to OUTPUT. OUTPUT expects ' - 'a GStreamer bin description string for your desired output.') - - elif setting == 'SPOTIFY_BITRATE': - if value not in (96, 160, 320): - errors[setting] = ( - 'Unavailable Spotify bitrate. Available bitrates are 96, ' - '160, and 320.') - - elif setting.startswith('SHOUTCAST_OUTPUT_'): - errors[setting] = ( - 'Deprecated setting, please set the value via the GStreamer ' - 'bin in OUTPUT.') - - elif setting in must_be_iterable and not hasattr(value, '__iter__'): - errors[setting] = ( - 'Must be a tuple. ' - "Remember the comma after single values: (u'value',)") - - elif setting not in defaults and not setting.startswith('CUSTOM_'): - errors[setting] = 'Unknown setting.' - - return errors diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py deleted file mode 100644 index ce763486..00000000 --- a/tests/utils/settings_test.py +++ /dev/null @@ -1,150 +0,0 @@ -from __future__ import unicode_literals - -import os - -from mopidy import exceptions, settings -from mopidy.utils import settings as setting_utils - -from tests import unittest - - -class ValidateSettingsTest(unittest.TestCase): - def setUp(self): - self.defaults = { - 'MPD_SERVER_HOSTNAME': '::', - 'MPD_SERVER_PORT': 6600, - 'SPOTIFY_BITRATE': 160, - } - - def test_no_errors_yields_empty_dict(self): - result = setting_utils.validate_settings(self.defaults, {}) - self.assertEqual(result, {}) - - def test_unknown_setting_returns_error(self): - result = setting_utils.validate_settings( - self.defaults, {'MPD_SERVER_HOSTNMAE': '127.0.0.1'}) - self.assertEqual( - result['MPD_SERVER_HOSTNMAE'], 'Unknown setting.') - - def test_custom_settings_does_not_return_errors(self): - result = setting_utils.validate_settings( - self.defaults, {'CUSTOM_MYAPP_SETTING': 'foobar'}) - self.assertNotIn('CUSTOM_MYAPP_SETTING', result) - - def test_not_renamed_setting_returns_error(self): - result = setting_utils.validate_settings( - self.defaults, {'SERVER_HOSTNAME': '127.0.0.1'}) - self.assertEqual( - result['SERVER_HOSTNAME'], - 'Deprecated setting. Use MPD_SERVER_HOSTNAME.') - - def test_unneeded_settings_returns_error(self): - result = setting_utils.validate_settings( - self.defaults, {'SPOTIFY_LIB_APPKEY': '/tmp/foo'}) - self.assertEqual( - result['SPOTIFY_LIB_APPKEY'], - 'Deprecated setting. It may be removed.') - - def test_unavailable_bitrate_setting_returns_error(self): - result = setting_utils.validate_settings( - self.defaults, {'SPOTIFY_BITRATE': 50}) - self.assertEqual( - result['SPOTIFY_BITRATE'], - 'Unavailable Spotify bitrate. ' - 'Available bitrates are 96, 160, and 320.') - - def test_two_errors_are_both_reported(self): - result = setting_utils.validate_settings( - self.defaults, {'FOO': '', 'BAR': ''}) - self.assertEqual(len(result), 2) - - -class SettingsProxyTest(unittest.TestCase): - def setUp(self): - self.settings = setting_utils.SettingsProxy(settings) - self.settings.local.clear() - - def test_set_and_get_attr(self): - self.settings.TEST = 'test' - self.assertEqual(self.settings.TEST, 'test') - - def test_getattr_raises_error_on_missing_setting(self): - try: - self.settings.TEST - self.fail('Should raise exception') - except exceptions.SettingsError as e: - self.assertEqual('Setting "TEST" is not set.', e.message) - - def test_getattr_raises_error_on_empty_setting(self): - self.settings.TEST = '' - try: - self.settings.TEST - self.fail('Should raise exception') - except exceptions.SettingsError as e: - self.assertEqual('Setting "TEST" is empty.', e.message) - - def test_getattr_does_not_raise_error_if_setting_is_false(self): - self.settings.TEST = False - self.assertEqual(False, self.settings.TEST) - - def test_getattr_does_not_raise_error_if_setting_is_none(self): - self.settings.TEST = None - self.assertEqual(None, self.settings.TEST) - - def test_getattr_does_not_raise_error_if_setting_is_zero(self): - self.settings.TEST = 0 - self.assertEqual(0, self.settings.TEST) - - def test_setattr_updates_runtime_settings(self): - self.settings.TEST = 'test' - self.assertIn('TEST', self.settings.runtime) - - def test_setattr_updates_runtime_with_value(self): - self.settings.TEST = 'test' - self.assertEqual(self.settings.runtime['TEST'], 'test') - - def test_runtime_value_included_in_current(self): - self.settings.TEST = 'test' - self.assertEqual(self.settings.current['TEST'], 'test') - - def test_value_ending_in_path_is_expanded(self): - self.settings.TEST_PATH = '~/test' - actual = self.settings.TEST_PATH - expected = os.path.expanduser('~/test') - self.assertEqual(actual, expected) - - def test_value_ending_in_path_is_absolute(self): - self.settings.TEST_PATH = './test' - actual = self.settings.TEST_PATH - expected = os.path.abspath('./test') - self.assertEqual(actual, expected) - - def test_value_ending_in_file_is_expanded(self): - self.settings.TEST_FILE = '~/test' - actual = self.settings.TEST_FILE - expected = os.path.expanduser('~/test') - self.assertEqual(actual, expected) - - def test_value_ending_in_file_is_absolute(self): - self.settings.TEST_FILE = './test' - actual = self.settings.TEST_FILE - expected = os.path.abspath('./test') - self.assertEqual(actual, expected) - - def test_value_not_ending_in_path_or_file_is_not_expanded(self): - self.settings.TEST = '~/test' - actual = self.settings.TEST - self.assertEqual(actual, '~/test') - - def test_value_not_ending_in_path_or_file_is_not_absolute(self): - self.settings.TEST = './test' - actual = self.settings.TEST - self.assertEqual(actual, './test') - - def test_value_ending_in_file_can_be_none(self): - self.settings.TEST_FILE = None - self.assertEqual(self.settings.TEST_FILE, None) - - def test_value_ending_in_path_can_be_none(self): - self.settings.TEST_PATH = None - self.assertEqual(self.settings.TEST_PATH, None) From a5d81b86c0c5ba768a8434491b2bb1b67439119a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Apr 2013 20:11:12 +0200 Subject: [PATCH 15/45] scanner: Use new config --- mopidy/scanner.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 8bc57349..364828f4 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -34,7 +34,6 @@ import pygst pygst.require('0.10') import gst -from mopidy import settings from mopidy.frontends.mpd import translator as mpd_translator from mopidy.models import Track, Artist, Album from mopidy.utils import log, path, versioning @@ -58,9 +57,9 @@ def main(): logging.warning('Failed %s: %s', uri, error) logging.debug('Debug info for %s: %s', uri, debug) - logging.info('Scanning %s', settings.LOCAL_MUSIC_PATH) + logging.info('Scanning %s', config['local']['music_path']) - scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug) + scanner = Scanner(config['local']['music_path'], store, debug) try: scanner.start() except KeyboardInterrupt: From 4f0e1e448cced6cf83d568320c08b77b6d554848 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 7 Apr 2013 22:01:34 +0000 Subject: [PATCH 16/45] config: Add path config value tests --- tests/utils/config_test.py | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/utils/config_test.py b/tests/utils/config_test.py index 77c846df..5439dcc7 100644 --- a/tests/utils/config_test.py +++ b/tests/utils/config_test.py @@ -298,6 +298,55 @@ class PortTest(unittest.TestCase): self.assertRaises(ValueError, value.deserialize, '') +class ExpandedPathTest(unittest.TestCase): + def test_is_bytes(self): + self.assertIsInstance(config.ExpandedPath('/tmp'), bytes) + + @mock.patch('mopidy.utils.path.expand_path') + def test_defaults_to_expanded(self, expand_path_mock): + expand_path_mock.return_value = 'expanded_path' + self.assertEqual('expanded_path', config.ExpandedPath('~')) + + @mock.patch('mopidy.utils.path.expand_path') + def test_orginal_stores_unexpanded(self, expand_path_mock): + self.assertEqual('~', config.ExpandedPath('~').original) + + +class PathTest(unittest.TestCase): + def test_deserialize_conversion_success(self): + result = config.Path().deserialize('/foo') + self.assertEqual('/foo', result) + self.assertIsInstance(result, config.ExpandedPath) + self.assertIsInstance(result, bytes) + + def test_deserialize_enforces_choices(self): + value = config.Path(choices=['/foo', '/bar', '/baz']) + self.assertEqual('/foo', value.deserialize('/foo')) + self.assertRaises(ValueError, value.deserialize, '/foobar') + + def test_deserialize_enforces_required(self): + value = config.Path() + self.assertRaises(ValueError, value.deserialize, '') + self.assertRaises(ValueError, value.deserialize, ' ') + + def test_deserialize_respects_optional(self): + value = config.Path(optional=True) + self.assertIsNone(value.deserialize('')) + self.assertIsNone(value.deserialize(' ')) + + @mock.patch('mopidy.utils.path.expand_path') + def test_serialize_uses_original(self, expand_path_mock): + expand_path_mock.return_value = 'expanded_path' + path = config.ExpandedPath('original_path') + value = config.Path() + self.assertEqual('expanded_path', path) + self.assertEqual('original_path', value.serialize(path)) + + def test_serialize_plain_string(self): + value = config.Path() + self.assertEqual('path', value.serialize('path')) + + class ConfigSchemaTest(unittest.TestCase): def setUp(self): self.schema = config.ConfigSchema() From d83d33aece9559f0c82b76e936e4ff478ee067e2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Apr 2013 10:01:24 +0200 Subject: [PATCH 17/45] scrobbler: Update to use 'scrobbler' config values --- mopidy/frontends/scrobbler/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/scrobbler/actor.py b/mopidy/frontends/scrobbler/actor.py index 469df17a..1809661a 100644 --- a/mopidy/frontends/scrobbler/actor.py +++ b/mopidy/frontends/scrobbler/actor.py @@ -30,8 +30,8 @@ class ScrobblerFrontend(pykka.ThreadingActor, CoreListener): try: self.lastfm = pylast.LastFMNetwork( api_key=API_KEY, api_secret=API_SECRET, - username=self.config['lastfm']['username'], - password_hash=pylast.md5(self.config['lastfm']['password'])) + username=self.config['scrobbler']['username'], + password_hash=pylast.md5(self.config['scrobbler']['password'])) logger.info('Connected to Last.fm') except (pylast.NetworkError, pylast.MalformedResponseError, pylast.WSError) as e: From 05c74abbe321681909312e91254c1c45a3121545 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Apr 2013 10:01:36 +0200 Subject: [PATCH 18/45] http: Revert to use 'static_dir' --- mopidy/frontends/http/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index 1b7e7c7d..4297378c 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -30,8 +30,7 @@ port = 6680 # # Change this to have Mopidy serve e.g. files for your JavaScript client. # "/mopidy" will continue to work as usual even if you change this setting. -# -static_path = +static_dir = [logging.levels] cherrypy = warning @@ -533,7 +532,7 @@ class Extension(ext.Extension): schema = config.ExtensionConfigSchema() schema['hostname'] = config.Hostname() schema['port'] = config.Port() - schema['static_path'] = config.Path(optional=True) + schema['static_dir'] = config.Path(optional=True) return schema def validate_environment(self): From 335c4d9fd8ca4f4bf1642da72b3025e37727d05e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Apr 2013 20:42:33 +0200 Subject: [PATCH 19/45] main/path: Inline constants used once --- mopidy/__main__.py | 7 ++++--- mopidy/utils/path.py | 5 ----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 94e245d0..ea7bea32 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -309,9 +309,10 @@ def validate_config(raw_config, schemas, extensions=None): def create_file_structures(): - path.get_or_create_folder(path.DATA_PATH) - path.get_or_create_folder(path.CONFIG_PATH) - path.get_or_create_file(path.CONFIG_FILE) + path.get_or_create_folder(path.expand_path('$XDG_DATA_DIR/mopidy')) + path.get_or_create_folder(path.expand_path('$XDG_CONFIG_DIR/mopidy')) + path.get_or_create_file( + path.expand_path('$XDG_CONFIG_DIR/mopidy/mopidy.conf')) def setup_audio(config): diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 88b8be3e..bcd610da 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -25,11 +25,6 @@ XDG_DIRS = { 'XDG_DATA_DIR': XDG_DATA_DIR, 'XDG_MUSIC_DIR': XDG_MUSIC_DIR, } -DATA_PATH = os.path.join(unicode(XDG_DATA_DIR), 'mopidy') -CONFIG_PATH = os.path.join(unicode(XDG_CONFIG_DIR), 'mopidy') -CONFIG_FILE = os.path.join(unicode(CONFIG_PATH), 'mopidy.conf') -SETTINGS_PATH = os.path.join(unicode(XDG_CONFIG_DIR), 'mopidy') -SETTINGS_FILE = os.path.join(unicode(SETTINGS_PATH), 'settings.py') def get_or_create_folder(folder): From 6e5cdb85b0369480b9638dab65ae3f4d77e66dbc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Apr 2013 20:44:27 +0200 Subject: [PATCH 20/45] spotify: Rename 'cache_path' to 'cache_dir' --- mopidy/backends/spotify/__init__.py | 4 ++-- mopidy/backends/spotify/session_manager.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 8aa0f4c0..704471b1 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -25,7 +25,7 @@ bitrate = 160 timeout = 10 # Path to the Spotify data cache. Cannot be shared with other Spotify apps -cache_path = $XDG_CACHE_DIR/mopidy/spotify +cache_dir = $XDG_CACHE_DIR/mopidy/spotify """ __doc__ = """A backend for playing music from Spotify @@ -75,7 +75,7 @@ class Extension(ext.Extension): schema['password'] = config.String(secret=True) schema['bitrate'] = config.Integer(choices=(96, 160, 320)) schema['timeout'] = config.Integer(minimum=0) - schema['cache_path'] = config.Path() + schema['cache_dir'] = config.Path() return schema def validate_environment(self): diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 22ad4632..a830138f 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -30,8 +30,8 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): def __init__(self, config, audio, backend_ref): - self.cache_location = config['spotify']['cache_path'] - self.settings_location = config['spotify']['cache_path'] + self.cache_location = config['spotify']['cache_dir'] + self.settings_location = config['spotify']['cache_dir'] PyspotifySessionManager.__init__( self, config['spotify']['username'], config['spotify']['password'], From 3339b79c1e269d2706427bbda8153deab56d4a28 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Apr 2013 20:55:27 +0200 Subject: [PATCH 21/45] local: Rename 'music_path' to 'music_dir', 'playlist_path' to 'playlists_dir' --- mopidy/backends/local/__init__.py | 14 +++++++------- mopidy/backends/local/library.py | 7 +++---- mopidy/backends/local/playlists.py | 22 +++++++++++----------- mopidy/scanner.py | 6 +++--- tests/backends/local/events_test.py | 4 ++-- tests/backends/local/library_test.py | 4 ++-- tests/backends/local/playback_test.py | 4 ++-- tests/backends/local/playlists_test.py | 24 ++++++++++++------------ tests/backends/local/tracklist_test.py | 4 ++-- 9 files changed, 44 insertions(+), 45 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 54a1c7a4..abd3eab6 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -11,13 +11,13 @@ default_config = """ # If the local extension should be enabled or not enabled = true -# Path to folder with local music -music_path = $XDG_MUSIC_DIR +# Path to directory with local media files +media_dir = $XDG_MUSIC_DIR -# Path to playlist folder with m3u files for local music -playlist_path = $XDG_DATA_DIR/mopidy/playlists +# Path to playlists directory with m3u files for local media +playlists_dir = $XDG_DATA_DIR/mopidy/playlists -# Path to tag cache for local music +# Path to tag cache for local media tag_cache_file = $XDG_DATA_DIR/mopidy/tag_cache """ @@ -55,8 +55,8 @@ class Extension(ext.Extension): def get_config_schema(self): schema = config.ExtensionConfigSchema() - schema['music_path'] = config.Path() - schema['playlist_path'] = config.Path() + schema['media_dir'] = config.Path() + schema['playlists_dir'] = config.Path() schema['tag_cache_file'] = config.Path() return schema diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index 2b1c93f7..a76ce594 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -14,17 +14,16 @@ class LocalLibraryProvider(base.BaseLibraryProvider): def __init__(self, *args, **kwargs): super(LocalLibraryProvider, self).__init__(*args, **kwargs) self._uri_mapping = {} - self._music_path = self.backend.config['local']['music_path'] - self._playlist_path = self.backend.config['local']['playlist_path'] + self._media_dir = self.backend.config['local']['media_dir'] self._tag_cache_file = self.backend.config['local']['tag_cache_file'] self.refresh() def refresh(self, uri=None): - tracks = parse_mpd_tag_cache(self._tag_cache_file, self._music_path) + tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir) logger.info( 'Loading tracks from %s using %s', - self._music_path, self._tag_cache_file) + self._media_dir, self._tag_cache_file) for track in tracks: self._uri_mapping[track.uri] = track diff --git a/mopidy/backends/local/playlists.py b/mopidy/backends/local/playlists.py index 063d044d..3b9e1d73 100644 --- a/mopidy/backends/local/playlists.py +++ b/mopidy/backends/local/playlists.py @@ -18,8 +18,8 @@ logger = logging.getLogger('mopidy.backends.local') class LocalPlaylistsProvider(base.BasePlaylistsProvider): def __init__(self, *args, **kwargs): super(LocalPlaylistsProvider, self).__init__(*args, **kwargs) - self._music_path = self.backend.config['local']['music_path'] - self._playlist_path = self.backend.config['local']['playlist_path'] + self._media_dir = self.backend.config['local']['media_dir'] + self._playlists_dir = self.backend.config['local']['playlists_dir'] self.refresh() def create(self, name): @@ -42,16 +42,16 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): return playlist def refresh(self): - logger.info('Loading playlists from %s', self._playlist_path) + logger.info('Loading playlists from %s', self._playlists_dir) playlists = [] - for m3u in glob.glob(os.path.join(self._playlist_path, '*.m3u')): + for m3u in glob.glob(os.path.join(self._playlists_dir, '*.m3u')): uri = path.path_to_uri(m3u) name = os.path.splitext(os.path.basename(m3u))[0] tracks = [] - for track_uri in parse_m3u(m3u, self._music_path): + for track_uri in parse_m3u(m3u, self._media_dir): try: # TODO We must use core.library.lookup() to support tracks # from other backends @@ -86,13 +86,13 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): def _get_m3u_path(self, name): name = formatting.slugify(name) - file_path = os.path.join(self._playlist_path, name + '.m3u') - path.check_file_path_is_inside_base_dir(file_path, self._playlist_path) + file_path = os.path.join(self._playlists_dir, name + '.m3u') + path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir) return file_path def _save_m3u(self, playlist): file_path = path.uri_to_path(playlist.uri) - path.check_file_path_is_inside_base_dir(file_path, self._playlist_path) + path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir) with open(file_path, 'w') as file_handle: for track in playlist.tracks: if track.uri.startswith('file://'): @@ -103,18 +103,18 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): def _delete_m3u(self, uri): file_path = path.uri_to_path(uri) - path.check_file_path_is_inside_base_dir(file_path, self._playlist_path) + path.check_file_path_is_inside_base_dir(file_path, self._playlists_dir) if os.path.exists(file_path): os.remove(file_path) def _rename_m3u(self, playlist): src_file_path = path.uri_to_path(playlist.uri) path.check_file_path_is_inside_base_dir( - src_file_path, self._playlist_path) + src_file_path, self._playlists_dir) dst_file_path = self._get_m3u_path(playlist.name) path.check_file_path_is_inside_base_dir( - dst_file_path, self._playlist_path) + dst_file_path, self._playlists_dir) shutil.move(src_file_path, dst_file_path) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 364828f4..0f87a4d1 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -57,9 +57,9 @@ def main(): logging.warning('Failed %s: %s', uri, error) logging.debug('Debug info for %s: %s', uri, debug) - logging.info('Scanning %s', config['local']['music_path']) + logging.info('Scanning %s', config['local']['media_dir']) - scanner = Scanner(config['local']['music_path'], store, debug) + scanner = Scanner(config['local']['media_dir'], store, debug) try: scanner.start() except KeyboardInterrupt: @@ -68,7 +68,7 @@ def main(): logging.info('Done scanning; writing tag cache...') for row in mpd_translator.tracks_to_tag_cache_format( - tracks, config['mpd']['music_path']): + tracks, config['mpd']['media_dir']): if len(row) == 1: print ('%s' % row).encode('utf-8') else: diff --git a/tests/backends/local/events_test.py b/tests/backends/local/events_test.py index 83d77a2f..e09bf4b9 100644 --- a/tests/backends/local/events_test.py +++ b/tests/backends/local/events_test.py @@ -10,8 +10,8 @@ class LocalBackendEventsTest(events.BackendEventsTest, unittest.TestCase): backend_class = actor.LocalBackend config = { 'local': { - 'music_path': path_to_data_dir(''), - 'playlist_path': '', + 'media_dir': path_to_data_dir(''), + 'playlists_dir': '', 'tag_cache_file': path_to_data_dir('empty_tag_cache'), } } diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index e582c788..74635b3e 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -10,8 +10,8 @@ class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase): backend_class = actor.LocalBackend config = { 'local': { - 'music_path': path_to_data_dir(''), - 'playlist_path': '', + 'media_dir': path_to_data_dir(''), + 'playlists_dir': '', 'tag_cache_file': path_to_data_dir('library_tag_cache'), } } diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index 4c304590..16592539 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -14,8 +14,8 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): backend_class = actor.LocalBackend config = { 'local': { - 'music_path': path_to_data_dir(''), - 'playlist_path': '', + 'media_dir': path_to_data_dir(''), + 'playlists_dir': '', 'tag_cache_file': path_to_data_dir('empty_tag_cache'), } } diff --git a/tests/backends/local/playlists_test.py b/tests/backends/local/playlists_test.py index 8528adf4..b35b504f 100644 --- a/tests/backends/local/playlists_test.py +++ b/tests/backends/local/playlists_test.py @@ -20,32 +20,32 @@ class LocalPlaylistsControllerTest( backend_class = actor.LocalBackend config = { 'local': { - 'music_path': path_to_data_dir(''), + 'media_dir': path_to_data_dir(''), 'tag_cache_file': path_to_data_dir('library_tag_cache'), } } def setUp(self): - self.config['local']['playlist_path'] = tempfile.mkdtemp() - self.playlist_path = self.config['local']['playlist_path'] + self.config['local']['playlists_dir'] = tempfile.mkdtemp() + self.playlists_dir = self.config['local']['playlists_dir'] super(LocalPlaylistsControllerTest, self).setUp() def tearDown(self): super(LocalPlaylistsControllerTest, self).tearDown() - if os.path.exists(self.playlist_path): - shutil.rmtree(self.playlist_path) + if os.path.exists(self.playlists_dir): + shutil.rmtree(self.playlists_dir) def test_created_playlist_is_persisted(self): - path = os.path.join(self.playlist_path, 'test.m3u') + path = os.path.join(self.playlists_dir, 'test.m3u') self.assertFalse(os.path.exists(path)) self.core.playlists.create('test') self.assertTrue(os.path.exists(path)) def test_create_slugifies_playlist_name(self): - path = os.path.join(self.playlist_path, 'test-foo-bar.m3u') + path = os.path.join(self.playlists_dir, 'test-foo-bar.m3u') self.assertFalse(os.path.exists(path)) playlist = self.core.playlists.create('test FOO baR') @@ -53,7 +53,7 @@ class LocalPlaylistsControllerTest( self.assertTrue(os.path.exists(path)) def test_create_slugifies_names_which_tries_to_change_directory(self): - path = os.path.join(self.playlist_path, 'test-foo-bar.m3u') + path = os.path.join(self.playlists_dir, 'test-foo-bar.m3u') self.assertFalse(os.path.exists(path)) playlist = self.core.playlists.create('../../test FOO baR') @@ -61,8 +61,8 @@ class LocalPlaylistsControllerTest( self.assertTrue(os.path.exists(path)) def test_saved_playlist_is_persisted(self): - path1 = os.path.join(self.playlist_path, 'test1.m3u') - path2 = os.path.join(self.playlist_path, 'test2-foo-bar.m3u') + path1 = os.path.join(self.playlists_dir, 'test1.m3u') + path2 = os.path.join(self.playlists_dir, 'test2-foo-bar.m3u') playlist = self.core.playlists.create('test1') @@ -77,7 +77,7 @@ class LocalPlaylistsControllerTest( self.assertTrue(os.path.exists(path2)) def test_deleted_playlist_is_removed(self): - path = os.path.join(self.playlist_path, 'test.m3u') + path = os.path.join(self.playlists_dir, 'test.m3u') self.assertFalse(os.path.exists(path)) playlist = self.core.playlists.create('test') @@ -100,7 +100,7 @@ class LocalPlaylistsControllerTest( self.assertEqual(track_path, contents.strip()) def test_playlists_are_loaded_at_startup(self): - playlist_path = os.path.join(self.playlist_path, 'test.m3u') + playlist_path = os.path.join(self.playlists_dir, 'test.m3u') track = Track(uri=path_to_uri(path_to_data_dir('uri2'))) playlist = self.core.playlists.create('test') diff --git a/tests/backends/local/tracklist_test.py b/tests/backends/local/tracklist_test.py index 3fc8a0be..2d8e87b6 100644 --- a/tests/backends/local/tracklist_test.py +++ b/tests/backends/local/tracklist_test.py @@ -12,8 +12,8 @@ class LocalTracklistControllerTest(TracklistControllerTest, unittest.TestCase): backend_class = actor.LocalBackend config = { 'local': { - 'music_path': path_to_data_dir(''), - 'playlist_path': '', + 'media_dir': path_to_data_dir(''), + 'playlists_dir': '', 'tag_cache_file': path_to_data_dir('empty_tag_cache'), } } From f9ed1ba4d3045aec5292fda2a0703ef4a34b07fc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Apr 2013 20:58:34 +0200 Subject: [PATCH 22/45] mpd: Rename 'music_path' to 'music_dir' --- mopidy/frontends/mpd/translator.py | 20 ++++++------- tests/frontends/mpd/translator_test.py | 40 +++++++++++++------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index d820b0e0..21be37cc 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -215,14 +215,14 @@ def query_from_mpd_search_format(mpd_query): return query -def tracks_to_tag_cache_format(tracks, music_path): +def tracks_to_tag_cache_format(tracks, media_dir): """ Format list of tracks for output to MPD tag cache :param tracks: the tracks :type tracks: list of :class:`mopidy.models.Track` - :param music_path: the path to the music dir - :type music_path: string + :param media_dir: the path to the music dir + :type media_dir: string :rtype: list of lists of two-tuples """ result = [ @@ -232,13 +232,13 @@ def tracks_to_tag_cache_format(tracks, music_path): ('info_end',) ] tracks.sort(key=lambda t: t.uri) - folders, files = tracks_to_directory_tree(tracks, music_path) - _add_to_tag_cache(result, folders, files, music_path) + folders, files = tracks_to_directory_tree(tracks, media_dir) + _add_to_tag_cache(result, folders, files, media_dir) return result -def _add_to_tag_cache(result, folders, files, music_path): - base_path = music_path.encode('utf-8') +def _add_to_tag_cache(result, folders, files, media_dir): + base_path = media_dir.encode('utf-8') for path, (entry_folders, entry_files) in folders.items(): try: @@ -249,7 +249,7 @@ def _add_to_tag_cache(result, folders, files, music_path): result.append(('directory', text_path)) result.append(('mtime', get_mtime(os.path.join(base_path, path)))) result.append(('begin', name)) - _add_to_tag_cache(result, entry_folders, entry_files, music_path) + _add_to_tag_cache(result, entry_folders, entry_files, media_dir) result.append(('end', name)) result.append(('songList begin',)) @@ -275,7 +275,7 @@ def _add_to_tag_cache(result, folders, files, music_path): result.append(('songList end',)) -def tracks_to_directory_tree(tracks, music_path): +def tracks_to_directory_tree(tracks, media_dir): directories = ({}, []) for track in tracks: @@ -284,7 +284,7 @@ def tracks_to_directory_tree(tracks, music_path): absolute_track_dir_path = os.path.dirname(uri_to_path(track.uri)) relative_track_dir_path = re.sub( - '^' + re.escape(music_path), b'', absolute_track_dir_path) + '^' + re.escape(media_dir), b'', absolute_track_dir_path) for part in split_path(relative_track_dir_path): path = os.path.join(path, part) diff --git a/tests/frontends/mpd/translator_test.py b/tests/frontends/mpd/translator_test.py index 828acf1a..2f646faf 100644 --- a/tests/frontends/mpd/translator_test.py +++ b/tests/frontends/mpd/translator_test.py @@ -23,7 +23,7 @@ class TrackMpdFormatTest(unittest.TestCase): ) def setUp(self): - self.music_path = '/dir/subdir' + self.media_dir = '/dir/subdir' mtime.set_fake_time(1234567) def tearDown(self): @@ -135,14 +135,14 @@ class QueryFromMpdListFormatTest(unittest.TestCase): class TracksToTagCacheFormatTest(unittest.TestCase): def setUp(self): - self.music_path = '/dir/subdir' + self.media_dir = '/dir/subdir' mtime.set_fake_time(1234567) def tearDown(self): mtime.undo_fake() def translate(self, track): - base_path = self.music_path.encode('utf-8') + base_path = self.media_dir.encode('utf-8') result = dict(translator.track_to_mpd_format(track)) result['file'] = uri_to_path(result['file'])[len(base_path) + 1:] result['key'] = os.path.basename(result['file']) @@ -174,11 +174,11 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.fail("Couldn't find end %s in result" % directory) def test_empty_tag_cache_has_header(self): - result = translator.tracks_to_tag_cache_format([], self.music_path) + result = translator.tracks_to_tag_cache_format([], self.media_dir) result = self.consume_headers(result) def test_empty_tag_cache_has_song_list(self): - result = translator.tracks_to_tag_cache_format([], self.music_path) + result = translator.tracks_to_tag_cache_format([], self.media_dir) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -187,12 +187,12 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_header(self): track = Track(uri='file:///dir/subdir/song.mp3') - result = translator.tracks_to_tag_cache_format([track], self.music_path) + result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) def test_tag_cache_has_song_list(self): track = Track(uri='file:///dir/subdir/song.mp3') - result = translator.tracks_to_tag_cache_format([track], self.music_path) + result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -202,7 +202,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_formated_track(self): track = Track(uri='file:///dir/subdir/song.mp3') formated = self.translate(track) - result = translator.tracks_to_tag_cache_format([track], self.music_path) + result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -213,7 +213,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_has_formated_track_with_key_and_mtime(self): track = Track(uri='file:///dir/subdir/song.mp3') formated = self.translate(track) - result = translator.tracks_to_tag_cache_format([track], self.music_path) + result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -224,7 +224,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_directories(self): track = Track(uri='file:///dir/subdir/folder/song.mp3') formated = self.translate(track) - result = translator.tracks_to_tag_cache_format([track], self.music_path) + result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) folder, result = self.consume_directory(result) @@ -238,7 +238,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_diretory_header_is_right(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') - result = translator.tracks_to_tag_cache_format([track], self.music_path) + result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) folder, result = self.consume_directory(result) @@ -250,7 +250,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_suports_sub_directories(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') formated = self.translate(track) - result = translator.tracks_to_tag_cache_format([track], self.music_path) + result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) @@ -278,7 +278,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): formated.extend(self.translate(tracks[0])) formated.extend(self.translate(tracks[1])) - result = translator.tracks_to_tag_cache_format(tracks, self.music_path) + result = translator.tracks_to_tag_cache_format(tracks, self.media_dir) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) @@ -296,7 +296,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase): formated.append(self.translate(tracks[0])) formated.append(self.translate(tracks[1])) - result = translator.tracks_to_tag_cache_format(tracks, self.music_path) + result = translator.tracks_to_tag_cache_format(tracks, self.media_dir) result = self.consume_headers(result) folder, result = self.consume_directory(result) @@ -312,10 +312,10 @@ class TracksToTagCacheFormatTest(unittest.TestCase): class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): - self.music_path = '/root' + self.media_dir = '/root' def test_no_tracks_gives_emtpy_tree(self): - tree = translator.tracks_to_directory_tree([], self.music_path) + tree = translator.tracks_to_directory_tree([], self.media_dir) self.assertEqual(tree, ({}, [])) def test_top_level_files(self): @@ -324,18 +324,18 @@ class TracksToDirectoryTreeTest(unittest.TestCase): Track(uri='file:///root/file2.mp3'), Track(uri='file:///root/file3.mp3'), ] - tree = translator.tracks_to_directory_tree(tracks, self.music_path) + tree = translator.tracks_to_directory_tree(tracks, self.media_dir) self.assertEqual(tree, ({}, tracks)) def test_single_file_in_subdir(self): tracks = [Track(uri='file:///root/dir/file1.mp3')] - tree = translator.tracks_to_directory_tree(tracks, self.music_path) + tree = translator.tracks_to_directory_tree(tracks, self.media_dir) expected = ({'dir': ({}, tracks)}, []) self.assertEqual(tree, expected) def test_single_file_in_sub_subdir(self): tracks = [Track(uri='file:///root/dir1/dir2/file1.mp3')] - tree = translator.tracks_to_directory_tree(tracks, self.music_path) + tree = translator.tracks_to_directory_tree(tracks, self.media_dir) expected = ({'dir1': ({'dir1/dir2': ({}, tracks)}, [])}, []) self.assertEqual(tree, expected) @@ -347,7 +347,7 @@ class TracksToDirectoryTreeTest(unittest.TestCase): Track(uri='file:///root/dir2/file4.mp3'), Track(uri='file:///root/dir2/sub/file5.mp3'), ] - tree = translator.tracks_to_directory_tree(tracks, self.music_path) + tree = translator.tracks_to_directory_tree(tracks, self.media_dir) expected = ( { 'dir1': ({}, [tracks[1], tracks[2]]), From 41d7ae9a2c093a7e0f70be97868f73f4662c1324 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Apr 2013 21:13:59 +0200 Subject: [PATCH 23/45] Replace 'folder' with 'dir' --- mopidy/__main__.py | 4 +- mopidy/backends/local/translator.py | 4 +- mopidy/frontends/mpd/translator.py | 10 ++-- mopidy/scanner.py | 4 +- mopidy/utils/path.py | 18 +++---- tests/backends/local/playlists_test.py | 2 +- tests/backends/local/translator_test.py | 2 +- tests/frontends/mpd/translator_test.py | 24 ++++----- tests/scanner_test.py | 2 +- tests/utils/path_test.py | 66 ++++++++++++------------- 10 files changed, 68 insertions(+), 68 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index ea7bea32..4e81856a 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -309,8 +309,8 @@ def validate_config(raw_config, schemas, extensions=None): def create_file_structures(): - path.get_or_create_folder(path.expand_path('$XDG_DATA_DIR/mopidy')) - path.get_or_create_folder(path.expand_path('$XDG_CONFIG_DIR/mopidy')) + path.get_or_create_dir(path.expand_path('$XDG_DATA_DIR/mopidy')) + path.get_or_create_dir(path.expand_path('$XDG_CONFIG_DIR/mopidy')) path.get_or_create_file( path.expand_path('$XDG_CONFIG_DIR/mopidy/mopidy.conf')) diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index a36be927..683ad6b4 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -10,7 +10,7 @@ from mopidy.utils.path import path_to_uri logger = logging.getLogger('mopidy.backends.local') -def parse_m3u(file_path, music_folder): +def parse_m3u(file_path, media_dir): r""" Convert M3U file list of uris @@ -49,7 +49,7 @@ def parse_m3u(file_path, music_folder): if line.startswith('file://'): uris.append(line) else: - path = path_to_uri(music_folder, line) + path = path_to_uri(media_dir, line) uris.append(path) return uris diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 21be37cc..7cf5b0c6 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -232,15 +232,15 @@ def tracks_to_tag_cache_format(tracks, media_dir): ('info_end',) ] tracks.sort(key=lambda t: t.uri) - folders, files = tracks_to_directory_tree(tracks, media_dir) - _add_to_tag_cache(result, folders, files, media_dir) + dirs, files = tracks_to_directory_tree(tracks, media_dir) + _add_to_tag_cache(result, dirs, files, media_dir) return result -def _add_to_tag_cache(result, folders, files, media_dir): +def _add_to_tag_cache(result, dirs, files, media_dir): base_path = media_dir.encode('utf-8') - for path, (entry_folders, entry_files) in folders.items(): + for path, (entry_dirs, entry_files) in dirs.items(): try: text_path = path.decode('utf-8') except UnicodeDecodeError: @@ -249,7 +249,7 @@ def _add_to_tag_cache(result, folders, files, media_dir): result.append(('directory', text_path)) result.append(('mtime', get_mtime(os.path.join(base_path, path)))) result.append(('begin', name)) - _add_to_tag_cache(result, entry_folders, entry_files, media_dir) + _add_to_tag_cache(result, entry_dirs, entry_files, media_dir) result.append(('end', name)) result.append(('songList begin',)) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 0f87a4d1..0c78839b 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -142,9 +142,9 @@ def translator(data): class Scanner(object): - def __init__(self, folder, data_callback, error_callback=None): + def __init__(self, base_dir, data_callback, error_callback=None): self.data = {} - self.files = path.find_files(folder) + self.files = path.find_files(base_dir) self.data_callback = data_callback self.error_callback = error_callback self.loop = gobject.MainLoop() diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index bcd610da..eb0cbbb0 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -27,16 +27,16 @@ XDG_DIRS = { } -def get_or_create_folder(folder): - folder = os.path.expanduser(folder) - if os.path.isfile(folder): +def get_or_create_dir(dir_path): + dir_path = os.path.expanduser(dir_path) + if os.path.isfile(dir_path): raise OSError( 'A file with the same name as the desired dir, ' - '"%s", already exists.' % folder) - elif not os.path.isdir(folder): - logger.info('Creating dir %s', folder) - os.makedirs(folder, 0755) - return folder + '"%s", already exists.' % dir_path) + elif not os.path.isdir(dir_path): + logger.info('Creating dir %s', dir_path) + os.makedirs(dir_path, 0755) + return dir_path def get_or_create_file(filename): @@ -121,7 +121,7 @@ def find_files(path): for dirpath, dirnames, filenames in os.walk(path, followlinks=True): for dirname in dirnames: if dirname.startswith(b'.'): - # Skip hidden folders by modifying dirnames inplace + # Skip hidden dirs by modifying dirnames inplace dirnames.remove(dirname) for filename in filenames: diff --git a/tests/backends/local/playlists_test.py b/tests/backends/local/playlists_test.py index b35b504f..acaac941 100644 --- a/tests/backends/local/playlists_test.py +++ b/tests/backends/local/playlists_test.py @@ -123,5 +123,5 @@ class LocalPlaylistsControllerTest( pass @unittest.SkipTest - def test_playlist_folder_is_createad(self): + def test_playlist_dir_is_created(self): pass diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index 61a86672..67907ff1 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -35,7 +35,7 @@ class M3UToUriTest(unittest.TestCase): uris = parse_m3u(path_to_data_dir('comment.m3u'), data_dir) self.assertEqual([song1_uri], uris) - def test_file_is_relative_to_correct_folder(self): + def test_file_is_relative_to_correct_dir(self): with tempfile.NamedTemporaryFile(delete=False) as tmp: tmp.write('song1.mp3') try: diff --git a/tests/frontends/mpd/translator_test.py b/tests/frontends/mpd/translator_test.py index 2f646faf..e4755d01 100644 --- a/tests/frontends/mpd/translator_test.py +++ b/tests/frontends/mpd/translator_test.py @@ -221,18 +221,18 @@ class TracksToTagCacheFormatTest(unittest.TestCase): self.assertEqual(formated, song_list) self.assertEqual(len(result), 0) - def test_tag_cache_suports_directories(self): + def test_tag_cache_supports_directories(self): track = Track(uri='file:///dir/subdir/folder/song.mp3') formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) - folder, result = self.consume_directory(result) + dir_data, result = self.consume_directory(result) song_list, result = self.consume_song_list(result) self.assertEqual(len(song_list), 0) self.assertEqual(len(result), 0) - song_list, result = self.consume_song_list(folder) + song_list, result = self.consume_song_list(dir_data) self.assertEqual(len(result), 0) self.assertEqual(formated, song_list) @@ -241,11 +241,11 @@ class TracksToTagCacheFormatTest(unittest.TestCase): result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) - folder, result = self.consume_directory(result) + dir_data, result = self.consume_directory(result) - self.assertEqual(('directory', 'folder/sub'), folder[0]) - self.assertEqual(('mtime', mtime('.')), folder[1]) - self.assertEqual(('begin', 'sub'), folder[2]) + self.assertEqual(('directory', 'folder/sub'), dir_data[0]) + self.assertEqual(('mtime', mtime('.')), dir_data[1]) + self.assertEqual(('begin', 'sub'), dir_data[2]) def test_tag_cache_suports_sub_directories(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') @@ -254,17 +254,17 @@ class TracksToTagCacheFormatTest(unittest.TestCase): result = self.consume_headers(result) - folder, result = self.consume_directory(result) + dir_data, result = self.consume_directory(result) song_list, result = self.consume_song_list(result) self.assertEqual(len(song_list), 0) self.assertEqual(len(result), 0) - folder, result = self.consume_directory(folder) + dir_data, result = self.consume_directory(dir_data) song_list, result = self.consume_song_list(result) self.assertEqual(len(result), 0) self.assertEqual(len(song_list), 0) - song_list, result = self.consume_song_list(folder) + song_list, result = self.consume_song_list(dir_data) self.assertEqual(len(result), 0) self.assertEqual(formated, song_list) @@ -299,8 +299,8 @@ class TracksToTagCacheFormatTest(unittest.TestCase): result = translator.tracks_to_tag_cache_format(tracks, self.media_dir) result = self.consume_headers(result) - folder, result = self.consume_directory(result) - song_list, song_result = self.consume_song_list(folder) + dir_data, result = self.consume_directory(result) + song_list, song_result = self.consume_song_list(dir_data) self.assertEqual(formated[1], song_list) self.assertEqual(len(song_result), 0) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 617f2537..edcc2242 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -196,7 +196,7 @@ class ScannerTest(unittest.TestCase): self.check('scanner/simple/song1.mp3', 'title', 'trackname') self.check('scanner/simple/song1.ogg', 'title', 'trackname') - def test_nonexistant_folder_does_not_fail(self): + def test_nonexistant_dir_does_not_fail(self): self.scan('scanner/does-not-exist') self.assert_(not self.errors) diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index 461f0809..a73cb8e4 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -13,7 +13,7 @@ from mopidy.utils import path from tests import unittest, path_to_data_dir -class GetOrCreateFolderTest(unittest.TestCase): +class GetOrCreateDirTest(unittest.TestCase): def setUp(self): self.parent = tempfile.mkdtemp() @@ -21,40 +21,40 @@ class GetOrCreateFolderTest(unittest.TestCase): if os.path.isdir(self.parent): shutil.rmtree(self.parent) - def test_creating_folder(self): - folder = os.path.join(self.parent, 'test') - self.assert_(not os.path.exists(folder)) - self.assert_(not os.path.isdir(folder)) - created = path.get_or_create_folder(folder) - self.assert_(os.path.exists(folder)) - self.assert_(os.path.isdir(folder)) - self.assertEqual(created, folder) + def test_creating_dir(self): + dir_path = os.path.join(self.parent, 'test') + self.assert_(not os.path.exists(dir_path)) + self.assert_(not os.path.isdir(dir_path)) + created = path.get_or_create_dir(dir_path) + self.assert_(os.path.exists(dir_path)) + self.assert_(os.path.isdir(dir_path)) + self.assertEqual(created, dir_path) - def test_creating_nested_folders(self): - level2_folder = os.path.join(self.parent, 'test') - level3_folder = os.path.join(self.parent, 'test', 'test') - self.assert_(not os.path.exists(level2_folder)) - self.assert_(not os.path.isdir(level2_folder)) - self.assert_(not os.path.exists(level3_folder)) - self.assert_(not os.path.isdir(level3_folder)) - created = path.get_or_create_folder(level3_folder) - self.assert_(os.path.exists(level2_folder)) - self.assert_(os.path.isdir(level2_folder)) - self.assert_(os.path.exists(level3_folder)) - self.assert_(os.path.isdir(level3_folder)) - self.assertEqual(created, level3_folder) + def test_creating_nested_dirs(self): + level2_dir = os.path.join(self.parent, 'test') + level3_dir = os.path.join(self.parent, 'test', 'test') + self.assert_(not os.path.exists(level2_dir)) + self.assert_(not os.path.isdir(level2_dir)) + self.assert_(not os.path.exists(level3_dir)) + self.assert_(not os.path.isdir(level3_dir)) + created = path.get_or_create_dir(level3_dir) + self.assert_(os.path.exists(level2_dir)) + self.assert_(os.path.isdir(level2_dir)) + self.assert_(os.path.exists(level3_dir)) + self.assert_(os.path.isdir(level3_dir)) + self.assertEqual(created, level3_dir) - def test_creating_existing_folder(self): - created = path.get_or_create_folder(self.parent) + def test_creating_existing_dir(self): + created = path.get_or_create_dir(self.parent) self.assert_(os.path.exists(self.parent)) self.assert_(os.path.isdir(self.parent)) self.assertEqual(created, self.parent) - def test_create_folder_with_name_of_existing_file_throws_oserror(self): + def test_create_dir_with_name_of_existing_file_throws_oserror(self): conflicting_file = os.path.join(self.parent, 'test') open(conflicting_file, 'w').close() - folder = os.path.join(self.parent, 'test') - self.assertRaises(OSError, path.get_or_create_folder, folder) + dir_path = os.path.join(self.parent, 'test') + self.assertRaises(OSError, path.get_or_create_dir, dir_path) class PathToFileURITest(unittest.TestCase): @@ -66,7 +66,7 @@ class PathToFileURITest(unittest.TestCase): result = path.path_to_uri('/etc/fstab') self.assertEqual(result, 'file:///etc/fstab') - def test_folder_and_path(self): + def test_dir_and_path(self): if sys.platform == 'win32': result = path.path_to_uri('C:/WINDOWS/', 'clock.avi') self.assertEqual(result, 'file:///C://WINDOWS/clock.avi') @@ -145,10 +145,10 @@ class SplitPathTest(unittest.TestCase): def test_empty_path(self): self.assertEqual([], path.split_path('')) - def test_single_folder(self): + def test_single_dir(self): self.assertEqual(['foo'], path.split_path('foo')) - def test_folders(self): + def test_dirs(self): self.assertEqual(['foo', 'bar', 'baz'], path.split_path('foo/bar/baz')) def test_initial_slash_is_ignored(self): @@ -190,10 +190,10 @@ class FindFilesTest(unittest.TestCase): def find(self, value): return list(path.find_files(path_to_data_dir(value))) - def test_basic_folder(self): + def test_basic_dir(self): self.assert_(self.find('')) - def test_nonexistant_folder(self): + def test_nonexistant_dir(self): self.assertEqual(self.find('does-not-exist'), []) def test_file(self): @@ -207,7 +207,7 @@ class FindFilesTest(unittest.TestCase): self.assert_( is_bytes(name), '%s is not bytes object' % repr(name)) - def test_ignores_hidden_folders(self): + def test_ignores_hidden_dirs(self): self.assertEqual(self.find('.hidden'), []) def test_ignores_hidden_files(self): From ef57c905262822afa518d7faa6aba7ab73901da6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 00:09:33 +0200 Subject: [PATCH 24/45] path: Use our expand_path instead of os.path.expanduser --- mopidy/__main__.py | 7 +++---- mopidy/utils/path.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 4e81856a..6a691cce 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -309,10 +309,9 @@ def validate_config(raw_config, schemas, extensions=None): def create_file_structures(): - path.get_or_create_dir(path.expand_path('$XDG_DATA_DIR/mopidy')) - path.get_or_create_dir(path.expand_path('$XDG_CONFIG_DIR/mopidy')) - path.get_or_create_file( - path.expand_path('$XDG_CONFIG_DIR/mopidy/mopidy.conf')) + path.get_or_create_dir('$XDG_DATA_DIR/mopidy') + path.get_or_create_dir('$XDG_CONFIG_DIR/mopidy') + path.get_or_create_file('$XDG_CONFIG_DIR/mopidy/mopidy.conf') def setup_audio(config): diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index eb0cbbb0..699b361f 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -28,7 +28,7 @@ XDG_DIRS = { def get_or_create_dir(dir_path): - dir_path = os.path.expanduser(dir_path) + dir_path = expand_path(dir_path) if os.path.isfile(dir_path): raise OSError( 'A file with the same name as the desired dir, ' @@ -40,7 +40,7 @@ def get_or_create_dir(dir_path): def get_or_create_file(filename): - filename = os.path.expanduser(filename) + filename = expand_path(filename) if not os.path.isfile(filename): logger.info('Creating file %s', filename) open(filename, 'w') From 5be8fa347fd3430aa30ecb4522d8e3d46888af36 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 8 Apr 2013 23:36:37 +0200 Subject: [PATCH 25/45] docs: Add 'confval' object type to Sphinx --- docs/conf.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 55f415b4..7f111014 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -266,3 +266,12 @@ latex_documents = [ needs_sphinx = '1.0' extlinks = {'issue': ('https://github.com/mopidy/mopidy/issues/%s', '#')} + + +def setup(app): + from sphinx.ext.autodoc import cut_lines + app.connect(b'autodoc-process-docstring', cut_lines(4, what=['module'])) + app.add_object_type( + b'confval', 'confval', + objname='configuration value', + indextemplate='pair: %s; configuration value') From 2cd0fdc57197663606134b9371b45693d93bef1c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 00:14:59 +0200 Subject: [PATCH 26/45] docs: Fix typo --- docs/modules/frontends/scrobbler.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/modules/frontends/scrobbler.rst b/docs/modules/frontends/scrobbler.rst index eee65724..2af9fcff 100644 --- a/docs/modules/frontends/scrobbler.rst +++ b/docs/modules/frontends/scrobbler.rst @@ -1,6 +1,6 @@ -********************************************** -:mod:`mopidy.frontends.scrobble` -- Scrobbler -********************************************** +*********************************************** +:mod:`mopidy.frontends.scrobbler` -- Scrobbler +*********************************************** .. automodule:: mopidy.frontends.scrobbler :synopsis: Music scrobbler frontend From 09a38d95238cf889ba035c36133b9c90d296fbe8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 00:22:24 +0200 Subject: [PATCH 27/45] docs: Use :confval: to make config docs linkable --- mopidy/audio/mixers/auto.py | 4 +- mopidy/audio/mixers/fake.py | 2 +- mopidy/audio/mixers/nad.py | 5 ++- mopidy/backends/local/__init__.py | 26 ++++++++---- mopidy/backends/spotify/__init__.py | 38 +++++++++++------ mopidy/backends/stream/__init__.py | 22 +++++++--- mopidy/frontends/http/__init__.py | 57 +++++++++++++++---------- mopidy/frontends/mpd/__init__.py | 59 ++++++++++++++++---------- mopidy/frontends/mpris/__init__.py | 18 +++++--- mopidy/frontends/scrobbler/__init__.py | 24 +++++++---- 10 files changed, 164 insertions(+), 91 deletions(-) diff --git a/mopidy/audio/mixers/auto.py b/mopidy/audio/mixers/auto.py index b24bcf4c..f1dde3f9 100644 --- a/mopidy/audio/mixers/auto.py +++ b/mopidy/audio/mixers/auto.py @@ -8,8 +8,8 @@ None **Configuration** -If this wasn't the default, you would set the ``audio/mixer`` config value to -``autoaudiomixer`` to use this mixer. +If this wasn't the default, you would set the :confval:`audio/mixer` config +value to ``autoaudiomixer`` to use this mixer. """ from __future__ import unicode_literals diff --git a/mopidy/audio/mixers/fake.py b/mopidy/audio/mixers/fake.py index 05e86923..f78c7c64 100644 --- a/mopidy/audio/mixers/fake.py +++ b/mopidy/audio/mixers/fake.py @@ -6,7 +6,7 @@ None **Configuration** -Set the ``audio/mixer`` config value to ``fakemixer`` to use this mixer. +Set the :confval:`audio/mixe:` config value to ``fakemixer`` to use this mixer. """ from __future__ import unicode_literals diff --git a/mopidy/audio/mixers/nad.py b/mopidy/audio/mixers/nad.py index 058333d1..9259d291 100644 --- a/mopidy/audio/mixers/nad.py +++ b/mopidy/audio/mixers/nad.py @@ -9,8 +9,9 @@ serial cable. **Configuration** -Set the ``audio/mixer`` config value to ``nadmixer`` to use it. You probably -also needs to add some properties to the ``audio/mixer`` config value. +Set the :confval:`audio/mixer` config value to ``nadmixer`` to use it. You +probably also needs to add some properties to the :confval:`audio/mixer` config +value. Supported properties includes: diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index abd3eab6..ffcf5869 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -7,17 +7,9 @@ from mopidy.utils import config, formatting default_config = """ [local] - -# If the local extension should be enabled or not enabled = true - -# Path to directory with local media files media_dir = $XDG_MUSIC_DIR - -# Path to playlists directory with m3u files for local media playlists_dir = $XDG_DATA_DIR/mopidy/playlists - -# Path to tag cache for local media tag_cache_file = $XDG_DATA_DIR/mopidy/tag_cache """ @@ -36,6 +28,24 @@ https://github.com/mopidy/mopidy/issues?labels=Local+backend None +**Configuration** + +.. confval:: local/enabled + + If the local extension should be enabled or not. + +.. confval:: local/media_dir + + Path to directory with local media files. + +.. confval:: local/playlists_dir + + Path to playlists directory with m3u files for local media. + +.. confval:: local/tag_cache_file + + Path to tag cache for local media. + **Default config** .. code-block:: ini diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 704471b1..c26a42e7 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -8,23 +8,11 @@ from mopidy.utils import config, formatting default_config = """ [spotify] - -# If the Spotify extension should be enabled or not enabled = true - -# Your Spotify Premium username username = - -# Your Spotify Premium password password = - -# The preferred audio bitrate. Valid values are 96, 160, 320 bitrate = 160 - -# Max number of seconds to wait for Spotify operations to complete timeout = 10 - -# Path to the Spotify data cache. Cannot be shared with other Spotify apps cache_dir = $XDG_CACHE_DIR/mopidy/spotify """ @@ -52,6 +40,32 @@ https://github.com/mopidy/mopidy/issues?labels=Spotify+backend .. literalinclude:: ../../../requirements/spotify.txt +**Configuration** + +.. confval:: spotify/enabled + + If the Spotify extension should be enabled or not. + +.. confval:: spotify/username + + Your Spotify Premium username. + +.. confval:: spotify/password + + Your Spotify Premium password. + +.. confval:: spotify/bitrate + + The preferred audio bitrate. Valid values are 96, 160, 320. + +.. confval:: spotify/timeout + + Max number of seconds to wait for Spotify operations to complete. + +.. confval:: spotify/cache_dir + + Path to the Spotify data cache. Cannot be shared with other Spotify apps. + **Default config** .. code-block:: ini diff --git a/mopidy/backends/stream/__init__.py b/mopidy/backends/stream/__init__.py index 17b85d33..11918500 100644 --- a/mopidy/backends/stream/__init__.py +++ b/mopidy/backends/stream/__init__.py @@ -7,11 +7,7 @@ from mopidy.utils import config, formatting default_config = """ [stream] - -# If the stream extension should be enabled or not enabled = true - -# Whitelist of URI schemas to support streaming from protocols = http https @@ -21,10 +17,12 @@ protocols = rtsp """ -__doc__ = """A backend for playing music for streaming music. +__doc__ = """ +A backend for playing music for streaming music. -This backend will handle streaming of URIs matching the ``stream/protocols`` -config value, assuming the needed GStreamer plugins are installed. +This backend will handle streaming of URIs matching the +:confval:`stream/protocols` config value, assuming the needed GStreamer plugins +are installed. **Issues** @@ -34,6 +32,16 @@ https://github.com/mopidy/mopidy/issues?labels=Stream+backend None +**Configuration** + +.. confval:: stream/enabled + + If the stream extension should be enabled or not. + +.. confval:: stream/protocols + + Whitelist of URI schemas to allow streaming from. + **Default config** .. code-block:: ini diff --git a/mopidy/frontends/http/__init__.py b/mopidy/frontends/http/__init__.py index 4297378c..32d55f23 100644 --- a/mopidy/frontends/http/__init__.py +++ b/mopidy/frontends/http/__init__.py @@ -7,29 +7,9 @@ from mopidy.utils import config, formatting default_config = """ [http] - -# If the HTTP extension should be enabled or not enabled = true - -# Which address the HTTP server should bind to -# -# 127.0.0.1 -# Listens only on the IPv4 loopback interface -# ::1 -# Listens only on the IPv6 loopback interface -# 0.0.0.0 -# Listens on all IPv4 interfaces -# :: -# Listens on all interfaces, both IPv4 and IPv6 hostname = 127.0.0.1 - -# Which TCP port the HTTP server should listen to port = 6680 - -# Which directory the HTTP server should serve at "/" -# -# Change this to have Mopidy serve e.g. files for your JavaScript client. -# "/mopidy" will continue to work as usual even if you change this setting. static_dir = [logging.levels] @@ -48,6 +28,36 @@ https://github.com/mopidy/mopidy/issues?labels=HTTP+frontend .. literalinclude:: ../../../requirements/http.txt +**Configuration** + +.. confval:: http/enabled + + If the HTTP extension should be enabled or not. + +.. confval:: http/hostname + + Which address the HTTP server should bind to. + + ``127.0.0.1`` + Listens only on the IPv4 loopback interface + ``::1`` + Listens only on the IPv6 loopback interface + ``0.0.0.0`` + Listens on all IPv4 interfaces + ``::`` + Listens on all interfaces, both IPv4 and IPv6 + +.. confval:: http/port + + Which TCP port the HTTP server should listen to. + +.. confval:: http/static_dir + + Which directory the HTTP server should serve at "/" + + Change this to have Mopidy serve e.g. files for your JavaScript client. + "/mopidy" will continue to work as usual even if you change this setting. + **Default config** .. code-block:: ini @@ -61,13 +71,13 @@ Setup The frontend is enabled by default if all dependencies are available. When it is enabled it starts a web server at the port specified by the -``http/port`` config value. +:confval:`http/port` config value. .. warning:: Security As a simple security measure, the web server is by default only available from localhost. To make it available from other computers, change the - ``http/hostname`` config value. Before you do so, note that the HTTP + :confval:`http/hostname` config value. Before you do so, note that the HTTP frontend does not feature any form of user authentication or authorization. Anyone able to access the web server can use the full core API of Mopidy. Thus, you probably only want to make the web server available from your @@ -405,7 +415,8 @@ Example to get started with 2. Create an empty directory for your web client. -3. Change the ``http/static_dir`` config value to point to your new directory. +3. Change the :confval:`http/static_dir` config value to point to your new + directory. 4. Start/restart Mopidy. diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 08bafd26..69297374 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -7,33 +7,11 @@ from mopidy.utils import config, formatting default_config = """ [mpd] - -# If the MPD extension should be enabled or not enabled = true - -# Which address the MPD server should bind to -# -# 127.0.0.1 -# Listens only on the IPv4 loopback interface -# ::1 -# Listens only on the IPv6 loopback interface -# 0.0.0.0 -# Listens on all IPv4 interfaces -# :: -# Listens on all interfaces, both IPv4 and IPv6 hostname = 127.0.0.1 - -# Which TCP port the MPD server should listen to port = 6600 - -# The password required for connecting to the MPD server password = - -# The maximum number of concurrent connections the MPD server will accept max_connections = 20 - -# Number of seconds an MPD client can stay inactive before the connection is -# closed by the server connection_timeout = 60 """ @@ -51,6 +29,43 @@ https://github.com/mopidy/mopidy/issues?labels=MPD+frontend None +**Configuration** + +.. confval:: mpd/enabled + + If the MPD extension should be enabled or not. + +.. confval:: mpd/hostname + + Which address the MPD server should bind to. + + ``127.0.0.1`` + Listens only on the IPv4 loopback interface + ``::1`` + Listens only on the IPv6 loopback interface + ``0.0.0.0`` + Listens on all IPv4 interfaces + ``::`` + Listens on all interfaces, both IPv4 and IPv6 + +.. confval:: mpd/port + + Which TCP port the MPD server should listen to. + +.. confval:: mpd/password + + The password required for connecting to the MPD server. If blank, no + password is required. + +.. confval:: mpd/max_connections + + The maximum number of concurrent connections the MPD server will accept. + +.. confval:: mpd/connection_timeout + + Number of seconds an MPD client can stay inactive before the connection is + closed by the server. + **Default config** .. code-block:: ini diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py index 43061013..813e03a2 100644 --- a/mopidy/frontends/mpris/__init__.py +++ b/mopidy/frontends/mpris/__init__.py @@ -7,11 +7,7 @@ from mopidy.utils import formatting, config default_config = """ [mpris] - -# If the MPRIS extension should be enabled or not enabled = true - -# Location of the Mopidy .desktop file desktop_file = /usr/share/applications/mopidy.desktop """ @@ -33,8 +29,18 @@ An example of an MPRIS client is the `Ubuntu Sound Menu Ubuntu/Debian. - An ``.desktop`` file for Mopidy installed at the path set in the - ``mpris/desktop_file`` config value. See :ref:`install-desktop-file` for - details. + :confval:`mpris/desktop_file` config value. See :ref:`install-desktop-file` + for details. + +**Configuration** + +.. confval:: mpris/enabled + + If the MPRIS extension should be enabled or not. + +.. confval:: mpris/desktop_file + + Location of the Mopidy ``.desktop`` file. **Default config** diff --git a/mopidy/frontends/scrobbler/__init__.py b/mopidy/frontends/scrobbler/__init__.py index c33a5fa3..f3127040 100644 --- a/mopidy/frontends/scrobbler/__init__.py +++ b/mopidy/frontends/scrobbler/__init__.py @@ -7,20 +7,14 @@ from mopidy.utils import config, formatting default_config = """ [scrobbler] - -# If the Last.fm extension should be enabled or not enabled = true - -# Your Last.fm username username = - -# Your Last.fm password password = """ __doc__ = """ -Frontend which scrobbles the music you play to your `Last.fm -`_ profile. +Frontend which scrobbles the music you play to your +`Last.fm `_ profile. .. note:: @@ -30,6 +24,20 @@ Frontend which scrobbles the music you play to your `Last.fm .. literalinclude:: ../../../requirements/scrobbler.txt +**Configuration** + +.. confval:: scrobbler/enabled + + If the scrobbler extension should be enabled or not. + +.. confval:: scrobbler/username + + Your Last.fm username. + +.. confval:: scrobbler/password + + Your Last.fm password. + **Default config** .. code-block:: ini From 74032e02ea8de8c80e79edb6154547d8a8fbee91 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 00:23:37 +0200 Subject: [PATCH 28/45] docs: Check for 'MIXER_TRACK_' to not confuse with old 'MIXER_TRACK' setting --- docs/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 7f111014..1e9f9cb2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,7 +32,8 @@ class Mock(object): def __getattr__(self, name): if name in ('__file__', '__path__'): return '/dev/null' - elif name[0] == name[0].upper() and not name.startswith('MIXER_TRACK'): + elif (name[0] == name[0].upper() + and not name.startswith('MIXER_TRACK_')): return type(name, (), {}) else: return Mock() From 18d34d6ffc0772af9a0082fcb0315b954c4a49b1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 00:27:22 +0200 Subject: [PATCH 29/45] docs: Fix typo --- mopidy/audio/mixers/fake.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/mixers/fake.py b/mopidy/audio/mixers/fake.py index f78c7c64..98afca2a 100644 --- a/mopidy/audio/mixers/fake.py +++ b/mopidy/audio/mixers/fake.py @@ -6,7 +6,8 @@ None **Configuration** -Set the :confval:`audio/mixe:` config value to ``fakemixer`` to use this mixer. +Set the :confval:`audio/mixer:` config value to ``fakemixer`` to use this +mixer. """ from __future__ import unicode_literals From 130347f9940b60fa5aa48024f0eb1b9abceaa8a6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 01:04:47 +0200 Subject: [PATCH 30/45] docs: Update config page howtos --- docs/{settings.rst => config.rst} | 147 +++++++++++++++++------------- docs/index.rst | 2 +- docs/installation/index.rst | 14 +-- 3 files changed, 91 insertions(+), 72 deletions(-) rename docs/{settings.rst => config.rst} (58%) diff --git a/docs/settings.rst b/docs/config.rst similarity index 58% rename from docs/settings.rst rename to docs/config.rst index e16cfbfd..cd75e6dc 100644 --- a/docs/settings.rst +++ b/docs/config.rst @@ -1,32 +1,38 @@ -******** -Settings -******** +************* +Configuration +************* -Mopidy has lots of settings. Luckily, you only need to change a few, and stay -ignorant of the rest. Below you can find guides for typical configuration -changes you may want to do, and a complete listing of available settings. +Mopidy has quite a few config values to tweak. Luckily, you only need to change +a few, and stay ignorant of the rest. Below you can find guides for typical +configuration changes you may want to do, and a listing of the available config +values. -Changing settings -================= +Changing configuration +====================== -Mopidy reads settings from the file ``~/.config/mopidy/settings.py``, where -``~`` means your *home directory*. If your username is ``alice`` and you are -running Linux, the settings file should probably be at -``/home/alice/.config/mopidy/settings.py``. +Mopidy primarily reads config from the file ``~/.config/mopidy/mopidy.conf``, +where ``~`` means your *home directory*. If your username is ``alice`` and you +are running Linux, the settings file should probably be at +``/home/alice/.config/mopidy/mopidy.conf``. -You can either create the settings file yourself, or run the ``mopidy`` +You can either create the configuration file yourself, or run the ``mopidy`` command, and it will create an empty settings file for you. -When you have created the settings file, open it in a text editor, and add +When you have created the configuration file, open it in a text editor, and add settings you want to change. If you want to keep the default value for a setting, you should *not* redefine it in your own settings file. -A complete ``~/.config/mopidy/settings.py`` may look as simple as this:: +A complete ``~/.config/mopidy/mopidy.conf`` may look as simple as this: - MPD_SERVER_HOSTNAME = u'::' - SPOTIFY_USERNAME = u'alice' - SPOTIFY_PASSWORD = u'mysecret' +.. code-block:: ini + + [mpd] + hostname = :: + + [spotify] + username = alice + password = mysecret .. _music-from-spotify: @@ -35,10 +41,16 @@ Music from Spotify ================== If you are using the Spotify backend, which is the default, enter your Spotify -Premium account's username and password into the file, like this:: +Premium account's username and password into the file, like this: - SPOTIFY_USERNAME = u'myusername' - SPOTIFY_PASSWORD = u'mysecret' +.. code-block:: ini + + [spotify] + username = myusername + password = mysecret + +This will only work if you have the Spotify Premium subscription. Spotify +Unlimited will not work. .. _music-from-local-storage: @@ -48,9 +60,8 @@ Music from local storage If you want use Mopidy to play music you have locally at your machine instead of or in addition to using Spotify, you need to review and maybe change some of -the ``LOCAL_*`` settings. See :mod:`mopidy.settings`, for a full list of -available settings. Then you need to generate a tag cache for your local -music... +the local backend config values. See :ref:`local-backend`, for a complete list. +Then you need to generate a tag cache for your local music... .. _generating-a-tag-cache: @@ -58,28 +69,26 @@ music... Generating a tag cache ---------------------- -Before Mopidy 0.3 the local storage backend relied purely on ``tag_cache`` -files generated by the original MPD server. To remedy this the command -:command:`mopidy-scan` was created. The program will scan your current -:attr:`mopidy.settings.LOCAL_MUSIC_PATH` and build a MPD compatible -``tag_cache``. +The program :command:`mopidy-scan` will scan the path set in the +:confval:`local/media_dir` config value for any media files and build a MPD +compatible ``tag_cache``. To make a ``tag_cache`` of your local music available for Mopidy: -#. Ensure that :attr:`mopidy.settings.LOCAL_MUSIC_PATH` points to where your +#. Ensure that the :confval:`local/media_dir` config value points to where your music is located. Check the current setting by running:: - mopidy --list-settings + mopidy --show-config -#. Scan your music library. The command outputs the ``tag_cache`` to - ``stdout``, which means that you will need to redirect the output to a file - yourself:: +#. Scan your media library. The command outputs the ``tag_cache`` to + standard output, which means that you will need to redirect the output to a + file yourself:: mopidy-scan > tag_cache #. Move the ``tag_cache`` file to the location - :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE` is set to, or change the - setting to point to where your ``tag_cache`` file is. + set in the :confval:`local/tag_cache_file` config value, or change the + config value to point to where your ``tag_cache`` file is. #. Start Mopidy, find the music library in a client, and play some local music! @@ -91,14 +100,14 @@ Connecting from other machines on the network As a secure default, Mopidy only accepts connections from ``localhost``. If you want to open it for connections from other machines on your network, see -the documentation for :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`. +the documentation for the :confval:`mpd/hostname` config value. If you open up Mopidy for your local network, you should consider turning on -MPD password authentication by setting -:attr:`mopidy.settings.MPD_SERVER_PASSWORD` to the password you want to use. -If the password is set, Mopidy will require MPD clients to provide the password -before they can do anything else. Mopidy only supports a single password, and -do not support different permission schemes like the original MPD server. +MPD password authentication by setting the :confval:`mpd/password` config value +to the password you want to use. If the password is set, Mopidy will require +MPD clients to provide the password before they can do anything else. Mopidy +only supports a single password, and do not support different permission +schemes like the original MPD server. Scrobbling tracks to Last.fm @@ -107,10 +116,13 @@ Scrobbling tracks to Last.fm If you want to submit the tracks you are playing to your `Last.fm `_ profile, make sure you've installed the dependencies found at :mod:`mopidy.frontends.scrobbler` and add the following to your -settings file:: +settings file: - LASTFM_USERNAME = u'myusername' - LASTFM_PASSWORD = u'mysecret' +.. code-block:: ini + + [scrobbler] + username = myusername + password = mysecret .. _install-desktop-file: @@ -137,7 +149,7 @@ in the Ubuntu Sound Menu, and may be restarted by selecting it there. The Ubuntu Sound Menu interacts with Mopidy's MPRIS frontend, :mod:`mopidy.frontends.mpris`. The MPRIS frontend supports the minimum requirements of the `MPRIS specification `_. The -``TrackList`` and the ``Playlists`` interfaces of the spec are not supported. +``TrackList`` interface of the spec is not supported. Using a custom audio sink @@ -161,13 +173,16 @@ sound from Mopidy either, as Mopidy by default uses GStreamer's against Mopidy. If you for some reason want to use some other GStreamer audio sink than -``autoaudiosink``, you can set the setting :attr:`mopidy.settings.OUTPUT` to a +``autoaudiosink``, you can set the :confval:`audio/output` config value to a partial GStreamer pipeline description describing the GStreamer sink you want to use. -Example of ``settings.py`` for using OSS4:: +Example ``mopidy.conf`` for using OSS4: - OUTPUT = u'oss4sink' +.. code-block:: ini + + [audio] + output = oss4sink Again, this is the equivalent of the following ``gst-inspect`` command, so make this work first:: @@ -186,33 +201,37 @@ server simultaneously. To use the SHOUTcast output, do the following: #. Install, configure and start the Icecast server. It can be found in the ``icecast2`` package in Debian/Ubuntu. -#. Set :attr:`mopidy.settings.OUTPUT` to ``lame ! shout2send``. An Ogg Vorbis - encoder could be used instead of the lame MP3 encoder. +#. Set the :confval:`audio/output` config value to ``lame ! shout2send``. An + Ogg Vorbis encoder could be used instead of the lame MP3 encoder. #. You might also need to change the ``shout2send`` default settings, run ``gst-inspect-0.10 shout2send`` to see the available settings. Most likely you want to change ``ip``, ``username``, ``password``, and ``mount``. For example, to set the username and password, use: - ``lame ! shout2send username="foobar" password="s3cret"``. + + .. code-block:: ini + + [audio] + output = lame ! shout2send username="alice" password="secret" Other advanced setups are also possible for outputs. Basically, anything you can use with the ``gst-launch-0.10`` command can be plugged into -:attr:`mopidy.settings.OUTPUT`. +:confval:`audio/output`. -Custom settings -=============== +Custom configuration values +=========================== -Mopidy's settings validator will stop you from defining any settings in your -settings file that Mopidy doesn't know about. This may sound obnoxious, but it -helps you detect typos in your settings, and deprecated settings that should be -removed or updated. +Mopidy's settings validator will stop you from defining any config values in +your settings file that Mopidy doesn't know about. This may sound obnoxious, +but it helps us detect typos in your settings, and deprecated settings that +should be removed or updated. -If you're extending Mopidy in some way, and want to use Mopidy's settings -system, you can prefix your settings with ``CUSTOM_`` to get around the -settings validator. We recommend that you choose names like -``CUSTOM_MYAPP_MYSETTING`` so that multiple custom extensions to Mopidy can be -used at the same time without any danger of naming collisions. +If you're extending Mopidy, and want to use Mopidy's configuration +system, you can add new sections to the config without triggering the config +validator. We recommend that you choose a good and unique name for the config +section so that multiple extensions to Mopidy can be used at the same time +without any danger of naming collisions. Available settings diff --git a/docs/index.rst b/docs/index.rst index 54745298..aae7e675 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -42,7 +42,7 @@ User documentation installation/index installation/raspberrypi - settings + config running clients/index authors diff --git a/docs/installation/index.rst b/docs/installation/index.rst index ab81b753..2eade257 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -42,7 +42,7 @@ in the same way as you get updates to the rest of your distribution. sudo apt-get update sudo apt-get install mopidy -#. Finally, you need to set a couple of :doc:`settings `, and then +#. Finally, you need to set a couple of :doc:`config values `, and then you're ready to :doc:`run Mopidy `. When a new release of Mopidy is out, and you can't wait for you system to @@ -89,8 +89,8 @@ package found in AUR. install `python2-pylast `_ from AUR. -#. Finally, you need to set a couple of :doc:`settings `, and then - you're ready to :doc:`run Mopidy `. +#. Finally, you need to set a couple of :doc:`config values `, and + then you're ready to :doc:`run Mopidy `. OS X: Install from Homebrew and Pip @@ -140,8 +140,8 @@ Pip. sudo pip install -U pyspotify pylast mopidy -#. Finally, you need to set a couple of :doc:`settings `, and then - you're ready to :doc:`run Mopidy `. +#. Finally, you need to set a couple of :doc:`config values `, and + then you're ready to :doc:`run Mopidy `. Otherwise: Install from source using Pip @@ -264,5 +264,5 @@ can install Mopidy from PyPI using Pip. sudo pip install mopidy==dev -#. Finally, you need to set a couple of :doc:`settings `, and then - you're ready to :doc:`run Mopidy `. +#. Finally, you need to set a couple of :doc:`config values `, and + then you're ready to :doc:`run Mopidy `. From f9f80e264e872b6815d7818a4a08d7aaa420d694 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 11:05:32 +0200 Subject: [PATCH 31/45] local: Create empty tag_cache if it doesn't exist --- mopidy/backends/local/library.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index a76ce594..148ff24f 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -1,9 +1,11 @@ from __future__ import unicode_literals import logging +import os from mopidy.backends import base from mopidy.models import Album, SearchResult +from mopidy.utils.encoding import locale_decode from .translator import parse_mpd_tag_cache @@ -19,12 +21,23 @@ class LocalLibraryProvider(base.BaseLibraryProvider): self.refresh() def refresh(self, uri=None): - tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir) + try: + if not os.path.exists(self._tag_cache_file): + basedir = os.path.dirname(self._tag_cache_file) + if not os.path.exists(basedir): + os.makedirs(basedir) + open(self._tag_cache_file, 'a').close() + except IOError as error: + logger.warning( + 'Could not create empty tag cache: %s', locale_decode(error)) + return logger.info( 'Loading tracks from %s using %s', self._media_dir, self._tag_cache_file) + tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir) + for track in tracks: self._uri_mapping[track.uri] = track From 796f7302aa97767aaef6a9ca8f8be6b09f21ac13 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 11:05:55 +0200 Subject: [PATCH 32/45] local: Log how many tracks and playlists are loaded --- mopidy/backends/local/library.py | 8 ++++---- mopidy/backends/local/playlists.py | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index 148ff24f..f7a1225a 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -32,15 +32,15 @@ class LocalLibraryProvider(base.BaseLibraryProvider): 'Could not create empty tag cache: %s', locale_decode(error)) return - logger.info( - 'Loading tracks from %s using %s', - self._media_dir, self._tag_cache_file) - tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir) for track in tracks: self._uri_mapping[track.uri] = track + logger.info( + 'Loaded %d local tracks from %s using %s', + len(tracks), self._media_dir, self._tag_cache_file) + def lookup(self, uri): try: return [self._uri_mapping[uri]] diff --git a/mopidy/backends/local/playlists.py b/mopidy/backends/local/playlists.py index 3b9e1d73..cd370eaa 100644 --- a/mopidy/backends/local/playlists.py +++ b/mopidy/backends/local/playlists.py @@ -42,8 +42,6 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): return playlist def refresh(self): - logger.info('Loading playlists from %s', self._playlists_dir) - playlists = [] for m3u in glob.glob(os.path.join(self._playlists_dir, '*.m3u')): @@ -65,6 +63,10 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider): self.playlists = playlists listener.BackendListener.send('playlists_loaded') + logger.info( + 'Loaded %d local playlists from %s', + len(playlists), self._playlists_dir) + def save(self, playlist): assert playlist.uri, 'Cannot save playlist without URI' From da7e36d94465ff3842b6cd1774a4cfbf66dc6a4d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 11:06:24 +0200 Subject: [PATCH 33/45] spotify: Use 'playlists' instead of 'playlist(s)' for consistency --- mopidy/backends/spotify/session_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index a830138f..c0592ea7 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -182,7 +182,7 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): bitrate=self.bitrate, username=self.username)) playlists = filter(None, playlists) self.backend.playlists.playlists = playlists - logger.info('Loaded %d Spotify playlist(s)', len(playlists)) + logger.info('Loaded %d Spotify playlists', len(playlists)) BackendListener.send('playlists_loaded') def logout(self): From 919becd05a38300268686c40a5087bcd3010c33c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 11:07:52 +0200 Subject: [PATCH 34/45] main: Show 'Disabled extensions: none' instead of nothing --- mopidy/__main__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 6a691cce..2607db13 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -236,8 +236,10 @@ def filter_enabled_extensions(raw_config, extensions): else: disabled_names.append(extension.ext_name) - logging.info('Enabled extensions: %s', ', '.join(enabled_names)) - logging.info('Disabled extensions: %s', ', '.join(disabled_names)) + logging.info( + 'Enabled extensions: %s', ', '.join(enabled_names) or 'none') + logging.info( + 'Disabled extensions: %s', ', '.join(disabled_names) or 'none') return enabled_extensions From 0b09ea34110a0a01b5860d8e98bc16e5d552c6d1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 11:21:25 +0200 Subject: [PATCH 35/45] main: Log what backends/frontends are started --- mopidy/__main__.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 2607db13..2709d519 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -327,12 +327,19 @@ def stop_audio(): def setup_backends(config, extensions, audio): - logger.info('Starting Mopidy backends') - backends = [] + backend_classes = [] for extension in extensions: - for backend_class in extension.get_backend_classes(): - backend = backend_class.start(config=config, audio=audio).proxy() - backends.append(backend) + backend_classes.extend(extension.get_backend_classes()) + + logger.info( + 'Starting Mopidy backends: %s', + ', '.join(b.__name__ for b in backend_classes)) + + backends = [] + for backend_class in backend_classes: + backend = backend_class.start(config=config, audio=audio).proxy() + backends.append(backend) + return backends @@ -354,10 +361,16 @@ def stop_core(): def setup_frontends(config, extensions, core): - logger.info('Starting Mopidy frontends') + frontend_classes = [] for extension in extensions: - for frontend_class in extension.get_frontend_classes(): - frontend_class.start(config=config, core=core) + frontend_classes.extend(extension.get_frontend_classes()) + + logger.info( + 'Starting Mopidy frontends: %s', + ', '.join(f.__name__ for f in frontend_classes)) + + for frontend_class in frontend_classes: + frontend_class.start(config=config, core=core) def stop_frontends(extensions): From 2ffef53b9a00a83f12aa085c05750365b161bff9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 11:22:56 +0200 Subject: [PATCH 36/45] mpris: Log who connected to D-Bus --- mopidy/frontends/mpris/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index 696e39bd..a7f049d2 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -93,7 +93,7 @@ class MprisObject(dbus.service.Object): mainloop = dbus.mainloop.glib.DBusGMainLoop() bus_name = dbus.service.BusName( BUS_NAME, dbus.SessionBus(mainloop=mainloop)) - logger.info('Connected to D-Bus') + logger.info('MPRIS server connected to D-Bus') return bus_name def get_playlist_id(self, playlist_uri): From 3b7d38e8bcf1efda324e5cc863fd5db50f29c8ed Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 11:23:06 +0200 Subject: [PATCH 37/45] scrobbler: Log who connected to Last.fm --- mopidy/frontends/scrobbler/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/scrobbler/actor.py b/mopidy/frontends/scrobbler/actor.py index 1809661a..74a11f82 100644 --- a/mopidy/frontends/scrobbler/actor.py +++ b/mopidy/frontends/scrobbler/actor.py @@ -32,7 +32,7 @@ class ScrobblerFrontend(pykka.ThreadingActor, CoreListener): api_key=API_KEY, api_secret=API_SECRET, username=self.config['scrobbler']['username'], password_hash=pylast.md5(self.config['scrobbler']['password'])) - logger.info('Connected to Last.fm') + logger.info('Scrobbler connected to Last.fm') except (pylast.NetworkError, pylast.MalformedResponseError, pylast.WSError) as e: logger.error('Error during Last.fm setup: %s', e) From 339fbbc2ddbe1d502c7e9cf34d5f1fbddad11dee Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 12:20:36 +0200 Subject: [PATCH 38/45] main: Use 'none' instead of emptry string Same logic as for extensions are now applied to backends and frontends. --- mopidy/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 2709d519..d5f8c93e 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -333,7 +333,7 @@ def setup_backends(config, extensions, audio): logger.info( 'Starting Mopidy backends: %s', - ', '.join(b.__name__ for b in backend_classes)) + ', '.join(b.__name__ for b in backend_classes) or 'none') backends = [] for backend_class in backend_classes: @@ -367,7 +367,7 @@ def setup_frontends(config, extensions, core): logger.info( 'Starting Mopidy frontends: %s', - ', '.join(f.__name__ for f in frontend_classes)) + ', '.join(f.__name__ for f in frontend_classes) or 'none') for frontend_class in frontend_classes: frontend_class.start(config=config, core=core) From 40dd539d061040f99da00aa955a7ce3cf78c9763 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 12:38:47 +0200 Subject: [PATCH 39/45] path: Test get_or_create_file() --- tests/utils/path_test.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index a73cb8e4..d40c822f 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -24,7 +24,6 @@ class GetOrCreateDirTest(unittest.TestCase): def test_creating_dir(self): dir_path = os.path.join(self.parent, 'test') self.assert_(not os.path.exists(dir_path)) - self.assert_(not os.path.isdir(dir_path)) created = path.get_or_create_dir(dir_path) self.assert_(os.path.exists(dir_path)) self.assert_(os.path.isdir(dir_path)) @@ -34,9 +33,7 @@ class GetOrCreateDirTest(unittest.TestCase): level2_dir = os.path.join(self.parent, 'test') level3_dir = os.path.join(self.parent, 'test', 'test') self.assert_(not os.path.exists(level2_dir)) - self.assert_(not os.path.isdir(level2_dir)) self.assert_(not os.path.exists(level3_dir)) - self.assert_(not os.path.isdir(level3_dir)) created = path.get_or_create_dir(level3_dir) self.assert_(os.path.exists(level2_dir)) self.assert_(os.path.isdir(level2_dir)) @@ -57,6 +54,35 @@ class GetOrCreateDirTest(unittest.TestCase): self.assertRaises(OSError, path.get_or_create_dir, dir_path) +class GetOrCreateFileTest(unittest.TestCase): + def setUp(self): + self.parent = tempfile.mkdtemp() + + def tearDown(self): + if os.path.isdir(self.parent): + shutil.rmtree(self.parent) + + def test_creating_file(self): + file_path = os.path.join(self.parent, 'test') + self.assert_(not os.path.exists(file_path)) + created = path.get_or_create_file(file_path) + self.assert_(os.path.exists(file_path)) + self.assert_(os.path.isfile(file_path)) + self.assertEqual(created, file_path) + + def test_creating_existing_file(self): + file_path = os.path.join(self.parent, 'test') + path.get_or_create_file(file_path) + created = path.get_or_create_file(file_path) + self.assert_(os.path.exists(file_path)) + self.assert_(os.path.isfile(file_path)) + self.assertEqual(created, file_path) + + def test_create_file_with_name_of_existing_dir_throws_ioerror(self): + conflicting_dir = os.path.join(self.parent) + self.assertRaises(IOError, path.get_or_create_file, conflicting_dir) + + class PathToFileURITest(unittest.TestCase): def test_simple_path(self): if sys.platform == 'win32': From 53827aa0227fd81e03f0ffe60766f4df8c09639e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 12:39:29 +0200 Subject: [PATCH 40/45] path: Close create file right away --- mopidy/utils/path.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 699b361f..8f842741 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -39,12 +39,12 @@ def get_or_create_dir(dir_path): return dir_path -def get_or_create_file(filename): - filename = expand_path(filename) - if not os.path.isfile(filename): - logger.info('Creating file %s', filename) - open(filename, 'w') - return filename +def get_or_create_file(file_path): + file_path = expand_path(file_path) + if not os.path.isfile(file_path): + logger.info('Creating file %s', file_path) + open(file_path, 'w').close() + return file_path def path_to_uri(*paths): From 02f9db451836ee1e6de0459ba7e6a3480dfe7650 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 12:40:28 +0200 Subject: [PATCH 41/45] path: Let get_or_create_file() create missing dirs --- mopidy/utils/path.py | 1 + tests/utils/path_test.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 8f842741..2ad51368 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -41,6 +41,7 @@ def get_or_create_dir(dir_path): def get_or_create_file(file_path): file_path = expand_path(file_path) + get_or_create_dir(os.path.dirname(file_path)) if not os.path.isfile(file_path): logger.info('Creating file %s', file_path) open(file_path, 'w').close() diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index d40c822f..9d1c16d3 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -70,6 +70,18 @@ class GetOrCreateFileTest(unittest.TestCase): self.assert_(os.path.isfile(file_path)) self.assertEqual(created, file_path) + def test_creating_nested_file(self): + level2_dir = os.path.join(self.parent, 'test') + file_path = os.path.join(self.parent, 'test', 'test') + self.assert_(not os.path.exists(level2_dir)) + self.assert_(not os.path.exists(file_path)) + created = path.get_or_create_file(file_path) + self.assert_(os.path.exists(level2_dir)) + self.assert_(os.path.isdir(level2_dir)) + self.assert_(os.path.exists(file_path)) + self.assert_(os.path.isfile(file_path)) + self.assertEqual(created, file_path) + def test_creating_existing_file(self): file_path = os.path.join(self.parent, 'test') path.get_or_create_file(file_path) From 87b526f8d3324236ecd0ca4a1723bef2fac7d094 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 12:41:07 +0200 Subject: [PATCH 42/45] main: Remove redundant dir creation --- mopidy/__main__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index d5f8c93e..7e1677ef 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -312,7 +312,6 @@ def validate_config(raw_config, schemas, extensions=None): def create_file_structures(): path.get_or_create_dir('$XDG_DATA_DIR/mopidy') - path.get_or_create_dir('$XDG_CONFIG_DIR/mopidy') path.get_or_create_file('$XDG_CONFIG_DIR/mopidy/mopidy.conf') From 172b6f046131e9735ab4310e8d499bea16fe2131 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 12:44:16 +0200 Subject: [PATCH 43/45] local: Use path helpers to create tag cache --- mopidy/backends/local/library.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index f7a1225a..6b953823 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -1,11 +1,9 @@ from __future__ import unicode_literals import logging -import os - from mopidy.backends import base from mopidy.models import Album, SearchResult -from mopidy.utils.encoding import locale_decode +from mopidy.utils import encoding, path from .translator import parse_mpd_tag_cache @@ -22,14 +20,11 @@ class LocalLibraryProvider(base.BaseLibraryProvider): def refresh(self, uri=None): try: - if not os.path.exists(self._tag_cache_file): - basedir = os.path.dirname(self._tag_cache_file) - if not os.path.exists(basedir): - os.makedirs(basedir) - open(self._tag_cache_file, 'a').close() - except IOError as error: + path.get_or_create_file(self._tag_cache_file) + except EnvironmentError as error: logger.warning( - 'Could not create empty tag cache: %s', locale_decode(error)) + 'Could not create empty tag cache: %s', + encoding.locale_decode(error)) return tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir) From bc959e2240f708b6f55b9c51de3b7bd8b7796e54 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 12:44:35 +0200 Subject: [PATCH 44/45] local: Move data files into an extension specific dir --- mopidy/backends/local/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index ffcf5869..0367cf15 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -9,8 +9,8 @@ default_config = """ [local] enabled = true media_dir = $XDG_MUSIC_DIR -playlists_dir = $XDG_DATA_DIR/mopidy/playlists -tag_cache_file = $XDG_DATA_DIR/mopidy/tag_cache +playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists +tag_cache_file = $XDG_DATA_DIR/mopidy/local/tag_cache """ __doc__ = """A backend for playing music from a local music archive. From bf83a159fedb54d9a12cf24f28ba012d9445f457 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Apr 2013 12:51:34 +0200 Subject: [PATCH 45/45] local: Create all needed dirs/files on startup --- mopidy/backends/local/actor.py | 25 +++++++++++++++++++++++++ mopidy/backends/local/library.py | 11 +++-------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/mopidy/backends/local/actor.py b/mopidy/backends/local/actor.py index a1655dd9..1817e133 100644 --- a/mopidy/backends/local/actor.py +++ b/mopidy/backends/local/actor.py @@ -5,6 +5,7 @@ import logging import pykka from mopidy.backends import base +from mopidy.utils import encoding, path from .library import LocalLibraryProvider from .playlists import LocalPlaylistsProvider @@ -18,8 +19,32 @@ class LocalBackend(pykka.ThreadingActor, base.Backend): self.config = config + self.create_dirs_and_files() + self.library = LocalLibraryProvider(backend=self) self.playback = base.BasePlaybackProvider(audio=audio, backend=self) self.playlists = LocalPlaylistsProvider(backend=self) self.uri_schemes = ['file'] + + def create_dirs_and_files(self): + try: + path.get_or_create_dir(self.config['local']['media_dir']) + except EnvironmentError as error: + logger.warning( + 'Could not create local media dir: %s', + encoding.locale_decode(error)) + + try: + path.get_or_create_dir(self.config['local']['playlists_dir']) + except EnvironmentError as error: + logger.warning( + 'Could not create local playlists dir: %s', + encoding.locale_decode(error)) + + try: + path.get_or_create_file(self.config['local']['tag_cache_file']) + except EnvironmentError as error: + logger.warning( + 'Could not create empty tag cache file: %s', + encoding.locale_decode(error)) diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py index 6b953823..669e72d7 100644 --- a/mopidy/backends/local/library.py +++ b/mopidy/backends/local/library.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import logging from mopidy.backends import base from mopidy.models import Album, SearchResult -from mopidy.utils import encoding, path from .translator import parse_mpd_tag_cache @@ -19,13 +18,9 @@ class LocalLibraryProvider(base.BaseLibraryProvider): self.refresh() def refresh(self, uri=None): - try: - path.get_or_create_file(self._tag_cache_file) - except EnvironmentError as error: - logger.warning( - 'Could not create empty tag cache: %s', - encoding.locale_decode(error)) - return + logger.debug( + 'Loading local tracks from %s using %s', + self._media_dir, self._tag_cache_file) tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir)